하이버네이트와 EHCACHE 적용
하이버네이트와 EHCACHE(ehcache.org)를 사용해서 2차 캐시를 적용해보자.
하이버네이트가 지원하는 캐시는 크게 3가지가 있다.
- 엔티티 캐시
엔티티 단위로 캐시한다. 식별자로 엔티티를 조회하거나 컬렉션이 아닌 연관된 엔티티를 로딩할 때 사용한다. - 컬렉션 캐시
엔티티와 연관된 컬렉션을 캐시한다. 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시한다.(하이버네이트 기능) - 쿼리 캐시
쿼리와 파라미터 정보를 키로 사용해서 캐시한다. 결과가 엔티티면 식별자 값만 캐시한다.(하이버네이트 기능)
참고로 JPA 표준에는 엔티티 캐시만 정의되어 있다.
환경설정
build.gradle에 cache 라이브러리를 추가한다.
dependencies {
// https://mvnrepository.com/artifact/org.hibernate/hibernate-ehcache
implementation group: 'org.hibernate', name: 'hibernate-ehcache', version: '4.3.11.Final'
}
src/main/resources/ehcache.xml 파일을 생성하고 캐시 정책을 정의한다.
옵션에 대한 설명은 최고영회님 블로그에 잘 정리되어있다.
<ehcache>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="1200"
timeToLiveSeconds="1200"
diskExpiryThreadIntervalSeconds="1200"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
yml 파일에 하이버네이트 캐시 사용정보를 정의한다.
spring:
jpa:
properties:
hibernate:
generate_statistics: true
format_sql: true
cache:
use_second_level_cache: true
region:
factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory // EHCACHE
javax:
persistence:
sharedCache:
mode: ENABLE_SELECTIVE
- hibernate.cache.use_second_level_cache : 2차 캐시를 활성화 한다. 엔티티 캐시와 컬렉션 캐시를 사용할 수 있다.
- hibernate.cache.use_query_cach : 쿼리 캐시를 활성화 한다.
- hibernate.cache.region.factory_class : 2차 캐시를 처리할 클래스를 지정한다.
- hibernate.generate_statistics : 이 속성을 true로 설정하면 하이버네이트가 여러 통계정보를 출력해주는데 캐시 적용 여부를 확인할 수 있다.(성능에 영향을 주므로 개발 황경에서만 적용해야 한다.)
@Cache
하이버네이트 전용인 @Cache 어노테이션을 사용하면 세밀한 캐시 설정이 가능하다.
@Cacheable // 엔티티 캐시
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // 엔티티 캐시(하이버네이트 전용)
@Entity
public class Parent {
....
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // 컬렉션 캐시
@OneToMany(mappedBy = "parent")
private List<Child> childs;
}
속성 | 설명 |
usage | CacheConcurrencyStrategy를 사용해서 캐시 동시성 전략을 설정한다. |
region | 캐시 지역 설정 |
include | 연관 객체를 캐시에 포함할지 선택한다. all, non-laze 옵션을 선택할 수 있다.(기본값 all) |
CacheConcurrencyStrategy 속성
속성 | 설명 |
NONE | 캐시를 설정하지 않는다. |
READ_ONLY | 읽기 전용으로 설정한다. 등록, 삭제는 가능하지만 수정은 불가능하다. 참고로 읽기 전용인 불변 객체는 수정되지 않으므로 하이버네이트는 2차 캐시를 조회할 때 객체를 복사하지 않고 원본 객체를 반환한다. |
NONSTRICT_READ_WRITE | 엄격하지 않은 읽고 쓰기 전략이다. 동시에 같은 엔티티를 수정하면 데이터 일관성이 깨질 수 있다. EHCACHE는 데이터를 수정하면 캐시 데이터를 무효화한다. |
READ_WRITE | 읽기 쓰기가 가능하고 READ COMMITTED 정도의 격리 수준을 보장한다. EHCACHE는 데이터를 수정하면 캐시 데이터도 같이 수정한다. |
TRANSACTIONAL | 컨테이너 관리 환경에서 사용할 수 있다. 설정에 따라 REPEATABLE READ 정도의 격리 수준을 보장받을 수 있다. |
캐시 동시성 전략 지원 여부
Cache | read-only | nonstrict-read-write | read-write | transactional |
ConcurrentHashMap | yes | yes | yes | |
EHCache | yes | yes | yes | yes |
Infinispan | yes | yes |
캐시 영역
위에서 캐시를 적용한 코드는 다음 캐시 영역에 저장된다
- 엔티티 캐시 영역 : jpabook.jpashop.domain.test.cache.Parent
- 컬렉션 캐시 영역 : jpabook.jpashop.domain.test.cache.Parent.child
엔티티 캐시 영역은 기본값으로 [패키지 명 + 클래스 명]을 사용하고, 컬렉션 캐시 영역은 엔티티 캐시 영역 이름에 캐시한 컬렉션의 필드 명이 추가된다.
필요하다면 @Cache(region = "customRegion", ...) 처럼 region 속성을 사용해서 캐시 영역을 직접 지정할 수 있다.
캐시 영역을 위한 접두사를 설정하려면 yml 설정에 hibernate.cache.region_prefix 를 사용하면 된다. 예를 들어 core로 설정하면 core.jpabook.jpashop... 으로 설정된다.
캐시 영역이 정해져 있으므로 영역별로 세부 설정을 할 수 있다.
만약 Parent를 600초 마다 캐시에서 제거하고 싶으면 EHCACHE를 다음과 같이 설정하면 된다.
<ehcache>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="1200"
timeToLiveSeconds="1200"
diskExpiryThreadIntervalSeconds="1200"
memoryStoreEvictionPolicy="LRU" />
<cache
name="jpabook.jpashop.domain.test.cache.Parent"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="600"
overflowToDisk="false" />
</ehcache>
쿼리 캐시
쿼리 캐시는 쿼리와 파라미터 정보를 키로 사용해서 쿼리 결과를 캐시하는 방법이다.
쿼리 캐시를 적용하려면 영속성 유닛 설정에 hibernate.cache.use_query_cache 옵션을 꼭 true로 설정해야 한다.
그리고 쿼리 캐시를 적용하려는 쿼리마다 org.hibernate.cacheable을 true로 설정하는 힌트를 주면 된다.
// 쿼리 캐시 사용
em.createQuery("select i from Item i", Item.class)
.setHint("org.hibernate.cacheable", true)
.getResultList();
// NamedQuery에 쿼리 캐시 적용
@Entity
@NamedQuery(
hints = @QueryHint(name = "org.hibernate.cacheable", value = "true"),
name = "Member.findByUsername",
query = "select m.address from Member m where m.name = :username"
)
public class Member {
...
}
쿼리 캐시 영역
hibernate.cache.use_query_cache 옵션을 true로 설정해서 쿼리 캐시를 활성화 하면 다음 두 캐시 영역이 추가된다.
- org.hibernate.cache.internal.StandardQueryCache
쿼리 캐시를 저장하는 영역이다. 이곳에는 쿼리, 쿼리 결과 집합, 쿼리를 실행한 시점의 타임스탬프를 보관한다. - org.hibernate.cache.spi.UpdateTimestampsCache
쿼리 캐시가 유효한지 확인하기 위해 쿼리 대상 테이블의 가장 최근 변경(등록, 수정, 삭제) 시간을 저장하는 영역이다. 이곳에는 테이블 명과 해당 테이블의 최근 변경된 타임스탬프를 보관한다.
쿼리 캐시는 캐시한 데이터 집합을 최신 데이터로 유지하려고 쿼리 캐시를 실행하는 시간과 쿼리 캐시가 사용하는 테이블들이 가장 최근에 변경된 시간을 비교한다. 쿼리 캐시를 적용하고 난 후에 쿼리 캐시가 사용하는 테이블에 조금이라도 변경이 있으면 데이터베이스에서 데이터를 읽어와서 쿼리 결과를 다시 캐시한다.
쿼리 캐시를 잘 활용하면 극적인 성능 향상이 있지만 빈번하게 변경이 있는 테이블에 사용하면 오히려 성능이 더 저하된다. 따라서 수정이 거의 일어나지 않는 테이블에 사용해야 효과를 볼 수 있다.
org.hibernate.cache.spi.UpdateTimestampsCache 쿼리 캐시 영역은 만료되지 않도록 설정해야 한다. 해당 영역이 만료되면 모든 쿼리 캐시가 무효화된다.
EHCACHE의 eternal="true" 옵션을 사용하면 캐시에서 삭제되지 않는다.
쿼리 캐시와 컬렉션 캐시의 주의점
엔티티 캐시를 사용해서 엔티티를 캐시하면 엔티티 정보를 모두 캐시하지만 쿼리 캐시와 컬렉션 캐시는 결과 집합의 식별자 값만 캐시한다. 따라서 쿼리 캐시와 컬렉션 캐시를 조회하면 그 안에는 사식 실별자 값만 들어 있다.
그리고 이 식별자 값을 한나씩 엔티티 캐시에 조회해서 실제 엔티티를 찾는다.
문제는 쿼리 캐시나 컬렉션 캐시만 사용하고 대상 엔티티에 엔티티 캐시를 적용하지 않으면 성능상 심각한 문제가 발생할 수 있다.
예를 들어 보자.
- select m from Member m 쿼리를 실행했는데 쿼리 캐시가 적용되어 있다. 조회 결과는 100건이다.
- 결과 집합에는 식별자만 있르므로 한 건씩 엔티티 캐시 영역에서 조회한다.
- Member 엔티티는 엔티티 캐시를 사용하지 않으므로 한 건씩 데이터베이스에서 조회한다.
- 100건의 SQL이 실행된다.
쿼리 캐시나 컬렉션 캐시만 사용하고 엔티티 캐시를 사용하지 않으면 최악의 상황에 결과 집합 수 만큼 SQL이 실행된다. 따라서 쿼리 캐시나 컬렉션 캐시를 사용하면 결과 대상 엔티티에는 꼭 엔티티 캐시를 적용해야 한다.
'JPA' 카테고리의 다른 글
Querydsl을 모듈화하여 쉽게 사용하기 (0) | 2024.06.26 |
---|---|
JPA 2차 캐시 기능 (0) | 2022.01.27 |
JPA 1차 캐시와 2차 캐시 소개 (0) | 2022.01.26 |
JPA에서 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock) 사용 (0) | 2022.01.26 |
낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)의 기초 (0) | 2022.01.26 |