Java
JPA] Hibernate 1차 캐시
ParkCheolu
2022. 6. 27. 16:06
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가 사용된다. 이 클래스의 인스턴스는 다음 과정으로 엔티티의 로딩을 실행한다.
- loadFromSessionCache(): 해당 키를 가진 엔티티가 1차 캐시에 존재하는지 확인한다. 존재한다면 그 엔티티를 반환한다.
- loadFromSecondLevelCache(): 1차 캐시에 엔티티가 존재하지 않는다면, 2차 캐시에 같은 방식으로 엔티티 존재 여부를 확인하고, 존재한다면 그 엔티티를 반환한다.
- loadFromDataSource(): 2차 캐시에 엔티티가 존재하지 않는다면, 데이터소스(DB)에 SQL을 실행하여 ResultSet을 얻고, 여기서 데이터를 읽어 Object[] 를 생성한다. 여기까지가 엔티티가 '읽혀진' 상태가 된다.
- 읽어온 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/