사용자 도구

사이트 도구


java:hibernate:cache

Hibernate Cache

Hibernate의 기본 캐시 전략

  • Memcached 등의 외부 서버보다는 로컬 JVM에서 값을 즉시 읽을 수 있는 ehcacheInfinispan 계통을 사용할 것.
  • 기본적으로 Replication-Invalidation 전략을 사용한다.
  • ehcache 사용시에
    • Entity와 컬렉션은 replicatePuts=false, replicateUpdatesViaCopy=false를 통해 네트워크 부하를 낮춘다. 이는 또한 여러 서버의 순차 배포시에 Class 버전이 서로 안 맞는 상황에서 직렬화 오류나는 것을 방어해준다. 비동기로 한다.
    • org.hibernate.cache.internal.StandardQueryCache는 완전 local 캐시로만 적용한다.
    • org.hibernate.cache.spi.UpdateTimestampsCachereplicatePuts=true, replicateUpdatesViaCopy=true 및 비동기방식을 통해 모든 서버가 일관성있고 동일한 값을 유지하게 한다. 또한 서버가 뜰때 다른 서버의 UpdateTimestampsCache들을 복제해 와서 사용해야 한다.

Cuncurrency Strategy

동시성 전략 4가지가 있는데 hibernate 문서 설명이 부족해서 로그를 찍어보며 확인해봄.

기본적으로 evictevictAll은 기본 흐름상에서 Hibernate에 의해 직접 호출되지는 않는다. 다른 메소드들에 의해 필요해 의해 호출된다. 이 두 메소드는 javax.persistence.Cacheevict, evictAll메소드가 실행될 때 호출된다.

테스트시에 쿼리 삭제, 수정 등을 하고서 flush후에 rollback 하면 어떻게 되는지 테스트

READ_ONLY

읽기 전용. 우편 번호처럼 잘 안 변하는 것들에 적합할 듯. Read Only의 의미가 수정만 못하게 하는 것인지 수정과 삭제를 둘 다 못하게 하는 것인지에 대해서는 확실치 않다. 그래서 삭제는 가능한 것으로 가정하고 스토리를 짜면 아래 방식으로 했을 때 잘 작동한다. 삭제도 못하게 하려 한다면 lockItem에서도 예외를 던져야 하는 것이 맞아 보인다.

@org.hibernate.annotations.Immutable로 지정된 엔티티와 컬렉션에 사용하는 것이 좋을 듯 하다.

  • 읽기
    • get : 캐시에서 읽어서 있으면 반환 한다.
  • 최초 데이터 가져온 후 캐시 생성
    • ​putFromLoad​ : minimalPutOverride == true일 경우에는 이미 캐시에 값이 존재하면 현재 것을 버린다. 아니면 항상 값을 캐시에 쓴다. 캐시에 썼으면 ​return true;​ 아니면 ​return false;​. ​putFromLoad​는 데이터는 가져왔으나 캐시가 없었을 때 호출되는 것인데, ​그 사이에 다른 쓰레드에서 ​캐시에 ​데이터를 썼다면 ​굳이 다시 쓸 필요는 없다.
  • 입력 : insert/afterInsert : 객체를 persist해도 호출하지 않음. 즉, persist시 캐싱 불가. return false;
  • 수정 - 예외를 발생시켜야 한다.
    • lockItem : 객체 수정 전에 호출, READ_ONLY이므로 수정이 말이 안되니 return null;. 여기서 예외를 던지면 삭제 기능이 작동할 수 없다.
    • update : 객체 수정 쿼리 직후 호출. READ_ONLY인 상황에서 객체 수정은 오류이므로 UnsupportedOperationException을 던진다.
    • afterUpdate : update에서 예외가 발생하지 않으면 호출된다. 그러나 위에서 이미 예외를 발생시켰으므로 원칙적으로는 호출돼서는 안된다. 하지만 아무튼 UnsupportedOperationException을 던지는 것이 권고인 듯.
    • unlockItem : update에서 예외를 발생 시켰으면 호출된다. 따라서 이것은 항상 호출돼야 한다. 여기서 캐시를 evict시킨다.
  • 삭제시 :
    • lockItem : 객체 삭제 전에 호출된다. 아무것도 하지 않고 return null;
    • remove : 삭제 쿼리 후 실행된다. 여기서 evict를 호출하여 캐시를 삭제한다.
    • unlockItem : remove 후에 호출된다. 아무것도 하지 않는다.
  • 삭제 쿼리
    • lockRegion : 쿼리 실행 전에 호출된다. 아무것도 하지 않고 retur null;
    • removeAll : 쿼리 실행 전에 호출된다. 아무것도 하지 않는다. 실제 쿼리가 날라가기 전이므로 이 시기에 캐시를 삭제하면 쿼리 결과가 Rollback시에 불필요한 부하만 증가할 것으로 보인다.
    • unlockRegion : 쿼리 실행 후에 호출된다. 여기서 evictAll을 호출한다. evictAll에서 실질적인 eviction을 수행한다.

NONSTRICT_READ_WRITE

엄격하지 않은 읽기/쓰기. 객체 동시 수정 등에 대한 고려를 하지 않고 캐싱을 한다. 이 방식은 하나의 객체가 동시에 수정될 가능성이 거의 없을 때 사용한다.

동시 수정이 적은 데이터에 대한 분산 캐시에서는 이것이 적합해 보인다.

  • 읽기
    • get : 캐시에서 읽어서 있으면 반환 한다.
  • 최초 데이터 가져온 후 캐시 생성
    • putFromLoad : minimalPutOverride == true일 경우에는 이미 캐시에 값이 존재하면 현재 것을 버린다. 아니면 항상 값을 캐시에 쓴다. 캐시에 썼으면 return true; 아니면 return false;
  • 입력 : insert/afterInsert : 객체를 persist해도 호출하지 않음. 즉, persist시 캐싱 불가. return false;
  • 수정
    • lockItem : 객체 수정 전에 호출, 캐시를 삭제할 것이기 때문에 Lock 불필요. return null;
    • update : 객체 수정 쿼리 직후 호출. 아무것도 하지 않고 수정이 없었다는 의미로 return false;.
    • afterUpdate : update 호출 직후 호출되며 여기서 캐시를 수정하지 말고 즉시 삭제(evict)해 버릴 것. 그리고 수정이 없었다는 의미로 return false;
    • unlockItem : 호출 안됨.
  • 삭제
    • lockItem : 객체 삭제 전에 호출된다. 아무것도 하지 않고 return null;
    • remove : 삭제 쿼리 후 실행된다. 여기서 evict를 호출하여 캐시를 삭제한다.
    • unlockItem : remove 후에 호출된다. 아무것도 하지 않는다.
  • 수정/삭제 쿼리
    • lockRegion : 쿼리 실행 전에 호출된다. 아무것도 하지 않고 retur null;
    • removeAll : 쿼리 실행 전에 호출된다. 아무것도 하지 않는다. 실제 쿼리가 날라가기 전이므로 이 시기에 캐시를 삭제하면 쿼리 결과가 Rollback시에 불필요한 부하만 증가할 것으로 보인다.
    • unlockRegion : 쿼리 실행 후에 호출된다. 여기서 evictAll을 호출한다. evictAll에서 실질적인 eviction을 수행한다.

READ_WRITE

엄격한 읽기/쓰기로 두개 이상의 쓰레드에서 동시 수정할 가능성에 대해 고려하고 만들어야 한다. READ_WRITE + 클러스터링 캐시 구현체를 사용할 경우 캐시 구현체는 Lock 기능을 제공해야만 한다. 객체 생성, 수정시에 캐시 서버에 대한 접근이 매우 빈번하여 분산 캐시에서는 성능이 떨어질 수 있다. 아래는 Memcached기준으로 생각해본 구현전략.

  • 읽기
    • get : 캐시에서 읽어서 있으면 반환 한다. txTimestamp로 작업필요할 듯.
  • 최초 데이터 가져온 후 캐시 생성
    • putFromLoad : 캐시에서 값을 읽어 존재하지 않으면 무조건 쓰고, 이미 캐시가 존재하며 현재 가진 것이 최신이면 덮어쓰고, 이미 있는 캐시가 더 최신 데이터이면 현재 것을 버린다.
  • 입력
    • lockItem
    • insert
    • afterInsert : 보통은 이것만 구현해도 될 듯. 객체를 캐시에서 가져와보고 이미 캐시가 존재하면 DB insert 직후 누군가가 이미 읽어가서 putFromLoad로 인해 캐시가 생성 됐다는 뜻이므로 cache에 다시 저장하지 말고 빠져나옴.
    • unlockItem
  • 수정
    • lockItem
    • update
    • afterUpdate : 보통은 이것만 구현해도 될 듯. 객체를 cache에서 get해보고 객체 version등을 체크해서 현재 가진것보다 최신이면 수정하지 말고, 현재가 더 최신이면 캐시 갱신
    • unlockItem
  • 삭제
    • 살펴보자…

TRANSACTIONAL

JTA에서만 사용한다.

Hibernate Cache 활성화

Map<String,Object> props = new HashMap<String,Object>();
 
// hibernate.cache.use_second_level_cache
props.put(AvailableSettings.USE_SECOND_LEVEL_CACHE, true); 
 
// hibernate.cache.use_query_cache
props.put(AvailableSettings.USE_QUERY_CACHE, true);
 
// hibernate.cache.region.factory_class - 캐시 구현체 지정
props.put(AvailableSettings.CACHE_REGION_FACTORY, CachingRegionFactory.class.getName());
 
// hibernate.cache.region_prefix
props.put(AvailableSettings.CACHE_REGION_PREFIX, "cachetest");
 
// hibernate.cache.default_cache_concurrency_strategy
props.put(AvailableSettings.DEFAULT_CACHE_CONCURRENCY_STRATEGY, CacheConcurrencyStrategy.READ_WRITE); 
 
// ...
EntityManagerFactory emf = Persistence.createEntityManagerFactory("cachetest", props);
  • Hibernate는 @Cache 애노테이션이 있으면 캐시를 무조건 수행했다. 즉, 이런 상황에서 캐시를 명시적으로 끄려면 hibernate.cache.use_second_level_cache=false를 명시적으로 지정해야만 한다.

JPA

  • JPA 캐시의 경우 원칙적으로 <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode> 등의 shared-cache-mode 설정이 필요하지만, Hibernate Cache Annotaion과 Hint를 사용할 때는 이 값을 완전히 무시하는 것으로 보인다.
      • ENABLE_SELECTIVE : 명시적으로 @Cacheable(true) 인 엔티티만 캐시
      • DISABLE_SELECTIVE : 모든 엔티티를 캐시하되 @Cacheable(false) 제외
      • NONE : 캐시 안함
  • 해당 값은 javax.persistence.sharedCache.mode 프라퍼티로 설정해도 된다.

@Cache

@Cache(
    CacheConcurrencyStrategy usage();                      (1)
    String region() default "";                            (2)
    String include() default "all";                        (3)
)
  • usage : NONE, READ_ONLY, NONSTRICT_READ_WRITE, READ_WRITE, TRANSACTIONAL
  • region : Region. 안적으면 FQCN 기준으로 자동 생성
  • include
    • all : 모든 필드
    • non-lazy : non-lazy인 필드들만
  • Hibernate는 JPA의 @Cacheable 애노테이션을 사실상 무시했고, @Cache 애노테이션만으로 작동하였다.

Entity Cache

  • Entity는 update 쿼리 실행시 region의 expire가 발생한다.
@Entity
@Table(name = "books")
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "books")
public class Book implements Serializable {
....
}

Entity Collection Cache

기본

엔티티의 컬렉션을 캐시하려면 해당 컬렉션에 @Cache 애노테이션을 붙여야 한다.

@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@JoinColumn(name="CUST_ID")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public SortedSet<Ticket> getTickets() {
    return tickets;
}

양방향 관계의 경우 명시적으로 Collection 쪽에 수정이 발생해야만 캐시가 갱신된다.

OneToOne not owning size

  • OneToOne 관계의 not owning side(mappedBy를 명시한 부모측)에는 캐시가 걸리지 않는 것으로 보인다.참조

Query Cache

  • Query Cache는 기본적으로 region을 지정하지 않으면 org.hibernate.cache.internal.StandardQueryCache region에 캐시 결과를 저장한다.
  • query는 명시적 expire는 없고, entity가 수정될 경우 해당 entity의 update time stamp를 기반으로 쿼리를 다시 날릴지 아니면 캐시된 데이터를 사용할지 결정한다.
  • query는 org.hibernate.cache.spi.UpdateTimestampsCache region을 통해 특정 Entity의 timestamp를 저장한다.
  • UpdateTimestampCache에 의해 query의 자동 expire가 이루어 지므로 굳이 Query에 대한 region을 따로 지정하지 않아도 된다.
  • setHint를 통해 캐시하도록 지정
    TypedQuery<Book> query = em.createNamedQuery("Book.byEdition", Book.class);
    query.setParameter("edition", 3);
    query.setHint("org.hibernate.cacheable", true);
    query.setHint("org.hibernate.cacheMode", CacheMode.NORMAL);
    query.setHint("org.hibernate.cacheRegion", "book-by-edition"); // region 지정
  • NamedQuery에 지정
    @Entity
    @Table(name = "books")
    @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "books")
    @NamedQuery(name = "Book.byEdition",
            query = "from Book where edition=:edition",
            hints = {
                    @QueryHint(name = "org.hibernate.cacheable", value = "true"),
                    @QueryHint(name = "org.hibernate.cacheRegion", value = "book-by-edition")
            }
    )
    public class Book implements Serializable {
    ....
    }

Spring Data JPA Query Cache

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

Evict All

현재 Hibernate는 모든 리젼(Region)을 evict하려면 다음을 호출해야 한다.

org.hibernate.Cache cache = sessionFactory.getCache();
cache.evictEntityRegions();
cache.evictQueryRegions();
cache.evictDefaultQueryRegion();
cache.evictCollectionRegions();

Cache 관련 Properties

  • org.hibernate.cacheable Whether or not a query is cacheable ( eg. new Boolean(true) ), defaults to false
  • org.hibernate.cacheMode Override the cache mode for this query ( eg. CacheMode.REFRESH ) - CacheMode
  • org.hibernate.cacheRegion Cache region of this query ( eg. new String(“regionName”) )

Query 실행 후 Entity Cache 갱신 문제

  • PK 기반 쿼리는 Cache에서 (있으면) 값을 가져오고, 쿼리 실행 후 필요하면 Cache에 값을 설정한다.
  • 하지만 Query 실행시에는 (Query Cache 가 꺼져 있으면) 쿼리 결과 Entity들을 다시 Cache에 넣는 일을 한다(putFromLoad).
  • minimalPutOverride=true가 이를 방지하는 역할을 한다. 캐시 구현체가 minimalPutOverride를 구현하지 않았을 수도 있다.
  • 하지만 minimalPutOverride를 켜지 않았을 경우에는 org.hibernate.cacheMode=CacheMode.GET 혹은 그 외 값으로 통해 해당 쿼리에 대한 캐시 사용을 꺼버린다.

참조문서

java/hibernate/cache.txt · 마지막으로 수정됨: 2018/09/13 14:28 저자 kwon37xi