[JPA] Second-Level Cache

신명철·2022년 11월 8일
2

JPA

목록 보기
13/14

Second-Level Cache

Hibernate는 1차 캐시를 제공한다. 1차 캐시는 영속성 컨텍스트에 존재한다. 따라서 영속성 컨텍스트와 생명주기가 동일하다. 즉, 트랜잭션이 종료되면 1차 캐시도 종료된다. 서로 다른 isolation에서의 concurrent를 보장하고자 하는 요구가 생긴다면 2차 캐시의 사용을 고려해볼 수 있다. 2차 캐시SessionFactory-Scoped 이기 때문에 같은 session factory에서 생성된 세션들은 모두 2차 캐시를 공유할 수 있다.

  • 요약하자면, 1차 캐시는 Transaction 범위 내에서 동작하고, 2차 캐시는 Application 범위 내에서 동작한다.

SessionFactory ?

데이터베이스에 도메인 모델을 맵핑하는 thread-safe한 ( 그리고 불변의) 표현이다.
org.hibernate.Session 인스턴스의 팩토리로 동작한다.
JPA의 EntityManagerFactory에 해당하는 명세이며 기본적으로 이 두 가지는 동일한 SessionFactory 구현으로 수렴됩니다. (위 다이어그램을 보면, EntityManagerFactory와 SessionFactory는 인터페이스이고 SessionFactoryImpl이 구현체이다. 두 인터페이스를 SessionFactoryImpl가 구현한다는 뜻) SessionFactory 하나를 생성하기 위해서는 매우 비싼값을 치러야한다(시스템 자원을 많이 소모한다). 그래서, 주어진 어떤 데이터베이스에 대해 애플리케이션은 오직 하나의 연관된 SassionFactor를 가져야한다. SessionFactory는 2단계 캐시, 커넥션 풀, 트랜잭션 시스템 통합 등과 같이 Hibernate가 모든 세션에서 사용하는 서비스를 유지 관리한다. https://docs.jboss.org/hibernate/orm/5.5/userguide/html_single/Hibernate_User_Guide.html#architecture

엔티티 조회 과정

  1. 1차 캐시에 instance가 있는 경우 1차 캐시에서 반환
  2. 1차 캐시에 instance가 없지만 2차 캐시에는 존재하는 경우 2차 캐시에서 반환
  3. 둘 다 없는 경우 DB에서 가져와서 반환

instance가 영속성 컨텍스트(1차 캐시)에 있으면 같은 session을 공유하는 작업들은 session이 종료될 때까지 1차 캐시 해당하는 데이터를 가져오게 되고, 영속성 컨텍스트가 종료되면 해당 instance도 같이 사라진다.


환경 설정

1. 의존성 설정

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>5.2.2.Final</version>
</dependency>
  • hibernate-ehcache 버전과 hibernate version이 동일해야 함을 주의해야 한다.

2. 2차 캐시 Enabling

hibernate.cache.use_second_level_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory
  • region factory는 cache provider와 hibernate 사이를 이어주는 역할을 한다. 여기서는 cache provider로 가장 많이 사용하는 EhCacheRegionFactory를 사용했다.

캐시 동시성 전략

@Cache의 속성인 usage의 값으로 CacheConcurrencyStrategy를 설정해서 캐시의 동시성 전략을 설정할 수 있다.

  • READ_ONLY
    • 읽기 전용으로 사용한다. (update 하려고 하면 exception을 던짐) 간단하고 성능이 좋다. 항상 변하지 않는 데이터를 대상으로 사용하는게 좋다.
  • NONSTRICT_READ_WRITE
    • 객체 동시 수정 등에 대한 고려를 전혀 하지 않고 캐싱을 한다. 이 방식은 하나의 객체가 동시에 수정될 가능성이 거의 없는 경우에 사용한다.
  • READ_WRITE
    • 엄격한 읽기/쓰기로 두 개 이상의 스레드에서 동시 수정할 가능성에 대해서 고려하고 만들어야 한다.
  • TRANSACTIONAL
    • JTA에서만 사용함

@Cache

@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface Cache {
	/**
	 * The concurrency strategy chosen.
	 */
	CacheConcurrencyStrategy usage();

	/**
	 * The cache region name.
	 */
	String region() default "";

	/**
	 * How lazy properties are included in the second level cache. Default value is "all"; other allowable
	 * value: "non-lazy"
	 */
	String include() default "all";
}
  • region을 설정해주면 entity별로 region을 구성해서 거기에 엔티티를 저장할 수 있다.
    • 기본 값은 Class 의 full name 이다.

Hibernate 캐시의 3가지 종류

hibernate는 다음과 같은 3가지 종류의 캐시를 지원한다.

  1. 엔티티 캐시
  2. 컬렉션 캐시
  3. 쿼리 캐시

1. 엔티티 캐시

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "foo")
public class Foo {
...
}
  • @Cacheable는 엔티티 캐시 적용시 사용하는 어노테이션이다. (hibernate 에서는 없어도 @Cache만으로 동작한다. mark 용으로 사용하는 것 같다..)
  • @Cache는 하이버네이트 전용으로 캐시와 관련돼서 더 세밀한 설정이 필요할 때 사용한다.
    • Foo 객체들은 "foo"라는 region에 저장된다.

EhCache API를 사용해서 다음과 같이 캐시에 바로 접근할 수도 있다.

Foo foo = new Foo();
fooService.create(foo);
fooService.findOne(foo.getId());
int size = CacheManager.ALL_CACHE_MANAGERS.get(0)
  .getCache("com.baeldung.hibernate.cache.model.Foo").getSize();
assertThat(size, greaterThan(0));
  • 여기서 com.baeldung.hibernate.cache.model.Foo는 Foo가 들어가는 region의 이름을 말한다.

2. 컬렉션 캐시

@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@OneToMany
	private Collection<Bar> bars;
}
  • 엔티티에 존재하는 컬렉션을 캐싱하고 싶다면 해당 컬렉션에 @Cache 어노테이션을 붙여줘야 한다.
  • 컬렉션의 들어있는 엔티티들의 id 값들만 캐싱된다.
  • hibernate는 컬렉션들을 각각 분리된 다른 region에 저장한다. region의 default 이름은 class의 full name + collection property 와 같다.(ex. com.baeldung.hibernate.cache.model.Foo.bars)

3. 쿼리 캐시

HQL(Hibernate Query Lang)의 결과를 캐싱할 수도 있다. 변경이 드문 엔티티들에 사용하면 유용하다.

쿼리 캐시를 사용하기 위해선 hibernate.cache.use_query_cache의 값을 true로 설정해줘야 한다.

hibernate.cache.use_query_cache=true

그리고 사용하는 쿼리에 queryHint 를 통해 캐시를 사용함을 명시해줘야한다.

entityManager.createQuery("select f from Foo f")
  .setHint("org.hibernate.cacheable", true)
  .getResultList();

Spring Data JPA를 사용하면 다음과 같다.

public interface UserRepository extends Repository<Foo, Long> {
 
  @QueryHints(value = {
              @QueryHint(name = "org.hibernate.cacheable", value = "true"),
              @QueryHint(name = "org.hibernate.cacheRegion", value = "user-by-lastname") // cache-region 값 설정
              })
  Page<User> findByLastname(String lastname, Pageable pageable);
}

참고

profile
내 머릿속 지우개

0개의 댓글