트랜잭션과 락 #
2차 캐시 #
1차 캐시와 2차 캐시
영속성 컨텍스트 내부에 Entity 를 보관하는 저장소, 1차캐시가 있다.
1차캐시를 통해 얻을 수 있는 이점은 많이 있지만 일반적으로 1차캐시의 유효 범위는 짧기 때문에 큰 이점을 보기는 힘들다.
* 1차캐시는 트랜잭션 시작 ~ 끝에서 유효하다. OSIV 를 사용해도 클라이언트의 요청이 들어온 시점 ~ 끝날 때까지만 유효하다.
하이버네이트를 포함하여 대부분의 JPA 구현체는 애플리케이션 범위의 캐시를 지원한다. 이것을 ‘공유 캐시’, ‘2차 캐시’라고 한다.
* 2차 캐시를 활용하면 애플리케이션의 조회 성능을 향상할 수 있다.
애플리케이션 범위의 캐시는 애플리케이션이 종료될 때까지 유지되는 캐시를 의미한다. 분산 캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수 있다.
2차 캐시를 적절히 활용하면 데이터베이스 접근 횟수를 획기적으로 줄일 수 있다.
2차 캐시의 구조(원리)는 1차 캐시와 동일하다. 다만 2차 캐시에서는 엔티티를 내어줄 때, 복사본을 내어준다.
복사본을 내어주는 이유는 ‘동시성’을 위함이다. 만약 객체 그대로를 내어준다면 동시에 여러 곳에서 같은 객체를 수정하는 문제가 발생할 수 있다. 이를 해결하려면 객체에 락을 걸어줘야하는데, 이는 동시성을 떨어뜨릴 수 있다. 따라서, 2차 캐시는 원본 대신에 복사본을 반환한다.
2차캐시의 특징은 다음과 같다.
- 영속성 유닛 범위의 캐시다.
- DB로부터 조회한 객체를 그대로 반환하지 않고, 복사본을 만들어서 반환한다.
- DB의 PK 를 기준으로 캐시하지만, 영속성 컨텍스트가 다르면 객체 동일성(a==b)을 보장하지 않는다.
JPA 2차 캐시 기능
JPA 2.0 부터 캐시에 대한 표준이 정의되었다고 한다. 다만 여러 구현체가 공통으로 사용하는 부분에 대해서만 표준화되었기에, (세밀한 기능은) 구현체에 의존해야 한다.
2차 캐시를 사용하려면 아래와 같은 어노테이션을 사용한다.
javax.persistence.Cacheable
@Cacheable
@Entity
public class Memeber {
@Id @GeneratedValue
private Long id;
...
}
다음으로 xml 에 shared-cache-mode 를 설정한다. 이는 애플리케이션 전체에 캐시를 어떻게 적용할지 설정하는 것이다.
* 보통 ENABLE_SELECTIVE
를 사용한다고 한다.
- ALL : 모든 엔티티를 캐시한다.
- NONE : 캐시를 사용하지 않는다.
- ENABLE_SELECTIVE : Cacheable(true) 로 설정된 엔티티에만 캐시를 적용한다.
- DISABLE_SELECTIVE : 모든 엔티티를 캐시하는데, Cacheable(false)로 명시된 엔티티는 캐시하지 않는다.
- UNSPECIFIED : JPA 구현체가 정의한 설정을 따른다.
캐시 조회, 저장 방식 설정
캐시를 무시하고 데이터베이스를 직접 조회하거나 캐시를 갱신하려면 ‘캐시 조회 모드’와 ‘캐시 보관 모드’를 사용한다.
캐시 조회 모드
public enum CacheRetrieveMode {
USE,
BYPASS
}
- USE : (기본값) 캐시에서 조회한다.
- BYPASS : 캐시를 무시하고 데이터베이스에 접근한다.
캐시 보관 모드
public enum CacheStoreMode {
USE,
BYPASS,
REFRESH
}
- USE : (기본값) 조회한 데이터를 캐시에 저장한다. 조회한 데이터가 이미 캐시에 있으면 데이터를 최신 상태로 갱신하지 않는다. 트랜잭션을 커밋하면 등록 수정한 엔티티도 캐시에 저장한다.
- BYPASS : 캐시에 저장하지 않는다.
- REFERSH : USE 전략 + 데이터베이스에서 조회한 엔티티를 최신 상태로 다시 캐시한다.
캐시 모드는 아래 예제 처럼 엔티티 매니저(EntityManager) 단위로 설정하거나 EntityManager.find(), EntityManager.refresh() 에 설정할 수 있다. Query.setHint() 에도 사용할 수 있다.
em.setProperty("javax.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS);
em.setProperty("javax.persistence.cache.storeMode", CacheStoreMode.BYPASS);
Map<Stirng, 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);
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 인터페이스를 제공한다. (앞서 말한 JPA 가 표준화한 부분이다.)
앞서 언급했듯이 세부적인 기능은 구현체에 의존해야하기에, 구현체의 설명서를 읽어봐야한다.
public interface Cache {
// 캐시에 해당 엔티티가 있는지 확인한다.
public boolean contains(Calss 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);
}
하이버네이트와 EMCACHE 적용
하이버네이트가 지원하는 캐시는 크게 3가지가 있다.
- 엔티티 캐시 : 엔티티 단위로 캐시한다. 식별자로 엔티티를 조회하거나 컬렉션이 아닌 연관된 엔티티를 로딩할 때 사용한다.
- 컬렉션 캐시 : 엔티티와 연관된 컬렉션을 캐시한다. 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시한다. (하이버네이트 기능)
- 쿼리 캐시 : 쿼리와 파라미터 정보를 키로 사용해서 캐시한다. 결과가 엔티티면 식별자 값만 캐시한다. (하이버네이트 기능)
* JPA 표준에는 엔티티 캐시만 정의되어 있다.
하이버네이트의 EMCACHE 를 적용하기 위해서는 아래와 같은 추가적인 설정이 필요하다.
- gradle hibernate-emcache 라이브러리 추가
- emcache.xml 추가 : emcache 관련 설정
- persistence.xml 내용 추가 : 하이버네이트에 캐시 사용 정보 설정 (configuration 통해서도 가능할 것이다.)
아래는 엔티티에 2차 캐시를 적용했을 때의 예시이다.
@Cacheable // 2차 캐시를 적용하기 위해 JPA 표준 어노테이션을 사용한다.
@Cache(usage = CacheConcurrencyStrategy.READ_WRTIE) // 하이버네이트 전용 어노테이션이다. 캐시와 관련되어 세밀한 기능을 설정하기 위해 사용된다.
@Entity
public class Member {
...
}
하이버네이트 전용 어노테이션 @Cache
의 사용법은 다음과 같다.
- usage : CacheConcurrencyStrategy 를 사용해서 ‘캐시 동시성 전략’을 설정한다.
- region : 캐시 region 을 설정한다.
- include : 연관 객체를 캐시에 포함할지 설정한다. all, non-lazy 옵션을 선택할 수 있다. 기본값은 all 이다.
* 중요한 옵션은 usage
옵션이다.
org.hibernate.annotations.CacheConcurrencyStrategy
에 대해 살펴보면 아래와 같다.
- NONE : 캐시를 설정하지 않는다.
- READ_ONLY : 읽기 전용으로 설정한다. 등록, 삭제는 가능하지만 수정은 불가능하다. 읽기 전용인 불변 객체는 수정되지 않음이 보장되기에 하이버네이트는 2차 캐시에서 복사본이 아닌 원본 객체를 반환한다.
- NONSTRICT_READ_WRITE : (엄격하지 않은) 읽고 쓰기 전략이다. 동시에 같은 엔티티를 수정하면 데이터의 일관성이 깨질 수 있다. EMCACHE 는 데이터를 수정하면 캐시 데이터를 무효화한다.
- READ_WRITE : 읽고 쓰기가 가능하다. READ COMMITTED 정도의 격리 수준을 보장한다. EMCACHE 는 데이터를 수정하면 캐시 데이터도 같이 수정한다.
- TRANSACTIONAL : 컨테이너 관리 환경에서 사용할 수 있다. 설정에 따라 REPEATABLE READ 정도의 격리 수준을 보장받을 수 있다.
하이버네이트가 제공하는 캐시의 종류는 아래와 같다.
* ConcurrentHashMap 은 개발 시에만 사용하길 권장한다.
- ConcurrentHashMap
- EMCache
- Infinispan
캐시 영역
엔티티 캐시 영역은 기본값으로 ‘[패키지 명 + 클래스 명]’ 을 사용한다.
컬렉션 캐시 영역은 [엔티티 캐시 영역 + 컬렉션의 필드명] 으로 사용한다.
* 필요 시 캐시 영역을 직접 지정할 수 있다.
캐시 영역을 구분함으로써, 캐시 영역 별로 세부 설정이 가능하다. 예를 들어 TestEntity 엔티티
에 대한 캐시 설정을 따로 해줄 수 있다.
쿼리 캐시
쿼리와 파라미터 정보를 키로 사용해서 쿼리 결과를 캐시하는 방법이다.
쿼리 캐시를 사용하기 위해서는 hibernate.cache.use_query_cache
옵션을 꼭 true
로 설정해주어야 한다.
또, 쿼리 캐시를 적용하려는 쿼리마다 org.hibernate.cacheable
을 true
로 설정하는 힌트를 주어야 한다.
em.createQuery("select i from Item i", Item.class)
.setHint("org.hiberante.cacheable", true)
.getResultList();
@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 {
...
}
쿼리 캐시 영역
- org.hibernate.cache.internal.StandardQueryCache : 쿼리 캐시를 저장하는 영역이다. 쿼리, 쿼리 결과 집합, 쿼리를 실행한 시점의 타임스탬프를 보관한다.
- org.hibernate.cache.spi.UpdateTimestampsCache : 쿼리 캐시가 유효한지 확인하기 위해 쿼리 대상 테이블의 가장 최근 변경 시간을 저장하는 영역이다. 테이블 명과 테이블의 최근 변경된 타임스탬프를 보관한다.
쿼리 캐시는 캐시한 데이터 집합을 최신 데이터로 유지하기 위해, 쿼리 캐시를 실행하는 시간과 쿼리 캐시가 사용하는 테이블들이 가장 최근에 변경된 시간을 비교한다.
쿼리 캐시를 적용하고 난 후에 쿼리 캐시가 사용하는 테이블에 조금이라도 변경이 있으면 데이터베이스에서 데이터를 읽어와서 쿼리 결과를 다시 캐시한다.
예시
- StandardQueryCache 에서 쿼리의 타임스태프를 조회한다.
- UpdateTimestampsCache 에서 해당 테이블의 타임스탬프를 조회한다.
- 만약 StandardQueryCache 의 타임스탬프가 더 오래되었다면, 캐시가 유효하지 않은 것으로 보고 데이터베이스에서 데이터를 조회하고 다시 캐시한다.
쿼리 캐시를 잘 활용하면 극적인 성능 향상이 있다. 하지만 빈번하게 변경이 있는 테이블에 사용하면 오히려 성능이 저하된다.
따라서 수정이 거의 일어나지 않는 테이블에 사용해야 효과를 볼 수 있다.
* UpdateTimestapmsCache 캐시 영역은 만료되지 않도록 설정해주어야 한다. 해당 영역이 만료되면 모든 쿼리 캐시가 무효화된다. EMCACHE 의 eternal="true"
옵션을 사용하면 캐시에서 삭제되지 않는다.
쿼리 캐시와 컬렉션 캐시의 주의점
엔티티 캐시를 사용해서 엔티티를 캐시하면 엔티티 정보 모두를 캐시한다.
하지만 쿼리 캐시와 컬렉션 캐시는 결과 집합의 식별자 값만 캐시한다. 따라서 쿼리 캐시와 컬렉션 캐시를 조회하면 그 안에는 식별자 값만 들어있다. 이 식별자 값을 하나씩 엔티티 캐시에서 조회하여 실제 엔티티를 찾는 것이다.
문제는 쿼리 캐시나 컬렉션 캐시만 사용하고 대상 엔티티에 엔티티 캐시를 적용하지 않으면 성능상 심각한 문제가 발생할 수 있다.
예시는 다음과 같다.
select m from Member m
쿼리를 실행하였는데, 쿼리 캐시가 적용되어 있고 결과 집합은 100건이라고 하자.- 결과 집합에는 식별자만 있으므로 한 건씩 엔티티 캐시 영역에서 조회한다.
- Member 엔티티는 엔티티 캐시를 사용하지 않으므로 한 건씩 DB에서 조회한다.
- 결국 100건의 SQL 이 실행된다.
쿼리 캐시나 컬렉션 캐시만 사용하고 엔티티 캐시를 사용하면 최악의 상황이 발생할 수 있다. 따라서 쿼리 캐시나 컬렉션 캐시를 사용하면 (대상) 엔티티에는 꼭 엔티티 캐시를 적용해야 한다.