문서의 이전 판입니다!
동시성 전략 4가지가 있는데 hibernate 문서 설명이 부족해서 로그를 찍어보며 확인해봄.
기본적으로 evict
와 evictAll
은 기본 흐름상에서 Hibernate에 의해 직접 호출되지는 않는다. 다른 메소드들에 의해 필요해 의해 호출된다. 이 두 메소드는 javax.persistence.Cache의 evict
, evictAll
메소드가 실행될 때 호출된다.
테스트시에 쿼리 삭제, 수정 등을 하고서 flush후에 rollback 하면 어떻게 되는지 테스트
읽기 전용. 우편 번호처럼 잘 안 변하는 것들에 적합할 듯. 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을 수행한다.엄격하지 않은 읽기/쓰기. 객체 동시 수정 등에 대한 고려를 하지 않고 캐싱을 한다. 이 방식은 하나의 객체가 동시에 수정될 가능성이 거의 없을 때 사용한다.
동시 수정이 적은 데이터에 대한 분산 캐시에서는 이것이 적합해 보인다.
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 + 클러스터링 캐시 구현체를 사용할 경우 캐시 구현체는 Lock 기능을 제공해야만 한다. 객체 생성, 수정시에 캐시 서버에 대한 접근이 매우 빈번하여 분산 캐시에서는 성능이 떨어질 수 있다. 아래는 Memcached기준으로 생각해본 구현전략.
get
: 캐시에서 읽어서 있으면 반환 한다. txTimestamp로 작업필요할 듯.putFromLoad
: 캐시에서 값을 읽어 존재하지 않으면 무조건 쓰고, 이미 캐시가 존재하며 현재 가진 것이 최신이면 덮어쓰고, 이미 있는 캐시가 더 최신 데이터이면 현재 것을 버린다.lockItem
insert
afterInsert
: 보통은 이것만 구현해도 될 듯. 객체를 캐시에서 가져와보고 이미 캐시가 존재하면 DB insert 직후 누군가가 이미 읽어가서 putFromLoad
로 인해 캐시가 생성 됐다는 뜻이므로 cache에 다시 저장하지 말고 빠져나옴.unlockItem
lockItem
update
afterUpdate
: 보통은 이것만 구현해도 될 듯. 객체를 cache에서 get해보고 객체 version등을 체크해서 현재 가진것보다 최신이면 수정하지 말고, 현재가 더 최신이면 캐시 갱신unlockItem
JTA에서만 사용한다.
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);
@Cache
애노테이션이 있으면 캐시를 무조건 수행했다. 즉, 이런 상황에서 캐시를 명시적으로 끄려면 hibernate.cache.use_second_level_cache=false
를 명시적으로 지정해야만 한다.<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( 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인 필드들만@Cacheable
애노테이션을 사실상 무시했고, @Cache
애노테이션만으로 작동하였다.@Entity @Table(name = "books") @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "books") public class Book implements Serializable { .... }
엔티티의 컬렉션을 캐시하려면 해당 컬렉션에 @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 쪽에 수정이 발생해야만 캐시가 갱신된다.
org.hibernate.cache.internal.StandardQueryCache
region에 캐시 결과를 저장한다.org.hibernate.cache.spi.UpdateTimestampsCache
region을 통해 특정 Entity의 timestamp를 저장한다.setHint
를 통해 캐시하도록 지정TypedQuery<Book> query = em.createNamedQuery("Book.byEdition", Book.class); query.setParameter("edition", 3); query.setHint("org.hibernate.cacheable", true); 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 { .... }
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); }