[자바 ORM 표준 JPA 프로그래밍] 15주차 스터디 (2)

박서영·6일 전

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

16.1 트랜잭션과 락

목표: 트랜잭션의 기초와 JPA가 제공하는 낙관적 락과 비관적 락

(1) 트랜잭션과 격리 수준

트랜잭션: ACID라 하는 원자성, 일관성, 격리성, 지속성을 보장해야함

ACID

  • 원자성: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하든가 모든 실패해야함
  • 일관성: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야함. 예) 데이터베이스에서 정한 무결성 제약 조건을 항상 만족
  • 격리성: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리함. 동시성과 관련된 성능이슈로 인해 격리수준을 선택할 수 있음 예) 동시에 같은 데이터 수정 방지.
  • 지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야함. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해 성공한 트랜잭션 내용을 복구해야함

트랜잭션에서 격리성을 완벽히 보장하기 위해서는 트랜잭션을 거의 차례대로 진행해야함

→ 단점: 동시성 처리 성능이 매우 나빠짐

→ 따라서 ANSI 표준에서는 트랜잭션의 격리 수준을 4단계로 나누어 정의함.

트랜잭션의 격리수준

  • READ UNCOMMITED (커밋되지 않은 읽기)
  • READ COMMITED (커밋된 읽기)
  • REPEATABLE READ (반복 가능한 읽기)
  • SERIALIZABLE (직렬화 가능)

READ UNCOMMITED의 격리수준이 가장 낮고, SERIALIZABLE의 격리 수준이 가장 높음.

격리 수준이 낮을 수록 동시성을 증가하지만 다양한 문제 발생 가능.

격리수준DIRTY READNON-REPEATABLE READPHANTOM READ
READ UNCOMMITEDOOO
READ COMMITEDOO
REPEATABLE READO
SERIALIZABLE
  • 격리수준에 따른 문제점
    • DIRTY READ
    • NON-REPEATABLE READ (반복 불가능한 읽기)
    • PHANTOM READ

1-READ UNCOMMITED

  • 커밋하지 않은 데이터를 읽을 수 있음
  • DIRTY READ
    • 예) 트랜잭션1이 데이터를 수정 중이고 커밋X → 트랜잭션2가 수정 중인 데이터 조회
    • 문제점: 트랜잭션2가 DIRTY READ한 데이터 사용 중 트랜잭션1이 롤백되면 데이터 정합성에 문제
  • DIRTY READ를 허용하는 격리수준을 READ UNCOMMITED라함.

2-READ COMMITED

  • 커밋한 데이터만 읽을 수 있음 → 즉, DIRTY READ는 발생하지 않음
  • 다만, NON-REPEATABLE READ는 발생할 수 있음
    • 예) 트랜잭션1이 회원 A를 조회 중 → 갑자기 트랜잭션2가 회원A를 수정 후 커밋 → 트랜잭션1이 다시 회원을 조회했을 때 수정된 데이터가 조회됨.
    • 이렇게 “반복해서 같은 데이터를 읽을 수 없는 상태”를 NON-REPEATABLE READ라함.
  • DIRTY READ는 허용하지 않지만, NON-REPEATABLE READ는 허용하는 격리수준.

3-REPEATABLE READ

  • 한 번 조회한 데이터를 반복해 조회해도 같은 데이터가 조회됨 → 하지만 PHANTOM READ는 발생할 수 있음
  • PHANTOM READ
    • 예) 트랜잭션1 10살 이하의 회원 조회 → 트랜잭션2가 5살 회원을 추가하고 커밋 → 트랜잭션1이 10살 이하의 회원을 조회했을 때, 회원 하나가 추가된 상태로 조회
    • 이렇게 “반복 조회 시 결과 집합이 달라지는 것”을 PHANTOM READ라함.
  • NON-REPEATABLE READ는 허용하지 않지만, PHANTOM READ는 허용하는 격리수준.

4-SERIALIZABLE

  • 가장 엄격한 트랜잭션 격리수준으로 PHANTOM READ도 발생하지 않음
  • 동시성 처리 능력이 급격히 떨어질 수 있음

애플리케이션은 대부분 동시성 처리가 중요하기에 데이터베이스들은 보통 READ COMMITED 격리수준을 기본으로 사용함. 더 높은 수준의 격리 수준이 일부 비즈니스 로직에 필요하면, 데이터베이스 트랜잭션이 제공하는 잠금 기능을 제공을 사용.


(2) 낙관적 락과 비관적 락 기초

JPA는 READ COMMITED로 트랜잭션 격리 수준을 가정. 더 높은 격리 수준일 필요할 때에는 낙관적 락 또는 비관적 락 중 하나를 사용

낙관적 락

  • 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법
  • 데이터베이스가 제공하는 락 기능이 아닌 JPA가 제공하는 버전 관리 기능을 사용함 (=즉, 애플리케이션이 제공하는 락)
  • 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없음

비관적 락

  • 트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 방법
  • 데이터베이스가 제공하는 락 기능을 사용
  • 대표적으로는 select for update 구문 존재

두 번의 갱신 분실 문제

  • 데이터베이스 트랜잭션 범위를 넘어서는 문제 역시 존재. 예) 사용자 A와 B가 모두 제목이 같은 공지사항 수정 → A가 먼저 완료 버튼을 누른 후, B가 완료 버튼 누름
    → 결과: 먼저 완료를 누른 A의 결과는 사라지고 B의 결과만 남음.
  • 3가지 해결방법 선택 가능:
    • 마지막 커밋만 인정

    • 최초 커밋만 인정

    • 충돌하는 갱신 내용 병합

      ⇒ 기본적으로는 마지막 커밋만 인정하는 것이 적용됨. 다만, 이는 상황마다 3가지 중 적합한 것이 변화하며 JPA가 제공하는 버전 관리 기능을 사용하면 최초 커밋만 인정하는 것 역시 구현 가능함. 충돌 내용을 병합하기 위해서는 애플리케이션 개발자가 직접 병합 방법을 제공해야함.


(3) @Version

JPA가 제공하는 낙관적 락 사용을 위해서 @Version 어노테이션을 사용해 버전 관리 기능을 추가해야함

@Version 어노테이션 적용 가능 타입: Long, Integer, Short, Timestamp

@Entity
public class Board {
	
	@Id
	private String id;
	private String title;
	
	@Version
	private Integer version;
}
  • @Version 어노테이션 추가 이후에는 엔티티 수정마다 버전이 하나씩 자동으로 추가됨.
  • 만약 엔티티 수정 시, 조회 시점의 버전과 수정 시점의 버전이 다르면 예외 발생 → 따라서 버전 정보를 사용하면 최초 커밋만 인정하기가 적용됨.

버전 정보 비교 방법

  • JPA가 버전 정보를 비교하는 방법:
    • 엔티티 수정 후 트랜잭션을 커밋 → 영속성 컨텍스트를 플러시하며 UPDATE 쿼리 실행. 이때 버전을 사용하는 엔티티의 경우 검색 조건에 엔티티의 버전 정보를 추가
    • 데이터베이스 버전과 엔티티 버전이 같으면 데이터를 수정하며 동시에 버전도 하나를 증가.
    • 만약, 데이터베이스 버전이 이미 증가해 수정 중인 엔티티의 버전과 다르면 UPDATE 쿼리의 WHERE문에서 VERSION 값이 다름 → 수정할 대상이 없음. 이 경우 JPA가 예외를 발생시킴.

“버전은 엔티티의 값을 변경하면 증가”

  • 값 타입인 임베디드 타입과 값 타입 컬렉션 → 논리적 개념상 해당 엔티티의 값이기에 수정하면 엔티티의 버전 증가
  • 연관관계 필드: 외래키를 관리하는 연관관계 주인 필드를 수정할 때만 버전이 증가
  • @Version으로 추가한 버전 관리 필드의 경우 JPA가 직접 관리하기에, 개발자가 임의로 수정해서는 안됨 (벌크 연산 제외)
    • 버전 값을 강제 증가하려면 특별한 락 옵션을 선택

      +) 벌크 연산은 버전을 무시. 벌크연산에서 버전을 증가하려면 버전 필드를 강제 증가시켜야함


(4) JPA 락 사용

JPA에서 제공하는 락은 다음 위치에 적용 가능

  • EntityManager.lock(), EntityManager.find(), EntityManager.refresh()
  • Query.setLockMode()
  • @NamedQuery

조회하면서 즉시 락을 걸 수도 있고, 필요 시에 락을 걸 수도 있음.

JPA에서 제공하는 락 옵션은 LockModeType에 정의되어있음.


(5) JPA 낙관적 락

  • JPA가 제공하는 낙관적 락은 버전(@Version)을 사용함 → 낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 알 수 있음
  • 낙관적 락에서 발생하는 예외:
    • OptimisticLockException(JPA 예외)
    • StaleObjectStateException(하이버네이트 예외)
    • ObjectOptimisticLockingFailureException(스프링 예외 추상화)

락 옵션 없이 @Version만 있어도 낙관적 락이 적용됨.

NONE

  • 락 옵션을 적용하지 않아도 엔티티에 @Version이 적용된 필드만 있으면 낙관적 락이 적용됨
    • 용도: 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경(삭제)되지 않아야함. 조회 시점부터 수정 시점까지를 보장
    • 동작: 엔티티 수정 시, 버전을 체크하며 버전을 증가함. 이때 데이터베이스의 버전값이 현재 버전이 아니면 예외 발생
    • 이점: 두 번의 갱신 분실 문제를 예방

OPTIMISTIC

  • @Version만 적용했을 때는 엔티티를 수정해야 버전을 체크하지만, 이 옵션을 추가하면 엔티티를 조회만 해도 버전을 체크함. 즉, 한 번 조회한 엔티티는 트랜잭션 종료까지 다른 트랜잭션에서 변경하지 않음을 보장
    • 용도: 조회한 엔티티는 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않아야함. 조회 시점부터 트랜잭션 종료까지 조회 엔티티가 변경되지 않음을 보장
    • 동작: 트랜잭션 커밋 시 버전 정보를 조회해 현재 엔티티의 버전과 같은지 검증. 같지 않을 경우 예외
    • 이점: DIRTY READ와 NON-REPEATABLE READ를 방지
//트랜잭션1 조회 title="제목A", verison = 1
Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC);

//중간에 트랜잭션2에서 해당 게시물 수정해 title = "제목C", version=2로 증가

//트랜잭션 1 커밋 시점에 버전 정보 검증, 예외 발생
//(데이터베이스 version=2, 엔티티 version=1)
tx.commit();
  • OPTIMISTIC 락으로 조회했기에, 트랜잭션 커밋 시 데이터베이스에 있는 버전 정보를 SELECT 쿼리로 조회해 처음 조회한 엔티티의 버전 정보와 비교. 이때 다르면 예외발생

OPTIMISTIC_FORCE_INCREMENT

  • 낙관적 락을 사용하면서 버전 정보를 강제 증가
    • 용도: 논리적인 단위의 엔티티 묶음을 관리할 수 있음.
    • 예) 게시물 - 첨부파일이 일대다, 다대일의 양방향 연관관계.
      • 첨부파일이 연관관계의 주인.
      • 게시물을 수정하는데 단순히 첨부파일만 추가하면 게시물의 버전은 증가X. 해당 게시물은 물리적으로 변경되지 않았지만, 논리적으로 변경됨
      • 게시물의 버전을 강제로 증가시키기 위해서 사용
    • 동작: 엔티티를 수정하지 않아도 트랜잭션 커밋 시 UPDATE 쿼리를 사용해 버전 정보를 강제로 증가시킴. 이때 데이터베이스의 버전이 엔티티의 버전과 다르면 예외 발생.
    • 이점: 강제로 버전을 증가해 논리적 단위의 엔티티 묶음을 버전 관리 가능

(6) JPA 비관적 락

  • 비관적 락 → 데이터베이스 트랜잭션 락 메커니즘에 의존하는 방법.
  • 주로 SQL 쿼리에 select for update 구문을 사용하며 시작, 버전 정보는 사용하지 않음
  • 비관적 락은 주로 PESSIMISTIC_WRITE 모드를 사용함
  • 비관적 락의 특징:
    • 엔티티가 아닌 스칼라 타입을 조회할 때도 사용 가능
    • 데이터 수정 즉시 트랜잭션 충돌 감지 가능
  • 비관적 락에서 발생하는 예외:
    • PersistenceLockException (JPA 예외)
    • PessimisticLockingFailureException (스프링 예외 추상화)

PESSIMISTIC_WRITE

일반적인 비관적 락의 옵션. 데이터베이스에 쓰기 락을 걸 때 사용

  • 용도: 데이터베이스에 쓰기 락을 검
  • 동작: 데이터베이스 select for update를 사용해 락을 검
  • 이점: NON-REPEATABLE READ를 방지. 락에 걸린 로우는 다른 트랜잭션이 수정 불가

PESSIMISTIC_READ

데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용. 일반적으로 잘 사용하지 않음.

  • MySQL: lock in share mode
  • PostgreSQL: for share

PESSIMISTIC_FORCE_INCREMENT

비관적 락 중 유일하게 버전 정보를 사용함. 버전 정보를 강제로 증가시킴. 하이버네이트는 nowait를 지원하는 데이터베이스에 대해 for update nowait 옵션을 적용함

  • 오라클: for update nowait
  • PostgreSQL: for update nowait
  • nowait를 지원하지 않으면 for update가 사용됨

(7) 비관적 락 타임아웃

비관적 락을 사용하는 경우 락을 획득할 때까지 트랜잭션이 대기 → 무한정 기다릴 수 없기에 타임아웃 시간을 줌.

응답이 없는 경우 LockTimeoutException이 발생. (다만, 데이터베이스 특성에 따라 동작하지 않을 수 있음)

Map<String, Object>properties = new HashMap<String,Object>();

//타임아웃 10초까지 대기 설정
properties.put("timeout", 10000);

Board board = em.find(Board.class, "boardId", LockModeType.PESSIMISTIC_WRITE,properties);

16.2 2차 캐시

(1) 1차 캐시와 2차 캐시

1차 캐시

  • 영속성 컨텍스트 내부 엔티티를 보관하는 저장소
  • 일반적인 웹 애플리케이션 환경은 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효 → 애플리케이션 전체의 관점에서는 데이터베이스 접근 횟수를 획기적으로 줄이지 못함
  • 엔티티 매니저로 조회/변경하는 모든 엔티티는 1차 캐시에 저장됨
  • 트랜잭션 커밋 또는 플러시 호출 시 1차 캐시에 있는 엔티티의 변경 내역을 데이터베이스에 동기화
  • 트랜잭션 시작 시 영속성 컨텍스트 생성 → 트랜잭션 종료 시

📌 1차 캐시 특징

  • 같은 엔티티가 있으면 해당 엔티티를 그대로 반환. 따라서 1차 캐시는 객체 동일성(a==b)를 보장
  • 기본적으로 영속성 컨텍스트 범위의 캐시

2차 캐시

  • 하이버네이트 등 JPA 구현체에서 지원하는 애플리케이션 범위 캐시
  • 애플리케이션이 종료될 때까지 캐시가 유지됨.
  • 분산 캐시 또는 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수도 있음
  • 엔티티 매니저를 통해 데이터 조회 시 2차 캐시에서 우선 찾고 없으면 데이터베이스에서 찾음
  • 동시성 극대화를 위해 캐시한 객체를 직접 반환하지 않고 복사본을 만들어 반환
    • 객체를 그대로 반환하면 여러 곳에서 같은 객체를 수정하는 문제 발생 가능
    • 락에 비해 객체를 복사하는 비용은 저렴함

📌 2차 캐시 특징

  • 영속성 유닛 범위의 캐시
  • 조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어 반환
  • 데이터베이스 기본키를 기준으로 캐시하지만, 영속성 컨텍스트가 다르면 객체 동일성(a==b)을 보장하지 않음

(2) JPA 2차 캐시 기능

JPA 2.0에서 캐시(2차 캐시) 표준을 정의함. JPA 캐시 표준은 여러 구현체가 공통으로 사용하는 부분만 표준화했기에, 세밀한 설정을 하려면 구현체에 의존적인 기능 사용 필요.

1-캐시 모드 설정

  • 2차 캐시 사용을 위해서는 @Cacheable 어노테이션을 사용.
@Cacheable
@Entity
public class Member {
	
	@Id @GeneratedValue
	private Long id;
}
  • persistence.xml에 shared-cache-mode를 설정해 애플리케이션 전체에 캐시를 어떻게 적용할지 옵션을 정해야함
<persistence-unit name="test">
	<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
</persistence-unit>
  • 캐시모드는 SharedCacheMode에 정의. 보통 ENABLE_SELECTIVE를 사용
    • ENABLE_SELECTIVE: Cacheable(true)로 설정된 엔티티만 캐시를 사용
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
	<property name="sharedCacheMode" value="ENABLE_SELECTIVE"/>

2-캐시 조회, 저장 방식 설정

캐시를 무시하고 데이터베이스를 직접 조회하거나 캐시를 갱신하려면 캐시 조회 모드와 캐시 보관 모드를 사용하면 됨.

em.setProperty("retrieveMode", CacheRetrieveMode.BYPASS);
  • 캐시 조회 모드나 보관 모드에 따라 사용할 프로퍼티와 옵션이 다름.
  • 프로퍼티 이름:
    • retrieveMode: 캐시조회 모드
    • storeMode: 캐시보관 모드
  • 옵션:
    • CacheRetrieveMode: 캐시 조회 모드 설정 옵션
    • CacheStoreMode: 캐시 보관 모드 설정 옵션
  1. 캐시 조회 모드
public enum CacheRetrieveMode {
	USE,
	BYPASS
}
  • USE: 캐시에서 조회. 기본값
  • BYPASS: 캐시를 무시하고 데이터베이스에 직접 접근
  1. 캐시 보관 모드
public enum CacheStoreMode {
	USE,
	BYPASS,
	REFRESH
}
  • USE: 조회한 데이터를 캐시에 저장. 조회한 데이터가 이미 캐시에 있으면 캐시 데이터를 최신 상태로 갱신하지는 않음
  • BYPASS: 캐시에 저장하지 않음
  • REFRESH: USE 전략에 추가로 데이터베이스에서 조회한 엔티티를 최신 상태로 다시 캐시함

⇒ 캐시 모드는 EntityManager.setProperty()로 엔티티 매니저 단위로 설정하거나, EntityManager.find(), EntityManager.refresh()에 더 세밀하게 설정할 수 있음. 또는 Query.setHint()에도 사용할 수 있음.

3-JPA 캐시 관리 API

  • JPA에서는 캐시 관리를 위한 Cache 인터페이스 제공. 이는 EntityManagerFactory에서 구할 수 있음
Cache cache = emf.getCache();
boolean contains =
	cache.contains(TestEntity.class, testEntity.getId());
System.out.println("contains = " + contains);
  • Cache 인터페이스의 기능
public interface Cache {

	//해당 엔티티에 캐시가 있는지 여부 확인
	public boolean contains(Class cls, Object primaryKey);
	
	//해당 엔티티 중 특정 식별자를 가진 엔티티를 캐시에서 제거
	public void evict(Class cls, Object primaryKey);
	
	//해당 엔티티 전체를 캐시에서 제거
	public void evict(Class cls);
	
	//모든 캐시에서 데이터 제거
	public vodi evictAll();
	
	//JPA Cache 구현체 조회
	public <T> T unwrap(Class<T> cls);
}

(3) 하이버네이트와 EHCACHE 적용

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

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

엔티티 캐시와 컬렉션 캐시

**@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)**
@Entity
public class ParentMember {
	
	@Id @GeneratedValue
	private Long id;
	
	private String name;
	
	**@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)**
	@OneToMany(mappedBy = "parentMember", cascade = CascadeType.ALL)
	private List<ChildMember> childMembers = new ArrayList<ChildMember>();
}
  • @Cacheable: 엔티티를 캐시하기위해서 사용
  • @Cache: 하이버네이트 전용 어노테이션. 캐시 관련 더 세밀한 설정을 위해 사용.

@Cache

  • Cache
속성설명
usageCacheConcurrencyStrategy를 사용해 캐시 동시성 전략을 설정
region캐시 지역 설정
include연관 객체를 캐시에 포함할지 선택. all, non-lazy 옵션을 선택. 기본값은 all.
  • CacheConcurrencyStrategy
속성설명
NONE캐시 설정 X
READ_ONLY읽기 전용으로 설정. 등록, 삭제는 가능하지만 수정은 불가능.
NONSTRICT_READ_WRITE엄격하지 않은 읽고 쓰기 전략. 동시에 같은 엔티티 수정 시 데이터 일관성이 깨질 수 있음.
READ_WRITE읽기 쓰기가 가능하고 READ COMMITED 정도의 격리 수준을 보장. EHCACHE는 데이터 수정하면 캐시 데이터도 같이 수정
TRANSACTIONAL컨테이너 관리 환경에서 사용할 수 있음. 설정에 따라 REPEATABLE READ 정도의 격리 수준 보장

캐시 영역

캐시를 적용한 코드는 캐시 영역에 저장됨

  • 엔티티 캐시 영역: 기본값으로 [패키지 명 + 클래스 명]을 사용
  • 컬렉션 캐시 영역: 엔티티 캐시 영역 이름에 컬렉션의 필드명이 추가됨

→ 필요시 @Cache(region = "customRegion", ...) 처럼 region 속성을 사용해 캐시 영역 직접 지정 가능

쿼리 캐시

  • 쿼리와 파라미터 정보를 키로 하여 쿼리 결과를 캐시하는 방법
  • 적용을 위해서는 영속성 유닛을 설정에 hibernate.cache.use_query_cache 옵션을 true로 설정해야함
  • 쿼리 캐시를 적용하려는 쿼리마다 cacheable을 true로 설정
em.createQuery("select i from Item i", Item.class)
	.setHint("cacheable", true)
	.getResultList();

쿼리 캐시 영역

쿼리 캐시 활성화 시 두 캐시 영역이 추가됨

  • StandardQueryCahce: 쿼리 캐시를 저장하는 영역. 쿼리, 쿼리 결과 집합, 실행 시점의 타임스탬프 보관
  • UpdateTimestampsCache: 쿼리 캐시가 유효한지 확인하기 위해 쿼리 대상 테이블의 가장 최근 변경 시간을 저장.

쿼리 캐시는 캐시한 데이터 집합을 최신 데이터로 유지하려고 쿼리 캐시를 실행하는 시간과 쿼리 캐시가 사용하는 테이블들이 가장 최근에 변경된 시간을 비교함.

⇒ 쿼리 캐시는 빈번한 변경이 있는 테이블에 사용하면 성능이 저하될 수 있으니 수정이 거의 일어나지 않는 테이블에 사용해야 효과적임.

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

  • 엔티티 캐시: 엔티티 캐시 → 엔티티 정보를 모두 캐시
  • 쿼리 캐시, 컬렉션 캐시: 결과 집합의 식별자 값만 캐시

⇒ 결국 이 식별자값을 하나씩 엔티티 캐시에서 조회해 실제 엔티티를 찾음.

따라서 쿼리 캐시나 컬렉션 캐시 사용 시 결과 대상 엔티티에는 반드시 엔티티 캐시를 적용해야함

profile
이불 밖은 위험해.

0개의 댓글