Ehcache의 주요 특징
- 효율적인 캐싱: 메모리와 디스크를 사용한 하이브리드 캐싱을 지원하여, 대용량 데이터의 캐싱도 효율적으로 처리할 수 있습니다.
- 메모리 캐시: 자주 사용되는 데이터를 메모리에 저장하여 빠른 응답 시간을 제공합니다. 메모리 캐시는 가장 빠른 캐싱 옵션입니다.
- 디스크 캐시: 메모리 캐시의 데이터를 디스크에 저장하여, 메모리 용량 초과 시에도 데이터를 유지할 수 있습니다. 디스크 캐시는 메모리보다 느리지만 더 많은 데이터를 저장할 수 있습니다.
- 분산 캐싱: 클러스터링과 분산 캐싱을 지원하여, 여러 애플리케이션 인스턴스 간의 캐시 일관성을 유지할 수 있습니다.
- Ehcache는 여러 노드 간에 캐시 데이터를 공유하고 동기화할 수 있는 클러스터링 기능을 제공합니다. 이를 통해 분산 환경에서 데이터 일관성을 유지할 수 있습니다.
- Terracotta와 통합하여 확장 가능한 분산 캐시 클러스터를 구축할 수 있습니다.
- Ehcache + Terracotta의 장점:
- 분산 캐시 처리: 여러 서버에서 동일한 캐시 데이터를 공유하고 동기화할 수 있습니다.
- 확장성: 여러 서버에 걸쳐 캐시를 분산 처리하므로, 대규모 애플리케이션에서도 효율적으로 캐시를 관리할 수 있습니다.
- 고가용성: Terracotta는 분산 환경에서 장애 복구 기능을 제공하여 데이터의 고가용성을 보장합니다.
- 데이터 일관성: 동기화 또는 비동기화 모드를 통해 캐시 데이터의 일관성을 유지할 수 있습니다.
- 플러그인 방식: Spring Framework, Hibernate, JPA 등과 쉽게 통합되어, 개발자가 캐싱을 손쉽게 구현할 수 있습니다.
- 캐시 구성: 다양한 캐시 정책(예: LRU, LFU, FIFO)을 통해 캐시의 만료, 용량 제어, 데이터 일관성 관리가 가능합니다.
- TTL(Time To Live): 캐시 항목의 유효 시간을 설정하여, 지정된 시간이 지나면 캐시에서 자동으로 제거됩니다.
- TTI(Time To Idle): 캐시 항목이 사용되지 않은 시간 기준으로 만료되는 설정입니다. 일정 시간 동안 접근되지 않은 캐시 항목은 제거됩니다.
- LRU(Least Recently Used): 최근에 사용되지 않은 항목을 먼저 제거하여 캐시의 크기를 관리하는 정책입니다.
- LFU(Least Frequently Used): 사용 빈도가 적은 항목을 먼저 제거하여 캐시의 크기를 관리합니다.
- 기본 및 확장 기능: 기본적인 캐시 기능 외에도, 캐시 이벤트 청취자, 트랜잭션 캐시, 캐시 복제 등 다양한 기능을 제공합니다.
- JCache 표준 지원 & Spring과의 통합:
- Ehcache는 Spring 프레임워크와의 통합을 지원하며, Spring의 캐시 추상화를 통해 쉽게 설정하고 사용할 수 있습니다.
- 애노테이션 기반 캐시 관리(@Cacheable, @CacheEvict 등)를 지원하여 개발 생산성을 높일 수 있습니다.
- 3.1 캐시 일관성 전략
- Write-through 캐시
- 개념: 애플리케이션이 데이터베이스에 데이터를 쓰는 동시에 캐시에 데이터를 갱신하는 방식입니다. 쓰기 작업이 캐시와 데이터베이스에 동시 반영됩니다.
- 장점: 데이터베이스와 캐시 간의 일관성이 보장됩니다.
- 단점: 쓰기 작업의 지연 시간이 증가할 수 있습니다.
- Write-behind 캐시
- 개념: 애플리케이션이 데이터를 캐시에 먼저 쓰고, 비동기적으로 데이터베이스에 반영하는 방식입니다. 캐시에 데이터를 저장한 후, 일정 시간 후에 데이터베이스에 기록됩니다.
- 장점: 쓰기 작업의 성능이 향상됩니다.
- 단점: 캐시와 데이터베이스 간의 일관성 문제가 발생할 수 있으며, 데이터 유실 가능성도 있습니다.
- Cache-aside 패턴 / 읽을때
- 개념: 애플리케이션이 먼저 캐시에서 데이터를 조회하고, 캐시된 데이터가 없으면 데이터베이스에서 데이터를 읽은 후 캐시에 저장하는 방식입니다.
- 장점: 읽기 작업 성능이 매우 높으며, 캐시된 데이터는 자주 읽히는 데이터로 효율적으로 관리됩니다.
- 단점: 캐시와 데이터베이스 간 일관성을 보장하지 않으므로, 캐시 무효화 전략이 필요합니다.
- TTL(Time To Live) 기반 캐시
- 개념: 캐시에 저장된 데이터에 **유효 기간(Time To Live, TTL)**을 설정하여, 데이터가 일정 시간이 지나면 자동으로 무효화됩니다.
- 장점: 오래된 데이터가 자동으로 갱신되므로 일관성을 유지할 수 있습니다.
- 단점: TTL을 적절하게 설정하지 않으면, 캐시 히트율이 떨어지거나 불필요한 갱신이 발생할 수 있습니다.
- 캐시 무효화(Cache Invalidation)
- 개념: 데이터베이스에서 데이터가 변경되면 해당 데이터를 캐시에서 무효화하여, 다음 읽기 작업 시 새로운 데이터를 캐시에 다시 로드하는 방식입니다.
- 장점: 데이터베이스와 캐시 간의 일관성을 보장합니다.
- 단점: 데이터베이스와 캐시 간 동기화 지연으로 인해 일시적인 일관성 문제가 발생할 수 있습니다.
- Write-through 캐시
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'javax.cache:cache-api'
implementation 'org.ehcache:ehcache:3.6.2'
application.properties
# 캐시 구성 파일 위치 설정
spring.cache.jcache.config=classpath:ehcache.xml
spring.cache.type=ehcache
ehcache.xml(자바로도 가능)
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
updateCheck="false">
<!-- 기본 캐시 설정 -->
<defaultCache
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="false"/>
<!-- 사용자 정의 캐시 설정 -->
<cache name="exampleCache"
maxEntriesLocalHeap="1000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
diskSpoolBufferSizeMB="20"
maxEntriesLocalDisk="10000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
캐시 구성 세부 사항
- 캐시 만료 정책
- timeToIdleSeconds: 캐시에 저장된 데이터가 얼마나 오랫동안 사용되지 않았을 때 만료될지 설정합니다.
- timeToLiveSeconds: 캐시에 저장된 데이터가 얼마나 오랫동안 존재할 수 있는지 설정합니다.
- eternal: 데이터가 영원히 만료되지 않도록 설정합니다. true일 경우 만료 시간 설정이 무시됩니다.
- 캐시 용량 관리
- maxEntriesLocalHeap: 캐시에서 허용할 최대 엔트리 수를 설정합니다.
- overflowToDisk: 메모리 캐시가 가득 차면 디스크로 데이터를 내보낼지 설정합니다.
- maxEntriesLocalDisk: 디스크 캐시에 허용할 최대 엔트리 수를 설정합니다.
- 데이터 유지 정책
- memoryStoreEvictionPolicy: 메모리 캐시에서 데이터를 제거할 정책을 설정합니다. LRU (Least Recently Used), LFU (Least Frequently Used), FIFO (First In, First Out) 등이 있습니다.
위 의존성/설정을 추가하고 프로젝트에 @EnableCaching 어노테이션을 추가한다.
spring-boot를 사용하면 @EnableCaching 어노테이션만으로도 ConcurrentMapCacheManager를 기본으로 등록해주기 때문에 따로 등록할 것은 없지만 커스텀을 할 경우 아래와 같이 빈을 등록하여 override 해야 한다.
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("directory"),
new ConcurrentMapCache("addresses")));
return cacheManager;
}
}
참고) 캐시 매니저 종류
- ConcurrentMapCacheManager: Java의 ConcurrentHashMap을 사용해 구현한 캐시를 사용하는 캐시 매니저
- SimpleCacheManager: 기본적으로 제공하는 캐시가 없어 사용할 캐시를 직접 등록하여 사용하기 위한 캐시 매니저
- EhCacheCacheManager: 자바에서 유명한 캐시 프레임워크 중 하나인 EhCache를 지원하는 캐시 매니저
- CompositeCacheManager: 1개 이상의 캐시 매니저를 사용하도록 지원해주는 혼합 캐시 매니저
- CaffeineCacheManager: Java 8로 Guava 캐시를 재작성한 Caffeine 캐시를 사용하는 캐시 매니저
- JCacheCacheManager: JSR-107 기반의 캐시를 사용하는 캐시 매니저
ConcurrentMapCache 생성자를 통해서 캐시 이름과 null값을 캐싱할지 여부, 사용할 concurrentMap을 지정할 수 있다. 참고로 ConcurrentMapCache는 만료시간에 의한 자동 만료 기능이 없기 때문에, 수동으로 체크 혹은 @Scheduled를 활용하여 구현해야 한다.
참고로 캐시 흐름을 로그로 보고 싶다면 별도 이벤트 리스너를 구현해야한다.
@Slf4j
public class CacheEventConfig implements CacheEventListener<Object, Object> {
@Override
public void onEvent(CacheEvent<?, ?> event) {
log.info(">>>[Caching event] working {} :: {}", event.getType(), event.getKey());
}
}
캐시 조회/저장 @Cacheable
캐시 해야 하는 데이터가 return 되는 메서드 위에 설정한다.
@Cacheable("addresses")
public String getAddress(Customer customer) {...}
getAddress가 호출될 때 실제 로직이 시행되기 전 input parameter(key = customer)에 해당하는 캐시를 먼저 확인하고, 없으면 실제 로직을 실행하고 캐싱을 한다. 위 예시는 Customer클래스가 key 값이므로(별도의 key값을 설정하지 않았으므로) hashcode와 equals 메서드를 오버라이드 해 어떤 데이터일 때 같다고 판별할지 명백히 해야 한다. 아래 별도의 링크를 통해 키 판별에 대해 확인하자.
@Cacheable({"addresses", "directory"})
public String getAddress(Customer customer) {...}
이렇게 여러 개의 캐시를 줄 수도 있다. 이때 둘 중 하나라도 값이 있으면 실제 로직을 실행하지 않는다.
@Cacheable(value = "bestSeller", key = "#book.bookNo")
public Book getBestSeller(Book book, User user, Date dateTime) {...}
Key값의 지정에는 SpEL이 사용된다. 그렇기 때문에 만약 파라미터가 객체라면 위와 같이 하위 속성에 접근할 수 있다.
@Cacheable(value = "bestSeller", key = "#book.bookNo", condition = "#user.type == 'ADMIN'")
public Book getBestSeller(Book book, User user, Date dateTime) {...}
만약 파라미터 값이 특정 조건인 경우에만 캐시를 적용하기를 원한다면 condition을 이용하면 된다.
캐시 삭제 @CacheEvict
@CacheEvict(value = "bestSeller")
public void clearBestSeller() {...}
@CacheEvict(value="directory", key=customer.name)
public String getAddress(Customer customer) {...}
기본적으로 메서드의 키에 해당하는 캐시만 제거한다. 위 예시는 이름이 같은 캐시만 제거할 것이다.
@Caching(evict = {
@CacheEvict("addresses"),
@CacheEvict(value="directory", key="#customer.name") })
public String getAddress(Customer customer) {...}
여러 이름의 캐시를 지울 때는 위와 같이 복수개 설정이 가능하다.
@CacheEvict(value="addresses", allEntries=true)
public String getAddress(Customer customer) {...}
key와 상관없이 모든 객체를 지우려면 allEntries = true로 지정하면 된다.
캐시 수정 @CachePut
캐시 값이 바뀌었다면 바로 수정해주어야 한다. @CachePut은 @Cacheable과 유사하게 실행 결과를 캐시에 저장하지만, 캐시의 저장 유무와 상관없이 항상 메서드의 로직을 실행한다는 점에서 다르다.
@CachePut(value="addresses")
public String getAddress(Customer customer) {...}
@CacheConfig
하나의 캐시에 대해 일괄 관리를 하고자 하면 아래와 같이 클래스 레벨에서 처리할 수도 있다. 이때 이름을 중복으로 적을 필요 없이 클래스 선언으로 해결된다.
@CacheConfig(cacheNames={"addresses"})
public class CustomerDataService {
@Cacheable
public String getAddress(Customer customer) {...}
}
조건절
위에서 잠깐 나왔지만 spEL을 활용하여 condition / unless의 조건절을 줄 수 있다.
@CachePut(value="addresses", condition="#customer.name=='Tom'")
public String getAddress(Customer customer) {...}
@CachePut(value="addresses", unless="#result.length()<64")
public String getAddress(Customer customer) {...}
캐시 key 설정에 대한 내용
https://jistol.github.io/spring/2017/02/09/springboot-cache-key/
https://sunghs.tistory.com/132
'개발 > spring' 카테고리의 다른 글
[swagger] Illegal DefaultValue null for parameter type integer (0) | 2022.06.03 |
---|---|
[scheduled] @Schueduled와 캐시 (0) | 2022.05.30 |
[spring-jpa] 부모-자식 트랜젝션 관계(propagation) (0) | 2022.05.27 |
[db connection] mysql driver (0) | 2022.05.09 |
[spring-jpa] native query 삽질로그 (0) | 2022.05.06 |