개발공부/JPA 스터디

[ORM 표준 JPA] 16장(완결) 2차 캐시와 락

klyhyeon 2021. 9. 5. 23:41
728x90

트랜잭션과 락, 2차 캐시

1) 트랜잭션과 락

트랜잭션은 ACID를 보장해야합니다.

  • A(Atomicity) 원자성: 트랜잭션 내 실행한 작업들을 마치 하나인 것처럼 모두 실패하거나 성공해야 합니다.
  • C(Consistency) 일관성: 모든 트랜잭션은 일관된 DB를 만족해야 합니다. 데이터 무결성
  • I(Isolation) 격리성: 동시에 실행되는 트랜잭션은 서로에 영향을 미치지 않도록 격리합니다. 격리성은 동시성과 관련된 성능 이슈
    로 격리 수준을 선택할 수 있습니다.
  • D(Durability) 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 생겨도
    DB 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 합니다.

참고로 트랜잭션은 원자성, 일관성, 지속성을 보장합니다. 격리성을 완벽히 보장하려면 커밋을 하나씩 처리해야하는데
이럴 경우 동시성 처리 성능이 매우 나빠집니다. ANSI(미국표준협회) 표준은 트랜잭션의 격리수준을 4단계로 나누었습니다.
순서대로 격시성이 낮음 -> 높음 입니다.

image

낙관적 락과 비관적 락 기초

말그래도 낙관적 락은 충돌이 발생하지 않는다는 과정하에 락을 걸고, 비관적 락은 보수적으로 우선 락을 거는 것을 말합니다.
트랜잭션 시 second lost updates problem 두 번의 갱신 분실 문제가 발생할 수 있는데, 이때는 3가지 방법이 있습니다.

e.g. 두 명이 같은 화면에서 작업하다 A가 먼저 수정완료 했지만 뒤이어 B가 수정완료를 해 나중에 완료한 B의 수정사항만 남게되는
문제입니다.

  • 마지막 커밋만 인정하기
  • 최초 커밋만 인정하기
    • @Version : JPA는 UPDATE쿼리가 나갈때마다 VERSION필드도 함께 UPDATE 하며 조회한 VERSION값이 없을때 충돌이 발생하며
      최초 커밋만 인정됩니다.
  • 충돌하는 갱신 내용 병합하기

JPA에서 권장하는 락 설정

추천하는 전략은 READ COMMITTED 격리 수준 + 낙관적 버전관리 입니다. LockModeType.OPTIMISTIC
JPA는 @Version을 사용해 낙관적 락을 설정합니다.

JPA 비관적 락

비관적 락은 엔티티가 아닌 스칼라 타입을 조회할 때에도 사용할 수 있습니다. 비관적 락을 사용하면 락을 획득할 때까지
대기해야되기 때문에 타임아웃을 설정해 무한정 대기를 방지할 수도 있습니다.

2) 2차 캐시

네트워크를 통해 DB를 접근하는 방식은 서버에서 내부 메모리를 접근하는 것보다 시간비용이 수만에서 수십만 배 이상 높습니다.
따라서 DB를 조회해서 내부 메모리 캐시에 저장해두면 비용을 아낄 수 있습니다.

트랜잭션이 종료될 때까지만 유효하거나, OSIV를 사용해도 클라이언트의 요청이 올때까지만 유효나 1차 캐시의 한계를
개선해 애플리케이션의 범위의 캐시를 지원하는데 이를 공유 캐시 혹은 2차 캐시라고 합니다.

image

1차 캐시

영속성 컨텍스트 그 자체여서 직접 생성/소멸을 컨트롤 할 수 없다는 한계가 있습니다. 특징은 엔티티를 조회했을 때 1차 캐시에 동일한
엔티티가 있을 경우 해당 엔티티를 반환하기 때문에 동일성을 보장합니다.

2차 캐시

1차 캐시와 달리 애플리케이션 종료까지 유지되며 분산 캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수 있습니다.
2차 캐시는 조회하는 엔티티가 있다면 복사본을 제공합니다. 이유는 동시성을 극대화하기 위함인데 애플리케이션 전체 범위에서 같은
객체를 수정할 경우 객체에 락을 걸어야 하는데 그럴 경우 동시성이 떨어지기 때문입니다. 락에 비하면 객체를 복사하는 비용은 저렴
하기 때문에 2차 캐시는 원본 대신에 복사본을 반환합니다.

image

2차 캐시 사용

엔티티 클래스에 @Cacheable 어노테이션을 사용하면 됩니다. 캐시 모드는 javax.persistence.SharedCacheMode에 정의되어 있습니다.
캐시 모드에 따라 캐시를 무시할수도 있고 더 세밀하게 캐시 사용을 설정할 수 있습니다.

캐시 영역

캐시를 하면 엔티티는 엔티티 캐시 영역, 연관 참조 엔티티는 컬렉션 캐시 영역에 저장됩니다.

쿼리 캐시

쿼리 캐시는 쿼리와 파라미터 정보를 키로 활용해서 쿼리 결과를 캐시하는 방법입니다. 적용하기 위해선
org.hibernate.cacheable = true로 설정해줘야 합니다.

쿼리/컬렉션 캐시를 대상 엔티티 캐시와 함께 쓰지 않으면 심각한 성능적 이슈가 발생할 수 있습니다. 이유는 쿼리 캐시는 엔티티 식별자 값만
들어있기 때문에 실제 엔티티 정보는 엔티티 캐시를 통해 하나씩 조회해오기 때문에 꼭 같이 사용해줘야 합니다.

엔티티 캐시 테스트

스프링 프레임워크 StopWatch 라이브러리를 사용해서 Cache 했을 때, 하지 않을 때 Task Time 차이를 테스트 해보려고 했으나
2차 캐시 검증에 실패중입니다. EHCACHE@Cacheable, @Cache 사용법을 익혀서 테스트에 성공한 뒤 업뎃하겠습니다.


@Test
@DisplayName("실패한_2차캐시_테스트")
    void 실패한_2차캐시_테스트() {
          parentRepository.findById(1L);
          StopWatch stopWatch = new StopWatch();
          stopWatch.start("cacheTest");
          log.info("task: " + stopWatch.currentTaskName());
          //캐시했을 때 stopwatch
          parentRepository.findById(1L);
          stopWatch.stop();
          log.info("task time: " + stopWatch.getLastTaskTimeMillis());
    }

Hibernate: 
    select
        parent0_.id as id1_4_0_,
        parent0_.name as name2_4_0_ 
    from
        parent parent0_ 
    where
        parent0_.id=?
2021-09-05 22:53:37.319  INFO 13756 --- [           main] l.jpa.repository.ParentRepositoryTest    : task time: 31

참고자료: 자바 ORM 표준 JPA 프로그래밍 (김영한 저)