[JPA] 스터디 - 16장 트랜잭션과 락, 2차 캐시

JPA 스터디 : 16. 트랜잭션과 락, 2차 캐시

16. 트랜잭션과 락, 2차 캐시

16.1 트랜잭션과 락

  • 트랜잭션 기초
  • JPA 낙관적 락
  • JPA 비관적 락

16.1.1 트랜잭션 & 격리수준

  • 트랜잭션의 네 가지 속성 : ACID
    1. 원자성 (Atomic) : All or Nothing
    2. 일관성 (Consistency) : 일관적인 데이터 유지. eg) DB 제약 조건 만족
    3. 격리성 (Isolation) : 트랜잭션 간 영향 미치지 X
    4. 지속성 (Durability) : 결과 저장
  • 문제는 격리성 : 격리성을 지키려면 동시성이 떨어짐 —-> 트랜잭션 격리 수준 네 단계
    1. READ UNCOMMITED
    2. READ COMMITED
    3. REPEATABLE READ
    4. SERIALIZABLE
  • 격리 수준 낮을수록 동시성 증가 : but, 문제 발생
격리수준DIRTY READNON-REPEATABLE READPHANTOM READ
READ UNCOMMITEDOOO
READ COMMITED OO
REPEATABLE READ  O
SERIALIZABLE   

16.1.2 낙관적 락과 비관적 락 기초

낙관적 락

  • 트랜잭션 대부분은 충돌이 발생하지 않는다 가정
  • JPA 제공 버전 관리 기능 사용 (= 애플리케이션이 제공하는 락)
  • 트랜잭션 커밋 전까지 트랜잭션의 충돌 알 수 X

비관적 락

  • 트랜잭션의 충돌이 발생하다 가정, 우선 락을 검
  • 데이터베이스가 제공하는 락 기능 사용
    • select for update 구문이 대표적인 예

두 번의 갱신 분실 문제

  • 두 트랜잭션 내 동일 데이터 변경 시 먼저 커밋한 내용이 분실되는 문제
  • 3가지 방법 존재
    1. 마지막 커밋만 인정
    2. 최초 커밋만 인정
    3. 충돌하는 갱신 내용 병합

16.1.3 @Version

  • 적용 가능 타입 : Long, Integer, Short, Timestamp
  • 버전 관리용 필드 추가 + @Version 어노테이션 추가
  • 수정할 때 마다 버전이 하나씩 자동 증가
    • 조회 시점의 버전과 수정 시점의 버전이 다르면 예외 발생
  • 버전 정보 사용 시 최초 커밋만 인정하기 적용

@Entity
public class Board {
    @Id
    private String id;
    //....

    @Version
    private Integer version;
}

버전 정보 비교 방법

UPDATE BOARD
SET
    TITLE=?
    VERSION=? (버전 + 1 증가)
WHERE
    ID=?
    AND VERSION=? (버전 비교)
  • 검색 조건 엔티티 버전 정보 추가
  • 버전 : 엔티티 값 변경 시 증가
    • 임베디드 타입과 값 타입 컬렉션 수정 시에도 증가
    • 연관관계 필드는 외래 키를 관리하는 연관관계의 주인 필드를 수정할 때만 버전 증가

16.1.4 JPA 락 사용

  • 락 적용 가능 위치
    • EntityManager.lock(), EntityManager.find(), EntityManager.refresh()
    • Query.setLockMode() : TypeQuery 포함
    • @NamedQuery
  • 락 옵션 : javax.persistence.LockModeType
    • 낙관적 락 : OPTIMISTIC(= READ), OPTIMISTIC_FORCE_INCREMENT(= WRITE)
    • 비관적 락 : PESSIMISTIC_READ, PESSIMISTIC_WRITE, PESSIMISTIC_FORCE_INCREMENT
    • 락 X : NONE

16.1.5 JPA 낙관적 락

  • JPA 제공 낙관적 락 : @Version 사용
    • 락 옵션 없어도 @Version만 있어도 낙관적 락 적용
  • 커밋 시점에 충돌 알 수 있음
  • 예외
    • javax.persistence.OptimisticLockException (JPA 예외)
    • org.hibernate.StaleObjectStateException (Hibernate 예외)
    • org.springframework.orm.ObjectOptimisticLockingFailureException (Spring 예외 추상화)

NONE

  • @Version 적용 필드 있으면 낙관적 락 적용
  • 용도 : 엔티티 수정할 때 다른 트랜잭션에 의해 변경, 삭제 불가. 조회 ~ 수정 시점 보장
  • 동작 : 엔티티 수정 시 버전 체크, 버전 증가 => DB 버전 값 불일치 시 예외 발생
  • 이점 : 두 번의 갱신 분실 문제 예방

OPTIMISTIC

  • 한 번 조회한 엔티티는 트랜잭션 종료할 때까지 다른 트랜잭션에서 변경하지 않음 보장
    • 엔티티를 수정하지 않고 단순히 조회만 해도 버전 확인
    • cf) @Version 만 있는 경우 엔티티를 수정해야 버전 확인
  • 용도 : 트랜잭션이 끝날 때까지 다른 트랜잭션이 변경 X. 조회 ~ 트랜잭션 끝 보장.
  • 동작 : 트랜잭션 커밋 시 버전 정보 조회, 버전 검증. => 불일치 시 예외
  • 이점 : DIRTY READ, NON-REPEATABLE READ 방지

OPTIMISTIC_FORCE_INCREMENT

  • 낙관적 락 사용 + 버전 정보 강제 증가
  • 용도 : 논리적 단위 엔티티 묶음 관리. 애그리거트 루트에 사용 가능.
  • 동작 : 엔티티 수정하지 않아도 트랜잭션 커밋 시 UPDATE 쿼리 사용해 버전 강제 증가. 추가로 엔티티 수정 시 버전 UPDATE 발생. (총 2번의 버전 증가 발생 가능)
  • 이점 : 논리적인 단위의 엔티티 묶음 버전 관리 가능

16.1.6 JPA 비관적 락

  • DB 트랜잭션 락 메커니즘에 의존. select for update 구문 주로 사용. 주로 PESSIMISTIC_WRITE 모드 사용
  • 특징
    1. 스칼라 타입 조회에도 사용 가능
    2. 데이터 수정 즉시 트랜잭션 충돌 감지
  • 예외
    • javax.persistence.PessimisticLockException (JPA 예외)
    • org.springframework.dao.PessimisticLockingFailureException (Spring 예외 추상화)

PESSIMISTIC_WRITE

  • 비관적 락의 일반적 옵션. DB 쓰기 락 걸 때 사용
  • 용도 : DB에 쓰기 락
  • 동작 : select for update 사용
  • 이점 : NON-REPEATABLE READ 방지.

PESSIMISTIC_READ

  • 데이터 반복 읽기 O, 수정 X 용도. 데이터베이스 대부분 PESSIMISTIC_WRITE로 동작
  • MySQL : lock in share mode
  • PostgreSQL : for share

PESSIMISTIC_FORCE_INCREMENT

  • 비관적 락 중 유일 버전 정보 사용. 버전 정보 강제 증가.
  • 하이버네이트는 nowait 지원 데이터베이스에 대해 for update nowait 옵션 적용
    • 오라클 : for update nowait
    • PostgreSQL : for udpate nowait
    • nowait 미지원 : for update 사용

16.1.7 비관적 락 & 타임아웃

  • 비관적 락 사용 시 락 획득까지 트랜잭션 대기 -> 타임아웃 설정 가능
  • javax.persistence.lock.timeout 세팅
  • https://www.baeldung.com/jpa-pessimistic-locking : lock scope, timeout, etc…

16.2 2차 캐시

16.2.1 1차 캐시 & 2차 캐시

1차 캐시

  1. 최초 조회 시 1차 캐시에 엔티티 X
  2. DB에서 엔티티 조회
  3. 1차 캐시에 보관
  4. 1차 캐시에 보관 결과 반환
  5. 이후 같은 엔티티 조회 시 1차 캐시 엔티티 그대로 반환
  • 영속성 컨텍스트 내부 엔티티 저장소
  • 일반적으로 트랜잭션 시작 ~ 종료 까지만 유효 (cf. OSIV : 클라이언트 요청 시작 ~ 끝)
  • 1차 캐시는 객체 동일성 보장

2차 캐시, 공유 캐시

  1. 영속성 컨텍스트는 엔티티가 필요하면 2차 캐시 조회
  2. 2차 캐시 엔티티 X, 데이터 베이스 조회
  3. 결과 2차 캐시에 보관
  4. 2차 캐시는 자신이 보관하고 있는 엔티티 복사 반환
  5. 2차 캐시에 저장되어 있는 엔티티 조회 시 복사복 만들어 반환
  • 애플리케이션 범위의 캐시, 분산/클러스터링 환경 캐시는 더 오래 유지 가능
  • Why 복사 반환?
    • 동일 객체 반환 시 여러 곳에서 같은 객체 동시 수정 문제 발생
  • 2차 캐시는 영속성 유닛 범위의 캐시
  • 2차 캐시는 복사본 반환
  • 2차 캐시는 DB 기본 키 기준 캐시, 영속성 컨텍스트가 다른 경우 객체 동일성 보장 X

16.2.2 JPA 2차 캐시 기능

캐시 모드 설정

  • javax.persistence.Cacheable 어노테이션 사용 : 기본값 true
@Cacheable
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    //....
}
  • 영속성 유닛 단위 캐시 어떻게 적용할지 shared-cache-mode 설정
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="sharedCacheMode" value="ENABLE_SELECTIVE"/>
    ...
캐시 모드설명
ALL모든 엔티티 캐시
NONE캐시 사용 X
ENABLE_SELECTIVECacheable(true) 설정된 엔티티만 캐시
DISABLE_SELECTIVE모든 엔티티 캐시, Cacheable(false)로 명시된 엔티티 캐시 X
UNSPECIFIEDJPA 구현체가 정의한 설정 따름

캐시 조회, 저장 방식 설정 (캐시 조회 모드, 캐시 보관 모드)

  • 캐시 조회 모드 : javax.persistence.cache.retrieveMode
    • 옵션 : javax.persistence.CacheretrieveMode
      1. USE : 캐시에서 조회. 기본값
      2. BYPASS : 캐시 무시. 데이터베이스 직접 접근
  • 캐시 보관 모드 : javax.persistence.cache.storeMode
    • 옵션 : javax.persistence.CacheStoreMode
      1. USE : 조회 데이터 캐시 저장. 기본값.
      2. BYPASS : 캐시 저장 X
      3. REFRESH : USE 전략 + 데이터베이스에서 조회한 엔티티 최신 상태 다시 캐시
  • 엔티티 매니저 단위 설정 가능. EntityManager.find()/.refresh()와 같이 상세 설정 가능. Query.setHint() 사용 가능
em.setProperty("javax.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS);
em.setProperty("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS);
Map<String, Object> param = new HashMap<String,Object>();
param.put("javax.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS);
param.put("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS);

em.find(TestEntity.class, id, param);
em.createQuery("select e from TestEntity e where e.id = :id",
    TestEntity.class)
    .setParameter("id", id)
    .setHint("javax.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS);
    .setHint("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS);
    .getSingleResult();

JPA 캐시 관리 API

  • JPA는 javax.persistence.Cahce 인터페이스 제공
  • EntityManagerFactory에서 가져올 수 있음
Cache cache = emf.getCache();
boolean contains = cache.contains(TestEntiy.class, testEntity.getId());
  • 기능
    • contains(Class cls, Object primaryKey) : 캐시에 있는지 확인
    • evict(Class cls, Object primaryKey) : 캐시 제거
    • evict(Class cls) : 엔티티의 전체 캐시 제거
    • evictAll() : 모든 캐시 데이터 제거

16.2.3 하이버네이트와 EHCACHE 적용

하이버네이트 지원 캐시 3가지

  1. 엔티티 캐시
    • 엔티티 단위 캐시. 식별자로 엔티티 조회. 컬렉션이 아닌 연관된 엔티티 로딩 시 사용.
  2. 컬렉션 캐시
    • 엔티티 연관 컬렉션 캐시. 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시
  3. 쿼리 캐시
    • 쿼리와 파라미터 정보를 키로 사용해 캐시. 결과가 엔티티면 식별자 값만 캐시

cf) JPA 표준은 엔티티 캐시만 정의되어 있음

EHCACHE 의존성 추가

<dependencies>
    <!-- other dependencies -->
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-ehcache</artifactId>
    </dependency>
</dependencies>

속성 설정

spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.use_query_cache=true
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory
spring.jpa.properties.hibernate.generate_statistics=true
  • hibernate.cache.use_second_level_cache
    • 2차 캐시 활성화. 엔티티 캐시, 컬렉션 캐시 사용 가능
  • hibernate.cache.use_query_cache
    • 쿼리 캐시 활성화
  • hibernate.cache.region.factory_class
    • 2차 캐시 처리할 클래스 지정
  • hibernate.generate_statistics
    • 통계정보 출력. 캐시 적용 여부 확인. (성능 영향 있어, 개발 환경에만 적용 추천)

엔티티 캐시 & 컬렉션 캐시

@Cacheable // 엔티티 캐시
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Entity
public class ParentMember {
    @Id @GeneratedValue
    private Long id;

    private Sring name;

    @Cache(usage = CacheConcurrencyStrategy.READ_WRTIE)
    @OneToMany(mappedBy = "parentMember", cascade = CascadeType.ALL)
    private List<ChildMember> childMembers = new ArrayList<ChildMember>();
}
  • javax.persistence.@Cacheable : 엔티티 캐시
  • org.hibernate.annotations.@Cache : 캐시 상세 설정 어노테이션. 컬렉션 캐시 적용 시 사용

@Cache

  • usage : CacheConcurrencyStrategy 사용해 캐시 동시성 전략 설정
  • region : 캐시 지역 설정
  • include : 연관 객체를 캐시에 포함할지 선택. all, non-lazy 옵션 중 선택. 기본값 all.

CacheConcurrencyStrategy

속성설명
NONE캐시 설정 X
READ_ONLY읽기 전용 설정. 등록, 삭제 가능. 수정 불가. 2차 캐시 조회 시 원본 객체 반환
NONSTRICT_READ_WRITE엄격하지 않은 읽고 쓰기 전략. 동시 같은 엔티티 수정 시 데이터 일관성 깨질 수 있음.
READ_WRITE읽기 쓰기 가능. READ COMMITTED 정도 격리 수준 보장.
TRANSACTIONAL컨테이너 관리 환경에서 사용 가능
  • Cacheprovider 에 따라 동시성 전략 지원 여부 상이
Cacheread-onlynonstrict-read-writeread-writetransactional
ConcurrentHashMapyesyesyes 
EHCACHEyesyesyesyes
Infinispanyes  yes

캐시 영역

  • 엔티티 캐시 영역 : 패키지명 + 클래스명
  • 컬렉션 캐시 영역 : 엔티티 캐시 영역 이름 + 컬렉션 필드 명
  • region 속성 통해 직접 지정 가능

쿼리 캐시

  • 키 : 쿼리 & 파라미터 정보
  • hibernate.cache.use_query_cache : true 설정
em.createQuery("select i from Item i", Item.class)
    .setHint("org.hibernate.cacheable", true)
    .getResultList();
@Entity
@NamedQuery(
    hints = @QueryHint(name = "org.hiberante.cacheable", value = "true"),
    name = "Member.findByUsername",
    query = "select m.address from Member m where m.name = :username"
)
public class Member {
    ...
}

쿼리 캐시 영역

  • org.hibernate.cache.internalStandardQueryCache : 쿼리 캐시 저장 영역. 쿼리, 쿼리 결과 집합, 쿼리 실행 시점 타임스탬프 보관
  • org.hibernate.cache.spi.UpdateTimestampsCache : 쿼리 캐시 유효 확인 위해 쿼리 대상 테이블의 가장 최근 변경(등록, 수정, 삭제) 시간 저장 영역. 테이블 명, 테이블 최근 변경 타임 스탬프 보관
    • 주의! 캐시 만료되지 않도록 설정해야 함

쿼리 캐시는 데이터 집합을 최신 데이터 유지 위해 쿼리 캐시 실행 시간 & 테이블 가장 최근 변경된 시간 비교. 쿼리 캐시 적용 후 쿼리 캐시 사용 테이블 변경 시 데이터베이스에서 데이트 조회 후 캐시 세팅.

수정이 거의 일어나지 않는 테이블에 사용해야 효과적

쿼리 캐시 & 컬렉션 캐시 주의점

쿼리 캐시, 컬렉션 캐시는 결과 집합의 식별자 값만 캐시
식별자 값을 하나씩 엔티티 캐시에서 조회해서 실제 엔티티 조회
쿼리, 컬렉션 캐시만 사용하고 대상 엔티티에 엔티티 캐시를 적용하지 않으면 성능상 심각한 문제 발생

  1. select m from Member m 쿼리 실행. 쿼리 캐시 적용. 결과 집합 100건
  2. 결과 집합에는 식별자만 있음. 한 건씩 엔티티 캐시 영역에서 조회
  3. Member 엔티티 캐시 사용 X. 한 건씩 데이터베이스에서 조회
  4. 100건의 SQL 실행

쿼리 캐시나 컬렉션 캐시를 사용하면 대상 엔티티에는 꼭 엔티티 캐시를 적용해야

참조

  • 자바 ORM 표준 JPA 프로그래밍, 김영한 지음