2차 캐시
1차 캐시와 2차 캐시
영속성 컨텍스트의 1차 캐시는 트랜잭션을 시작하고 종료할 때까지만 유효하다. OSIV를 사용해도 클라이언트의 요청이 들어올 때부터 끝날 때까지만 1차 캐시가 유효하다. 따라서 애플리케이션 전체로 보면 DB 접근 횟수를 획기적으로 줄이지는 못한다.
하이버네이트를 포함한 대부분의 JPA 구현체들은 애플리케이션 범위의 캐시를 지원하는데 이것을 공유 캐시 또는 2차 캐시라 한다.
1차 캐시
- 영속성 컨텍스트 내부에 존재하며 엔티티 매니저로 조회한다. 변경하는 모든 엔티티는 1차 캐시에 저장된다.
- 트랜잭션을 커밋하거나 플러시를 호출하면 1차 캐시에 있는 엔티티의 변경 내역을 DB에 동기화한다.
- OSIV를 사용하면 요청의 시작부터 끝까지 같은 영속성 컨텍스트를 유지할 수 있다.
- 1차 캐시는 끄고 키는 옵션이 아니며, 영속성 컨텍스트 자체가 사실상 1차 캐시를 의미한다.
2차 캐시
- 애플리케이션에서 공유하는 캐시를 JPA에서 공유 캐시라 하는데 일반적으로 2차 캐시라 부른다. 애플리케이션을 종료할 때까지 캐시가 유지된다. 분산 캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수도 있다.
- 엔티티 매니저를 통해 데이터를 조회할 때 우선 2차 캐시에서 조회하고 없으면 DB에서 조회한다. 따라서 2차 캐시를 적절히 활용하면 DB 접근 횟수를 획기적으로 줄일 수 있다.
- 2차 캐시는 동시성을 극대화하기 위해 객체를 직접 반환하지 않고 복사본을 만들어서 반환한다. 캐시한 객체를 그대로 반환하면 여러 곳에서 같은 객체를 동시에 수정하는 문제가 발생할 수 있다. 이 문제를 해결하려면 객체에 락을 걸어야 하는데, 이렇게 되면 동시성이 떨어질 수 있다.
- DB 기본키를 기준으로 캐싱하지만 영속성 컨텍스트가 다르면 객체 동일성을 보장하지 않는다.
JPA 2차 캐시
캐시 모드 설정
2차 캐시를 사용하려면 엔티티에 javax.persistence.Cacheable
애너테이션을 사용한다. @Cacheable(true)
, @Cacheable(false)
옵션을 설정할 수 있는데 기본값은 true다.
@Cacheable
@Entity
public class Member {
...
}
스프링 프레임워크를 사용할 경우 아래와 같이 EntityManagerFactory 설정에서 sharedCacheMode
속성을 정의하여 애플리케이션에 캐시를 어떻게 적용할지 설정할 수 있다.
@Primary
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean bean = builder.dataSource(dataSource).build();
bean.setPackagesToScan("패키지 경로");
bean.setPersistenceUnitName("영속성 컨텍스트 이름");
bean.setSharedCacheMode(SharedCacheMode.ENABLE_SELECTIVE);
return bean;
}
SharedCacheMode 옵션 | 설명 |
---|---|
ALL | 모든 엔티티를 캐시 |
NONE | 캐시를 사용하지 않음 |
ENABLE_SELECTIVE | Cacheable(true) 로 설정된 엔티티만 캐시 적용 |
DISABLE_SELECTIVE | 모든 엔티티를 캐시하는데 Cacheable(false) 로 명시된 엔티티는 캐시 적용 안함 |
UNSPECIFIED | JPA 구현체가 정의한 설정을 따름 |
캐시 조회, 저장 방식 설정
캐시를 무시하고 DB를 직접 조회하거나 캐시를 갱신할 때 캐시 조회 모드와 보관 모드를 사용한다. 캐시 모드에 따라 사용할 프로퍼티 옵션이 다르다.
캐시 모드 | 프로퍼티 명 | 옵션 |
---|---|---|
캐시 조회 모드 | javax.persistence.cache.retrieveMode |
javax.persistence.CacheRetrieveMode |
캐시 보관 모드 | javax.persistence.cache.storeMode |
javax.persistence.CacheStoreMode |
// find() 예제
Map<String, Object> param = new HashMap<String, Object>();
param.put("javax.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS);
param.put("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS);
em.find(TestEntity.class, id, param);
// JPQL 예제
em.createQuery("select e from TestEntity e where e.id = :id", TestEntity.class)
.setParameter("id", id)
.setHint("javax.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS)
.setHint("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS)
.getSingleResult();
JPA 캐시 관리 API
JPA는 캐시 관리를 위해 javax.persistence.Cache
인터페이스를 제공한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
Cache cache = emf.getCache();
boolean contains = cache.contains(TestEntity.class, testEntity.getId());
public interface Cache {
// 해당 엔티티가 캐시에 존재 여부 반환
public boolean contains(Class cls, Object primaryKey);
// 해당 엔티티중 특정 식별자를 가진 엔티티를 캐시에서 제거
public void evict(Class cls, Object primaryKey);
// 해당 엔티티 전체를 캐시에서 제거
public void evict(Class cls);
// 모든 캐시 데이터 제거
public void evictAll();
// JPA Cache 구현체 조회
public <T> T unwrap(Class<T> cls);
}
하이버네이트와 EHCACHE 적용
하이버네이트가 지원하는 캐시는 크게 3가지가 있다. JPA 표준에는 엔티티 캐시만 정의되어 있다.
-
엔티티 캐시 : 엔티티 단위로 캐시한다. 식별자로 엔티티를 조회하거나 컬렉션이 아닌 연관된 엔티티를 로딩할 때 사용한다.
-
컬렉션 캐시 : 엔티티와 연관된 컬렉션을 캐시한다. 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시한다.(하이버네이트 기능)
-
쿼리 캐시 : 쿼리와 파라미터 정보를 키로 사용해서 캐시한다. 결과가 엔티티면 식별자 값만 캐시한다.(하이버네이트 기능)
환경설정
하이버네이트에서 EHCACHE를 사용하려면 hibernate-ehcache
라이브러리를 추가해야 한다.
EHCACHE는 ehcache.xml 설정 파일에서 캐시 정책을 정의한다.
<!-- 파일경로 src/main/resources/ehcache.xml -->
<ehcache>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="1200"
timeToLiveSeconds="1200"
diskExpiryThreadIntervalSeconds="1200"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
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
하이버네이트 전용 애너테이션으로 세밀한 캐시 설정이 가능하다.
@Cacheable // 엔티티 캐시
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // 엔티티 캐시(하이버네이트 전용)
@Entity
public class Parent {
...
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // 컬렉션 캐시
@OneToMany(mappedBy = "parent")
private List<Child> childs;
}
@Cache 속성 |
설명 |
---|---|
usage | CacheConcurrencyStrategy를 사용해서 캐시 동시성 전략을 설정한다. |
region | 캐시 지역 설정 |
include | 연관 객체를 캐시에 포함할지 선택한다. |
CacheConcurrencyStrategy 속성 |
설명 |
---|---|
NONE | 캐시를 설정하지 않는다. |
READ_ONLY | 읽기 전용으로 설정한다. 등록, 삭제는 가능하지만 수정은 불가능하다. 참고로 읽기 전용인 불변 객체는 수정되지 않으므로 하이버네이트는 2차 캐시를 조회할 때 객체를 복사하지 않고 원본 객체를 반환한다. |
NONSTRICT_READ_WRITE | 엄격하지 않은 읽고 쓰기 전략이다. 동시에 같은 엔티티를 수정하면 데이터 일관성이 깨질 수 있다. EHCACHE는 데이터를 수정하면 캐시 데이터를 무효화한다. |
READ_WRITE | 읽기 쓰기가 가능하고 READ COMMITTED 정도의 격리 수준을 보장한다. EHCACHE는 데이터를 수정하면 캐시 데이터도 같이 수정한다. |
TRANSACTIONAL | 컨테이너 관리 환경에서 사용할 수 있다. 설정에 따라 REPEATABLE READ 정도의 격리 수준을 보장받을 수 있다. |
캐시 영역
캐시를 적용한 코드는 캐시 영역(Cache Region)에 저장된다.
- 엔티티 캐시 영역 : 기본값 [패키지명 + 클래스명] (
jpa.domain.test.cache.Parent
) - 컬렉션 캐시 영역 : 기본값 [엔티티 캐시 영역 이름 + 필드명] (
jpa.domain.test.cache.Parent.child
)
필요하다면 @Cache(region="customRegion", ...)
과 같이 캐시 영역을 직접 지정할 수 있다. 캐시 영역이 정해져 있으면 세부 설정이 가능하다.
<ehcache>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="1200"
timeToLiveSeconds="1200"
diskExpiryThreadIntervalSeconds="1200"
memoryStoreEvictionPolicy="LRU"/>
<cache
name="jpa.domain.test.cache.Parent"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="600"
overflowToDisk="false"/>
</ehcache>
쿼리 캐시
쿼리 캐시는 쿼리와 파라미터 정보를 키로 사용해서 쿼리 결과를 캐시하는 방법이다.
쿼리 캐시를 적용하려면 영속성 유닛 설정에 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 {
...
}
쿼리 캐시 영역
use_query_cache
옵션을 true로 설정해서 쿼리 캐시를 활성화하면 다음 두 캐시 영역이 추가된다.
org.hibernate.cache.internal.StandardQueryCache
: 쿼리 캐시를 저장하는 영역이다. 이 곳에는 쿼리, 쿼리 결과 집합, 쿼리를 실행한 시점의 타임스탬프를 보관한다.org.hibernate.cache.spi.UpdateTimestampsCache
: 쿼리 캐시가 유효한지 확인하기 위해 쿼리 대상 테이블의 가장 최근 변경(등록, 수정, 삭제) 시간을 저장하는 영역이다. 이 곳에는 테이블명과 해당 테이블의 최근 변경된 타임스탬프를 보관한다.
쿼리 캐시는 캐시한 데이터 집합을 최신 데이터로 유지하려고 쿼리 캐시를 실행하는 시간과 쿼리 캐시가 사용하는 테이블들이 가장 최근에 변경된 시간을 비교한다. 쿼리 캐시 적용 후 쿼리 캐시가 사용하는 테이블에 변경이 있으면 DB에서 데이터를 읽어와서 쿼리 결과를 다시 캐시한다.
org.hibernate.cache.spi.UpdateTimestampsCache
쿼리 캐시 영역은 만료되지 않도록 설정해야 한다. 해당 영역이 만료되면 모든 쿼리 캐시가 무효화된다. EHCACHE의 eternal="true"
옵션을 사용하면 캐시에서 삭제되지 않는다.
쿼리 캐시를 잘 활용하면 극적인 성능 향상이 있지만 빈번하게 변경이 있는 테이블에 사용하면 오히려 성능이 저하된다. 따라서 수정이 거의 일어나지 않는 테이블에 사용해야 효과를 볼 수 있다.
쿼리 캐시와 컬렉션 캐시의 주의점
- 엔티티 캐시를 사용해서 엔티티를 캐시하면 엔티티 정보를 모두 캐시하지만 쿼리 캐시와 컬렉션 캐시는 결과 집합의 식별자 값만 캐시한다. 따라서 쿼리 캐시와 컬렉션 캐시를 조회하면 그 안에는 식별자 값만 들어 있다. 식별자 값을 하나씩 엔티티 캐시에 조회해서 실제 엔티티를 찾는다. 문제는 쿼리 캐시나 컬렉션 캐시만 사용하고 대상 엔티티에 엔티티 캐시를 적용하지 않으면 아래와 같이 성능상 심각한 문제가 발생할 수 있다.
select m from Member m
쿼리를 실행했는데 쿼리 캐시가 적용되어 있다. 조회결과는 100건이다.- 결과 집합에는 식별자만 있으므로 한 건씩 엔티티 캐시 영역에서 조회한다.
- Member 엔티티는 엔티티 캐시를 사용하지 않으므로 한 건씩 DB에서 조회한다.
- 100건의 SQL이 실행된다.
쿼리 캐시나 컬렉션 캐시만 사용하고 엔티티 캐시를 사용하지 않으면 최악의 상황에 결과 집합 수 만큼 SQL이 실행된다. 따라서 쿼리 캐시나 컬렉션 캐시를 사용하면 결과 대상 엔티티에는 꼭 엔티티 캐시를 적용해야 한다.
Comments