[JPA] 스터디 - 16장 트랜잭션과 락, 2차 캐시
JPA 스터디 : 16. 트랜잭션과 락, 2차 캐시
16. 트랜잭션과 락, 2차 캐시
16.1 트랜잭션과 락
- 트랜잭션 기초
- JPA 낙관적 락
- JPA 비관적 락
16.1.1 트랜잭션 & 격리수준
- 트랜잭션의 네 가지 속성 :
ACID
- 원자성 (Atomic) : All or Nothing
- 일관성 (Consistency) : 일관적인 데이터 유지. eg) DB 제약 조건 만족
- 격리성 (Isolation) : 트랜잭션 간 영향 미치지 X
- 지속성 (Durability) : 결과 저장
- 문제는
격리성
: 격리성을 지키려면 동시성이 떨어짐 —-> 트랜잭션 격리 수준 네 단계- READ UNCOMMITED
- READ COMMITED
- REPEATABLE READ
- SERIALIZABLE
- 격리 수준 낮을수록 동시성 증가 : but, 문제 발생
격리수준 | DIRTY READ | NON-REPEATABLE READ | PHANTOM READ |
---|---|---|---|
READ UNCOMMITED | O | O | O |
READ COMMITED | O | O | |
REPEATABLE READ | O | ||
SERIALIZABLE |
16.1.2 낙관적 락과 비관적 락 기초
낙관적 락
- 트랜잭션 대부분은 충돌이 발생하지 않는다 가정
- JPA 제공 버전 관리 기능 사용 (=
애플리케이션
이 제공하는 락) - 트랜잭션 커밋 전까지 트랜잭션의 충돌 알 수 X
비관적 락
- 트랜잭션의 충돌이 발생하다 가정, 우선 락을 검
데이터베이스
가 제공하는 락 기능 사용- select for update 구문이 대표적인 예
두 번의 갱신 분실 문제
- 두 트랜잭션 내 동일 데이터 변경 시 먼저 커밋한 내용이 분실되는 문제
- 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 모드 사용
- 특징
- 스칼라 타입 조회에도 사용 가능
- 데이터 수정 즉시 트랜잭션 충돌 감지
- 예외
- 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차 캐시에 엔티티 X
- DB에서 엔티티 조회
- 1차 캐시에 보관
- 1차 캐시에 보관 결과 반환
- 이후 같은 엔티티 조회 시 1차 캐시 엔티티 그대로 반환
- 영속성 컨텍스트 내부 엔티티 저장소
- 일반적으로 트랜잭션 시작 ~ 종료 까지만 유효 (cf. OSIV : 클라이언트 요청 시작 ~ 끝)
- 1차 캐시는 객체 동일성 보장
2차 캐시, 공유 캐시
- 영속성 컨텍스트는 엔티티가 필요하면 2차 캐시 조회
- 2차 캐시 엔티티 X, 데이터 베이스 조회
- 결과 2차 캐시에 보관
- 2차 캐시는 자신이 보관하고 있는 엔티티 복사 반환
- 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_SELECTIVE | Cacheable(true) 설정된 엔티티만 캐시 |
DISABLE_SELECTIVE | 모든 엔티티 캐시, Cacheable(false)로 명시된 엔티티 캐시 X |
UNSPECIFIED | JPA 구현체가 정의한 설정 따름 |
캐시 조회, 저장 방식 설정 (캐시 조회 모드, 캐시 보관 모드)
- 캐시 조회 모드 : javax.persistence.cache.retrieveMode
- 옵션 : javax.persistence.CacheretrieveMode
- USE : 캐시에서 조회. 기본값
- BYPASS : 캐시 무시. 데이터베이스 직접 접근
- 옵션 : javax.persistence.CacheretrieveMode
- 캐시 보관 모드 : javax.persistence.cache.storeMode
- 옵션 : javax.persistence.CacheStoreMode
- USE : 조회 데이터 캐시 저장. 기본값.
- BYPASS : 캐시 저장 X
- REFRESH : USE 전략 + 데이터베이스에서 조회한 엔티티 최신 상태 다시 캐시
- 옵션 : javax.persistence.CacheStoreMode
- 엔티티 매니저 단위 설정 가능. 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가지
- 엔티티 캐시
- 엔티티 단위 캐시. 식별자로 엔티티 조회. 컬렉션이 아닌 연관된 엔티티 로딩 시 사용.
- 컬렉션 캐시
- 엔티티 연관 컬렉션 캐시. 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시
- 쿼리 캐시
- 쿼리와 파라미터 정보를 키로 사용해 캐시. 결과가 엔티티면 식별자 값만 캐시
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 에 따라 동시성 전략 지원 여부 상이
Cache | read-only | nonstrict-read-write | read-write | transactional |
---|---|---|---|---|
ConcurrentHashMap | yes | yes | yes | |
EHCACHE | yes | yes | yes | yes |
Infinispan | yes | 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 : 쿼리 캐시 유효 확인 위해 쿼리 대상 테이블의 가장 최근 변경(등록, 수정, 삭제) 시간 저장 영역. 테이블 명, 테이블 최근 변경 타임 스탬프 보관
- 주의! 캐시 만료되지 않도록 설정해야 함
쿼리 캐시는 데이터 집합을 최신 데이터 유지 위해 쿼리 캐시 실행 시간 & 테이블 가장 최근 변경된 시간 비교. 쿼리 캐시 적용 후 쿼리 캐시 사용 테이블 변경 시 데이터베이스에서 데이트 조회 후 캐시 세팅.
수정이 거의 일어나지 않는 테이블에 사용해야 효과적
쿼리 캐시 & 컬렉션 캐시 주의점
쿼리 캐시, 컬렉션 캐시는 결과 집합의 식별자 값
만 캐시
식별자 값을 하나씩 엔티티 캐시에서 조회해서 실제 엔티티 조회
쿼리, 컬렉션 캐시만 사용하고 대상 엔티티에 엔티티 캐시를 적용하지 않으면 성능상 심각한 문제 발생
- select m from Member m 쿼리 실행. 쿼리 캐시 적용. 결과 집합 100건
- 결과 집합에는 식별자만 있음. 한 건씩 엔티티 캐시 영역에서 조회
- Member 엔티티 캐시 사용 X. 한 건씩 데이터베이스에서 조회
- 100건의 SQL 실행
쿼리 캐시나 컬렉션 캐시를 사용하면 대상 엔티티에는 꼭 엔티티 캐시를 적용해야
참조
- 자바 ORM 표준 JPA 프로그래밍, 김영한 지음