1차 캐시의 특징

  • 1차 캐시의 실체는 JPA의 PersistenceContext 이며, 하이버네이트 구현체로는 Session 이다.
  • 1차 캐시가 작동하기 위해서는 PersistenceContext(Session) 인스턴스가 필요하다. 이 인스턴스는 트랜잭션이 시작될 때 팩토리로부터 생성된다. 즉 1차 캐시가 작동하기 위해서는 트랜잭션이 필요하다.
  • 1차 캐시는 특정 "Session(세션)" 인스턴스에 바인딩된다. 세션은 사실상 하나의 런타임 트랜잭션을 의미하며, 한 트랜잭션에 저장된 캐시는 다른 트랜잭션으로부터 격리된다. 즉 '볼 수 없다.'
  • 캐시된 인스턴스의 범위는 '세션' 까지이다. 인스턴스를 캐싱한 세션이 닫히면, 즉 트랜잭션이 종료되면 그 안에서 저장된 캐시도 모두 소멸된다.
  • 하이버네이트에서 1차 캐시는 기본적으로 활성화(사용) 상태이며, 비활성화 할 수 없다.
  • 한 세션(트랜잭션)에서 한 엔티티를 처음 쿼리할 때, 이 엔티티는 쿼리를 실행하여 DB로부터 읽어들이고 이를 세션에 바인딩된 1차 캐시에 저장한다.
  • 이미 세션의 1차 캐시에 저장된 엔티티를 다시 쿼리하면 이 엔티티는 DB가 아닌 캐시로부터 읽어들인다. 즉 쿼리를 실행하지 않는다.
  • 세션의 1차 캐시에 존재하는 인스턴스는 EntityManager.evict() 메서드를 통해 삭제할 수 있다. 삭제 후 같은 엔티티를 쿼리하면 이는 DB로부터 읽어들인다.
  • 세션의 1차 캐시의 데이터를 모두 삭제하려면 EntityManager.clear() 를 호출한다.
  • 읽기 전용 트랜잭션(@Transactional(readOnly = true)) 에서도 조회된 엔티티는 1차 캐시에 저장된다. 대신, 이 때 저장된 엔티티는 readonly 플래그를 지닌다.

 

엔티티 조회 시 쿼리를 생략하고 1차 캐시를 반환하는 조건 (이 조건은 기본적으로 2차 캐시에도 동일하게 적용)

  • EntityManager.find (by PK)를 통해 조회될 경우에만 1차 캐시를 반환한다. Spring-data-jpa 사용 시 이는 repository.findById 가 된다. 하이버네이트는 1차 캐시에 데이터 저장 시 primary key가 되는 프로퍼티(with @Id)를 키로 사용하기 때문에 다른 컬럼을 사용하여 엔티티 조회 시 1차 캐시에서 이를 찾을 수 없다.
  • 위의 내용에 이어서, 커스텀 쿼리는 1차 캐시를 사용할 수 없다. (ex: Member @Id id: Long, name: String 일 때, memberRepository.findByName 은 1차 캐시에 엔티티가 존재해도 쿼리를 호출한다.) 
  • 위의 내용에 이어서, PK 를 통해 조회하더라도 그것이 커스텀 쿼리라면 1차 캐시를 사용할 수 없다. 예를 들어, findOneById(Long id) 라는 커스텀 쿼리 메서드는 PK 로 조회하지만 1차 캐시가 존재해도 무조건 쿼리를 호출한다.
  • 위의 이유로, JPQL 은 무조건 쿼리를 호출한다. JPQL 은 EntityManager.createQuery 를 통해 쿼리를 직접 생성해 호출하며, EntityManager.find 와는 다르다.
  • 이 조건들은 '1차 캐시를 반환하는' 조건이지, '엔티티를 1차 캐시에 저장하는' 조건이 아니다. 기본적으로 1차 캐시에는 primary key 데이터를 키로 하여 조회된 엔티티를 모두 저장한다. 즉 JPQL로 조회된 엔티티는 1차 캐시에 저장되어 findById 호출 시 1차 캐시로부터 반환된다.

 

캐시의 작동 과정

위 조건을 만족하는 경우, 하이버네이트의 LoadEntityEvent 가 작동한다. 이 인터페이스의 구현체로는 DefaultLoadEventListener가 사용된다. 이 클래스의 인스턴스는 다음 과정으로 엔티티의 로딩을 실행한다.

  1. loadFromSessionCache(): 해당 키를 가진 엔티티가 1차 캐시에 존재하는지 확인한다. 존재한다면 그 엔티티를 반환한다.
  2. loadFromSecondLevelCache(): 1차 캐시에 엔티티가 존재하지 않는다면, 2차 캐시에 같은 방식으로 엔티티 존재 여부를 확인하고, 존재한다면 그 엔티티를 반환한다.
  3. loadFromDataSource(): 2차 캐시에 엔티티가 존재하지 않는다면, 데이터소스(DB)에 SQL을 실행하여 ResultSet을 얻고, 여기서 데이터를 읽어 Object[] 를 생성한다. 여기까지가 엔티티가 '읽혀진' 상태가 된다.
  4. 읽어온 Object[] 를 1차 캐시와 2차 캐시에 저장한다. 그리고 이 Object[ ] 를 엔티티 인스턴스로 변환하고, 이를 1차 캐시에 저장한다. 즉 dirty checking을 위한 데이터는 사실 엔티티로 저장되지 않는다. (이 저장된 데이터가 후에 데이터 변경 여부 확인(dirty checking)에 그대로 사용된다.)

정리하면, 1차 캐시 - 2차 캐시 - DB 순으로 조회하며, DB 조회 시 2차 캐시에는 ResultSet 으로부터 데이터를 추출한 Object[] 를, 1차 캐시에는 Object[], 엔티티를 저장한다. 때문에 cache hit이 발생하여 1차 캐시의 데이터를 반환할 때는 엔티티를 반환하지만, 2차 캐시의 엔티티를 반환할 때는 Object[] 를 반환된다. 그리고 2차 캐시로부터 반환된 Object[] 는 엔티티로 변환되고, 또다시(DB 조회에서와 같이) 1차 캐시에 Object[], 엔티티가 모두 저장된다.

 

레퍼런스:

https://howtodoinjava.com/hibernate/understanding-hibernate-first-level-cache-with-example/

https://stackoverflow.com/questions/64190242/in-spring-data-jpa-a-derived-find-method-doesnt-use-first-level-cache-while-ca

https://vladmihalcea.com/jpa-hibernate-second-level-cache/

'Java' 카테고리의 다른 글

JPA] @OneToOne lazy loading  (0) 2022.08.22
test] @Testcontainers, @Container  (0) 2022.08.03

+ Recent posts