Close
Close full mode
logo만렙 개발자 키우기

(2) 2차 캐시

Git RepositoryEdit on Github
Last update: 9 months ago by nowwaterReading time: 6 min

16.2.1 1차 캐시와 2차 캐시

네트워크를 통해 데이터베이스에 접근하는 시간 비용 >>>>> 애플리케이션 서버에서 내부 메모리에 접근하는 시간 비용
=> 수만~수십만 배 이상 차이

따라서 조회한 데이터를 메모리에 캐시해서 데이터베이스 접근 횟수를 줄이면 애플리케이션 성능을 획기적으로 개선 가능


1차 캐시

영속성 컨텍스트 내부에 엔티티를 보관하는 저장소

  • 일반적인 웹 애플리케이션 환경은 트랜잭션 시작 ~ 종료 까지만 유효하다. 영속성 컨텍스트 내부에 있다.

    • JPA를 J2EE나 스프링 프레임워크 같은 컨테이너 위에서 실행하면 트랜잭션과 영속성 컨텍스트의 생명주기가 동일함
  • OSIV를 사용해도 클라이언트의 요청이 들어올 때부터 끝날 때까지만 유효하다.

    • 애플리케이션 전체로 보면 데이터베이스 접근 횟수를 획기적으로 줄이지는 못한다.
  • 엔티티 매니저로 조회하거나 변경하는 모든 엔티티는 1차 캐시에 저장된다.

    • 트랜잭션 커밋/플러시 호출 시 1차 캐시에 있는 엔티티의 변경 내역을 데이터베이스에 동기화
  • 끄고 켤 수 있는 옵션이 아니다. 영속성 컨텍스트 자체가 1차 캐시

image

특징

  • 1차 캐시는 같은 엔티티가 있으면 해당 엔티티를 그대로 반환한다. 따라서 1차 캐시는 객체 동일성(a == b)을 보장한다.
  • 1차 캐시는 기본적으로 영속성 컨텍스트 범위의 캐시다

    • 컨테이너 환경에서는 트랜잭션 범위의 캐시, OSIV를 적용하면 요청 범위의 캐시

2차 캐시

하이버네이트를 포함한 대부분의 JPA 구현체들이 지원하는 애플리케이션 범위의 캐시 - 공유 캐시라고도 한다.

=> 애플리케이션 조회 성능 향상 가능

image

  • 애플리케이션 범위의 캐시로 애플리케이션을 종료할 때까지 캐시가 유지된다.
  • 분산 캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수 있다.
  • 엔티티 매니저를 통해 데이터를 조회할 때 우선 2차 캐시에서 찾고, 없으면 데이터베이스에서 찾는다.

    • 적절히 활용하면 데이터베이스 조회 횟수를 획기적으로 줄일 수 있다.

image

동시성을 극대화하기 위해 캐시한 객체의 복사본을 만들어서 반환한다.

만약 캐시한 원본 객체를 반환하면 동시에 여러 곳에서 수정해야할 경우 락을 걸어야해서 동시성이 떨어질 수 있다.

락에 비하면 객체를 복사하는 비용이 더 저렴하다.

특징

  • 2차 캐시는 영속성 유닛 범위의 캐시다.
  • 2차 캐시는 조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어서 반환한다.
  • 2차 캐시는 데이터베이스 기본 키를 기준으로 캐시하지만, 영속성 컨텍스트가 다르면 객체 동일성(a == b)을 보장하지 않는다.

16.2.2 JPA 2차 캐시 기능

JPA 2.0 부터 여러 구현체가 공통으로 사용하는 부분에 대해 2차 캐시 표준을 정의, 세밀한 설정을 하려면 구현체에 의존적인 기능을 사용해야 한다.

캐시 모드 설정

  • @Cacheable(true) - 기본값

  • @Cacheable(false)

@Cacheable
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
...
}

애플리케이션 전체(영속성 유닛 단위)에 캐시를 적용하는 옵션 설정

<persisternce-unit name="test">
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
</persistence-unit>

SharedCacheMode 캐시 모드 설정

보통은 EMABLE_SELECTIVE 를 사용한다.

캐시 모드설명
ALL모든 엔티티를 캐시한다.
NONE캐시를 사용하지 않는다.
ENABLE_SELECTIVECacheable(true)로 설정된 엔티티만 캐시를 적용
DISABLE_SELECTIVE모든 엔티티를 캐시하는데 Cacheable(false)로 명시된 엔티티는 캐시하지 않는다.
UNSPECIFIEDJPA 구현체가 정의한 설정을 따른다.

캐시 조회, 저장 방식 설정

캐시를 무시하고 데이터베이스를 직접 조회하거나 캐시를 갱신하려면 캐시 조회 모드캐시 보관 모드를 사용한다.

// 엔티티 매니저 범위 적용
em.setProperty("javax.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS);
// 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();

캐시 조회 모드

  • 프로퍼티 이름 : javax.persistence.cache.retrieveMode
  • 설정 옵션 : javax.persistence.CacheRetrieveMode
public enum CacheRetrieveMode {
USE, BYPASS
}
  • USE : 캐시에서 조회한다. (기본값)

  • BYPASS : 캐시를 무시하고 데이터베이스에 직접 접근한다.


캐시 보관 모드

  • 프로퍼티 이름 : javax.persistence.cache.storeMode
  • 설정 옵션 : javax.persistence.CacheStoreMode
public enum CacheStoreMode {
USE, BYPASS, REFRESH
}
  • USE : 조회한 데이터를 캐시에 저장. (기본값)

    • 조회한 데이터가 이미 캐시에 있으면 캐시 데이터를 최신 상태로 갱신하지는 않는다.

    • 트랜잭션을 커밋하면 등록/수정한 엔티티도 캐시에 저장한다.

  • BYPASS : 캐시에 저장하지 않는다.
  • REFRESH : USE 전략에 추가로 데이터베이스에서 조회한 엔티티를 최신 상태로 다시 캐시한다.

JPA 캐시 관리 API

JPA는 캐시를관리하기 위한 javax.persistence.Cache 인터페이스를 제공

// Cache 관리 객체 조회
Cache cache = emf.getCache();
boolean contains = cache.contains(TestEntity.class, testEntity.getId());
System.out.println("contains = " + contains);

Cache 인터페이스

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);
}

16.2.3 하이버네이트와 EHCACHE 적용

하이버네이트가 지원하는 캐시는 크게 3가지가 있다.

1. 엔티티 캐시

  • 엔티티 단위로 캐시한다.

  • 식별자로 엔티티를 조회하거나 컬렉션이 아닌 연관된 엔티티를 로딩할 때 사용.

2. 컬렉션 캐시

  • 엔티티와 연관된 컬렉션을 캐시한다.

  • 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시

3. 쿼리 캐시

  • 쿼리와 파라미터 정보를 키로 사용해서 캐시한다.

  • 결과가 엔티티면 식별자 값만 캐시

JPA 표준엔 엔티티 캐시만 정의되어 있다.

환경설정

hibernate-ehcache 라이브러리를 추가해야 한다.

엔티티 캐시와 컬렉션 캐시

@Cacheable // 엔티티 캐시 적용
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // 하이버네이트 전용. 캐시와 관련된 더 세밀한 설정 사용
@Entity
public class ParentMember {
@Id @GeneratedValue
private Long id;
private String name;
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // 컬렉션 캐시 적용
@OneToMany(mappedBy = "parentMember", cascade = CascadeType.NULL)
private List<ChildMember> childMembers = new ArrayList<ChildMember>();
}

@Cache

속성설명
usageCacheConcurrencyStrategy 를 사용해서 캐시 동시성 전략을 설정
region캐시 지역 설정
include연관 객체를 캐시에 포함할지 선택. all, non-lazy 옵션을 선택할 수 있다. 기본값은 all

중요한 것은 캐시 동시성 전략을 설정할 수 있는 usage 속성.

CacheConcurrencyStrategy 속성

속성설명
NONE캐시 설정 X
READ_ONLY읽기 전용 설정. 등록/삭제는 가능하지만 수정은 불가능
읽기 전용인 불변 객체는 수정되지 않으므로 하이버네이트는 2차 캐시 조회 시 객체를 복사하지 않고 원본 객체를 반환
NONSTRICT_READ_WRITE엄격하지 않은 읽고 쓰기 전략. 동시에 같은 엔티티 수정 시 데이터 일관성이 깨질 수 있다. <br? EHCACHE는 데이터 수정 시 캐시 데이터를 무효화함
READ_WRITE읽기 쓰기가 가능하고, READ COMMITTED 정도의 격리 수준을 보장.
EHCACHE는 데이터 수정 시 캐시 데이터도 같이 수정
TRANSACTIONAL컨테이너 관리 환경에서 사용 가능. 설정에 따라 REPEATABLE READ 정도의 격리 수준을 보장

캐시 영역

위에서 캐시를 적용한 코드는 다음 캐시 영역에 저장된다.

  • 엔티티 캐시 영역 : ParentMember

    • 엔티티 캐시 영역은 기본값으로 [패키지 명 + 클래스 명] 을 사용
  • 컬렉션 캐시 영역 : childMembers

    • 컬렉션 캐시 영역은 엔티티 캐시 영역 이름에 캐시한 컬렉션의 필드 명이 추가된다.

    • 필요하다면 @Cache(region = "customRegion", ...) 처럼 region 속성을 사용해서 캐시 영역을 직접 지정 가능


쿼리 캐시

쿼리와 파라미터 정보를 키로 사용해서 쿼리 결과를 캐시하는 방법

쿼리 캐시를 적용하려면 쿼리 캐시를 적용하려는 쿼리마다 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 {
...
}

쿼리 캐시 영역

쿼리 캐시를 활성화하면 두 캐시 영역이 추가된다.

1. org.hibernate.cache.internal.StandardQueryCache

  • 쿼리 캐시를 저장하는 영역

  • 이곳에는 쿼리, 쿼리 결과 집합, 쿼리를 실행한 시점의 타임스탬프를 보관

2. org.hibernate.cache.spi.UpdateTimestampsCache

  • 쿼리 캐시가 유효한지 확인하기 위해 쿼리 대상 테이블의 가장 최근 변경(등록, 수정, 삭제) 시간을 저장하는 영역

  • 테이블 명과 해당 테이블의 최근 변경된 타임스탬프를 보관

  • 해당 엔티티가 매핑한 테이블 이름으로 타임스탬프를 갱신

쿼리 캐시는 캐시한 데이터 집합을 최신 데이터로 유지하기 위해, 쿼리 캐시를 실행하는 시간과 쿼리 캐시가 사용하는 테이블들이 가장 최근에 변경된 시간을 비교

쿼리 캐시 적용 후 쿼리 캐시가 사용하는 테이블에 조금이라도 변경이 있으면 데이터베이스에서 데이터를 읽어와서 쿼리 결과를 다시 캐시

// 쿼리 캐시 사용
public List<ParentMember> findParentMembers(){
return em.createQuery("select p from ParentMember p join p.childMembers c", ParentMember.class)
.setHint("org.hibernate.cacheable", true)
.getResultList();
}
  • 쿼리 실행 시 우선 StandardQueryCache 캐시 영역에서 타임스탬프를 조회
  • 쿼리가 사용하는 PARENTMEMBER, CHILDMEMBERUpdateTimestampsCache 캐시 영역에서 조회해서 테이블들의 타임스탬프를 확인
  • 만약 StandardQueryCache 캐시 영역의 타임스탬프가 더 오래되었으면 캐시가 유효하지 않은 것으로 보고 데이터베이스에서 데이터를 조회해서 다시 캐시

쿼리 캐시를 잘 활용하면 극적인 성능 향상이 있지만, 빈번히 변경되는 테이블에 사용하면 오히려 성능이 저하된다.

=> 수정이 거의 일어나지 않는 테이블에 사용해야 효과적

쿼리 캐시와 컬렉션 캐시의 주의점

엔티티 캐시는 엔티티 정보를 모두 캐시

쿼리 캐시와 컬렉션 캐시는 결과 집합의 식별자 값만 캐시. 식별자 값을 조회해서 엔티티 캐시에서 조회하여 실제 엔티티를 찾는다.


문제는 쿼리 캐시나 컬렉션 캐시만 사용하고 대상 엔티티에 엔티티 캐시를 적용하지 않으면 성능상 심각한 문제가 발생할 수 있다.

문제 상황

  1. select m from Member m 쿼리를 실행했는데, 쿼리 캐시가 적용되어 있고 결과 집합은 100건이다.
  1. 결과 집합에는 식별자만 있으므로 한 건씩 엔티티 캐시 영역에서 조회한다.
  1. Member 엔티티는 엔티티 캐시를 사용하지 않으므로 한 건씩 데이터베이스에서 조회한다.
  1. 결국 100건의 SQL이 실행된다.

쿼리 캐시, 컬렉션 캐시만 사용 + 엔티티 캐시 사용 X => 최악의 상황에 결과 집합 수만큼 SQL이 실행

따라서 쿼리 캐시나 컬렉션 캐시를 사용하려면 결과 대상 엔티티에는 꼭 엔티티 캐시를 적용해야 한다.

정리

  • 트랜잭션의 격리 수준은 4 단계가 있다. 격리 수준이 낮을수록 동시성은 증가하지만 격리 수준에 따른 다양한 문제가 발생한다.
  • 영속성 컨텍스트는 데이터베이스 트랜잭션이 READ COMMITTED 격리 수준이어도 애플리케이션 레벨에서 반복 가능한 읽기(REPEATABLE READ)를 제공한다.
  • JPA는 낙관적 락비관적 락을 지원한다.

    • 낙관적 락은 애플리케이션이 지원하느 락

    • 비관적 락은 데이터베이스 트랜잭션 락 메커니즘에 의존

  • 2차 캐시를 사용하면 애플리케이션의 조회 성능을 극적으로 끌어올릴 수 있다.
🚀 JPA — Previous
(1) 트랜잭션과 락
Next — 🍃 Spring Boot
스프링 개념 정리