사이드 프로젝트에 캐시를 적용하며 이것저것 알아보는 시리즈
1. [Spring] 캐시란? + Spring Boot 내장 캐시 알아보기
2. [Cache/Spring] EhCache vs Cache2k vs Caffeine 캐시 비교하기
3. [Cache/Spring] 내 어플리케이션의 캐시 히트율 알아보기
개요
포스팅을 작성하는 24년 기준, Spring Boot에서 공식적으로 Auto-configuration을 지원하는 캐시 라이브러리는 Ehcache, Cache2k, Caffeine 3가지가 있다. 캐시 그게 그거 아니야? 싶지만 각 라이브러리별 지원하는 기능과 관리법이 다르니 각각을 간단하게 비교해보자.
용어 정리
용어 | 의미 |
로컬 캐시, 글로벌 캐시 | 이전 포스팅 참고 |
Eviction | 데이터 공간(메모리) 확보를 위해 안 쓰는 데이터를 지우는 것. size base |
Expiration | 데이터에 유통 기한을 적용하는 것. time base |
JSR (Java Specification Requests) | 자바 플랫폼에 대한 규격을 제안 또는 기술한 것 |
JSR-107 (JCache) | - JSR 중 Java 표준 캐시 (JCache)를 정의한 사양. - 객체 생성, 접근성 공유, 스풀링, 무효화, JVM 전반에 대한 일관성 등을 포함한 Java 객체 인메모리 캐싱을 구현하는 API와 코드를 제공한다. - 표준 사양이기 때문에 javax.cache API를 기반으로 코드를 짜면 캐시와 관련된 어플리케이션 코드를 수정하지 않고도 JCache 구현체를 쉽게 교체할 수 있다. |
SoR (System of Record) | 데이터를 신뢰할 수 있는 권위있는 소스. 파일 시스템이나 long-term-storage 같은 전통적인 DB를 일컫는다. |
Thundering-herd | - 다수의 프로세스 (혹은 스레드)가 동시에 같은 리소스에 접근하려 할 때 발생하는 문제. - 캐시 미스가 발생하거나 캐시가 만료되었을 때, 수많은 요청이 갑작스레 원본 저장소 (DB)에 몰릴 경우 일시적으로 캐시와 DB 모두에 과부하가 발생할 수 있다. - Line에서 Thundering-herd를 해결한 사례 |
EhCache
✔ 공식 홈페이지 : https://www.ehcache.org/
✔ github repo : https://github.com/ehcache
오픈소스 로컬 캐시 라이브러리. 공식 문서 소개에 따르면 자바에서 제일 널리 알려진 캐시 라이브러리라고 한다. 포스팅 작성일(24.08.12) 기준 3.10버전이 제일 최신 버전이고, Java8 이상부터 동작한다.
✅ 특징
다른 캐시 라이브러리에는 없는 다양한 특징이 있다. 하나씩 알아보자.
1️⃣ JCache (javax.cache API) 지원
3버전부터 자바 표준 캐시인 JCache와의 호환성을 제공한다. EhCache를 Spring에서 사용하는 방법은 1) xml을 이용하는 방법과 2) 코드를 이용하는 방법 2가지가 있다. JCache와 호환되도록 설정하고 싶다면 JCache 사양에 기재된 인터페이스 : Cache, CachingProvider, CacheManager 등을 활용해 코드로 캐시를 설정해주거나, 혹은 xml 설정 파일에 jsr107 서비스를 추가하면 된다.
- 코드 기반으로 EhCache3 & JCache 사용하는 방법 : [ 공식문서 LINK ]
- xml 기반으로 EhCache3 & JCache 사용하는 방법 : [ 공식문서 LINK ]
단, EhCache3와 JCache를 통한 EhCache3의 동작은 구성 메커니즘에 따라 조금씩 달라질 수 있다고 한다. 달라질 수 있는 설정은 아래 더보기 참고!
by-reference vs by-value
- JCache MutableConfiguration을 사용해 EhCache를 구성할 경우, setStoreByValue(true)가 디폴트로 사용된다.
- 따라서 직렬화 가능한 key-value로 사용이 제한된다.
- heap-only tier를 이용할 경우, 직렬화기와 copier를 따로 설정하지 않는다면 기본적으로 by-reference로 동작한다.
- 그 외 tier를 이용할 경우, 직렬화기에 의해 기본적으로 by-value로 동작한다.
Cache-through와 Compare-and-Swap 동작
- compare-and-swap 동작을 위한 캐시 로더의 역할이 다르다.
- JCache를 통한 EhCache3의 경우, putIfAbsent(K, V) 연산을 할 때 캐시에 매핑이 없다면 캐시 로더가 동작하지 않는다.
- 즉, 캐시에 key에 해당하는 값이 없어도 원본 저장소 (DB 등)에서 가져오지 않는다.
- 따라서 DB에는 key에 해당하는 값이 있는데, 무시하고 put (=update) 하기 때문에 블라인드 업데이트가 발생해 원본 데이터를 유실할 수도 있다.
- Native EhCache3의 경우, putIfAbsent(K, V) 연산을 할 때 항상 CacheLoaderWriter가 동작한다.
- 즉, 캐시에 key에 해당하는 값이 없으면 원본 저장소에서 값을 가져온다.
- 만약 원본 저장소에도 값이 없으면 그때서야 put (=insert)한다.
2️⃣ Storage Tier
3버전부터 off-heap 저장 공간을 추가로 제공하면서, GC에 의해 캐시가 정리되지 않도록 설정할 수 있게 되었다.
계층 | 의미 |
On-Heap Store | - RAM 메모리를 이용해 캐시 항목을 저장한다. Java app과 동일한 힙 메모리를 사용하므로 GC 스캔 대상이다. - JVM이 사용하는 힙 공간이 많을 수록 GC STW로 인한 어플리케이션 성능 저하가 크므로, 속도는 제일 빠르지만 가장 제한된 스토리지 리소스이다. |
Off-Heap Store | - 사용 가능한 RAM에 의해 크기가 제한된다. GC 영향을 받지 않는다. - 저장된 데이터에 액세스하기 위해 JVM heap 공간으로 저장된 데이터들을 옮겨야 하기에 on-heap보단 느리다. |
Disk Store | - 디스크 (파일 시스템)을 이용해 캐시 항목을 저장한다. - 저장 공간은 풍부하지만 RAM 기반 계층보단 훨씬 느리다. 처리량이 많은 서비스에는 적합하지 않다. |
Clustered Store | - 원격 서버에 캐시를 저장한다. 즉 로컬 캐시가 아닌 글로벌 캐시. - 네트워크 지연 시간, 클라이언트/서버 일관성 유지 등으로 성능 저하가 발생하므로 로컬 스토리지보다 느리다. |
3️⃣ Cache Usage Pattern
캐시를 사용할 때 쓸 수 있는 몇 가지 access 패턴이 존재한다.
- Cache aside : 어플리케이션 코드에서 캐시를 직접 사용하는 방법
- Cache as SoR : 캐시를 Primary SoR 처럼 사용하는 방법으로, SoR reading과 writing을 캐시에 위임한다.
- Read through : 요청받은 KV가 캐시에 없으면 SoR에서 조회한 후 캐시에 저장한다.
- Write through : KV를 캐싱하도록 요청받으면 cache를 갱신한 후, SoR에 value를 저장한다.
- Write behind : 요청이 들어왔을 때 바로 SoR에 Write하지 않고, 지연시간을 두어 유저 스레드가 더 빨리 움직이게 한다.
Cache as SoR 패턴은 CacheLoaderWriter를 설정하고 캐시 설정 파일(코드 베이스 or xml)에 등록하여 사용할 수 있다.
Cache as SoR 패턴을 이용할 경우, 어플리케이션 코드가 SoR 관련 작업을 몰라도 되므로 전체 코드 복잡도가 감소한다. 또한 캐시별로 write-through, write-behind 전략을 다르게 가져갈 수 있고, Thundering-herd 문제를 해결할 수 있다. 단 코드 레벨에서 캐시 access를 직접 확인할 수 없어 가독성이 조금 떨어질 수 있으니 참고하자.
✅ Cache Expiration 정책
Java 코드 레벨, 또는 XML 레벨에서 아래 세 가지의 expiration 정책을 제공한다.
옵션 | 의미 |
no expiry | 캐시에 만료 설정을 하지 않는다 |
TTL (time-to-live) |
캐시가 마지막으로 put(= write) 된 후 일정 시간이 지나면 자동으로 만료한다. |
TTI (time-to-idle) |
캐시가 마지막으로 retrieve(= read = 참조) 된 후 일정 시간이 지나면 자동으로 만료된다. |
코드 레벨에서 expiry 설정을 하는 방법은 다음과 같다.
CacheConfiguration<Long, String> cacheConfiguration = CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
ResourcePoolsBuilder.heap(100))
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(20)))
.build();
xml 레벨에서 expiry 설정을 하는 방법은 보다 간단하다.
<cache alias="withExpiry">
<expiry>
<ttl unit="seconds">20</ttl>
</expiry>
</cache>
✅ Cache Eviction 정책
EhCache3부턴 캐시 저장 공간 (heap, off-heap, disk)에 맞게 자동으로 고정되므로, eviction policy를 직접 고를 수 없다.
EhCache2라면 다음 4가지 전략을 고려할 수 있다.
LRU | Least Recently Used. 디폴트 설정. 가장 오랫동안 사용되지 않은 캐시부터 삭제한다. |
LFU | Least Frequently Used. 가장 적게 사용된 캐시부터 삭제한다. |
FIFO | First In, First Out. 캐시가 저장된 순서대로 삭제한다. |
Custom | Ehcache 1.6 이상의 버전을 사용할 경우, 자체 eviction algorithm을 사용할 수 있다. |
Cache2k
✔ 공식 wiki : https://cache2k.org/
✔ github repo : https://github.com/cache2k/cache2k
Java 어플리케이션을 위한 well-engineered 인메모리 캐시 구현을 제공해주는 오픈소스 라이브러리. JCache를 제공한다. 논블로킹 기반의 높은 성능이 셀링 포인트인 듯 하다. 공식문서에 당당히 벤치마크 섹션이 있으니 궁금하면 구경해보자.
(⚠️ 어플리케이션 성격, 서버 스펙, 캐시 설정 등에 따라 비교 결과가 충분히 달라질 수 있으니 동일한 환경에서 이런 차이가 있구나~ 정도로 받아들이는게 좋다)
✅ 특징
공식 문서의 '기능 한 눈에 보기' 페이지에 소개된 feature 들 중 특징적이라고 생각되는 몇 가지를 가져왔다.
1️⃣ Resilience (회복)과 exception handling 지원
캐시 로드 중 예외가 발생하면 해당 키에 대한 새 요청을 멈춰서 데이터 소스 오버플로를 방지하고 리소스 사용량을 최소화 할 수 있다. 캐시 사용 중 발생할 수 있는 예외를 CacheLoaderException으로 감싸서 던지며, 이를 제어할 수 있는 ExceptionPropagator를 제공해 예외처리에 필요한 정보를 전파할 수 있다.
2️⃣ Null 값 지원
이 부분이 좀 특이했는데, cache2k는 설정을 통해 Null값을 저장할 수 있다.
Cache<Integer, Person> cache =
new Cache2kBuilder<Integer, Person>(){}
.name("persons")
.entryCapacity(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.permitNullValues(true) // 여기!
.build();
기본적으로는 Not Null로 캐시에 null을 저장하려고 하면 NPE가 발생하지만, permitNullValues(true)를 이용해 null 캐싱 & 조회를 허용할 수 있다. 단, 이 경우 일반적인 cache aside 패턴(캐시 먼저 찌르고 없으면 DB 조회)이 유효하지 않으므로 별도의 예외처리가 필요하다. 처리 방법이 궁금하다면 아래 링크를 참고하자.
https://cache2k.org/docs/latest/user-guide.html#how-to-enable-null-values
공식문서에 따르면 null 값을 저장해도 추가 메모리 점유나 CPU 오버헤드가 발생하지 않으며, Null 값이 많이 저장될 수 있는 사례에도 캐시를 효과적으로 적용할 수 있어 유용할 것이라고 한다. JCache와 대부분의 캐시 라이브러리들은 null을 지원해주지 않으므로 캐시를 적용할 데이터에 null값이 많고, 정제하기가 힘든 경우 cache2k를 고려하면 좋을 것 같다.
✅ Cache Expiration 정책
지정된 시간이 지나면 캐시를 만료하도록 expiration 정책을 지원한다. 만료 정책은 아래와 같다.
옵션 | 의미 |
Eternal (no expiry) |
캐시에 만료 설정을 하지 않는다. 기본 설정. |
expireAfterWrite (time-to-live) |
캐시 항목이 생성되거나 수정된 후 일정 시간이 지나면 자동으로 만료된다. 지연 시간(= Lag time), 복원 예외 처리 (= Resilience)를 설정해 사용자 정의 ExpiryPolicy를 만들 수 있다. |
단, Ehcache, caffeine와 다르게 TTI (= time to idle / expire after access)는 성능 문제로 지원하지 않는다. 만약 필요하다면 Idle Scan과 Cache.expireAt 또는 MutableCacheEntry.setExpiryTime을 이용해 직접 구현할 수 있다. 아래는 setExpiryTime을 활용해 캐시가 유효 시간 동안 사용되지 않을 경우 자동으로 만료하는 코드이다.
private Cache<K, V> cache;
public void put(K key, V value) {
cache.mutate(key, entry -> {
entry.setValue(value);
entry.setExpiryTime(entry.getStartTime() + expireAfterAccess);
});
}
public V get(K key) {
return cache.invoke(key, entry -> {
entry.setExpiryTime(entry.getStartTime() + expireAfterAccess);
return entry.getValue();
});
}
주의할 점은 만료된 캐시가 즉시 삭제되진 않는다는 점이다. 캐시 만료 이벤트가 발생한 후, 이벤트 리스너가 캐시를 실제로 제거할 때까지 시간이 조금 걸릴 수도 있으니 메모리에 민감하다면 주의하자.
캐시가 만료될 때 자동으로 값을 갱신(=재로드)할 수 있는 refresh 기능도 있다. 캐시를 생성할 때 캐시 빌더에 refresh 옵션을 true로 넣어주면 된다. 이후 expire policy에 따라 만료될 때마다 loader를 활용해 캐시를 갱신한다.
Cache<Key, ValueWithGauge> cache = new Cache2kBuilder<Key, ValueWithGauge>() {}
.loader(k -> new ValueWithGauge())
.refreshAhead(true)
.// 생략
.build();
✅ Cache Eviction 정책
공식 문서에서 eviction과 관련된 목차는 찾지 못했다. 하지만 Cache2kBuilder의 javadoc을 보니, 옵션에 entryCapacity()라는 메서드가 있었다.
캐시가 보유할 수 있는 최대 항목수를 지정할 수 있으며, 최대값에 도달하면 캐시 evict 알고리즘에 의해 이전에 저장된 항목을 제거한다.
다만 공식문서, javadoc 어딜 봐도 어떤 기준으로 캐시 항목을 evict하는지 (e.g., LRU 등) 알 수 없었고, 따로 evict 알고리즘 설정을 커스텀하거나 변경할 수도 없는 것 같았다. 만약 정해진 순서에 따라 항목이 evict되길 원한다면 cache2k는 불만족스러운 선택지가 될 것 같다. 혹시 cache2k의 evict 알고리즘을 아신다면 댓글로 알려주세요!
Caffeine
✔ 공식 wiki : https://github.com/ben-manes/caffeine/wiki
✔ github repo : https://github.com/ben-manes/caffeine
Google Guava에서 영감을 받은 오픈소스 캐시 라이브러리. 최적의 성능에 가까운 고성능 캐싱을 제공한다고 한다. JCache를 제공한다. 공식 wiki에서 다른 라이브러리와의 성능 벤치마크를 확인할 수 있다. (하지만 cache2k는 목록에 없었다. cache2k 공식 문서에는 caffeine과의 벤치마크가 있으니 참고)
✅ 특징
공식 문서를 보면 카페인에서 제공하는 유용한 기능들이 꽤 많다. 그 중 특징적이라고 생각되는 것들을 몇 가지 가져왔다.
1️⃣ 비동기 캐시 로딩 지원
카페인 캐시는 LoadingCache와 AsyncCache, AsyncLoadingCache를 지원한다.
LoadingCache | 동기식으로 동작하는 캐시. 캐시에 요청된 값이 없다면 지정된 로더를 이용해 값을 즉시 로드한다. |
AsyncCache | 비동기로 동작하는 캐시. 로딩은 직접적으로 제공하지 않고, 캐시에 값이 있는 경우 저장된 값을 비동기로 조회한다. synchronous()를 이용해 비동기 작업이 완료될 때까지 캐시 접근을 차단할 수 있다. |
AsyncLoadingCache | 비동기로 동작하는 캐시. AsyncCache와 다르게, 캐시에 값이 없을 경우 지정된 로더를 이용해 값을 비동기로 로드한다. |
2️⃣ 자유도 높은 정책
Time-based, Size-based, Reference-based 등 다양한 캐시 만료(or 관리) 정책을 지원한다. 삭제 방법도 수동 & 정책에 의한 자동 중 선택할 수 있으며, 리스너를 활용한 이벤트 기반으로 제거도 가능하다. 이 외에도 로더를 활용해 refresh 설정을 하거나, policy()를 이용해 캐시 정책에 직접 접근할 수도 있다. 위와 같은 옵션들을 캐시 빌더를 이용해 손쉽게 관리할 수 있다는 것도 큰 장점이다.
옵션이 다양한 만큼 상황과 데이터 성격에 맞는 유연한 캐시 설계가 가능할 것으로 보인다.
3️⃣ 통계, 모니터링 기능 지원
Caffeine.recordStats()을 이용해 캐시 지표를 손쉽게 수집할 수 있다. 캐시 히트율, 캐시 evict count, 캐시 로드 평균 시간 등 캐시 튜닝에 필요한 메트릭들을 다양하게 지원한다.
캐시 통계는 pull 기반, push 기반 두 가지로 제공된다. pull 방식은 Cache.stats()를 직접 호출하는 방식이고, push 방식은 StatsCounter를 정의해 캐시 작업 중 메트릭이 직접 업데이트 되도록 설정하는 방식이다.
Dropwizard Metrics, Prometheus, Micrometer 등을 활용할 수도 있다.
✅ Cache Expiration 정책
아래 세 가지의 timed-base eviction (= expiration) 정책을 제공한다.
expireAfterAccess(long, TimeUnit) | - 항목에 마지막으로 access(read or write)한 후 지정된 시간이 지나면 해당 캐시를 expire - 캐시된 데이터가 세션에 있거나, 혹은 비활성될 때 만료할 필요가 있을 때 적절하다. |
expireAfterWrite(long, TimeUnit) | - 항목이 생성된 후, 또는 가장 최근에 값을 교체한 후 지정된 시간이 지나면 캐시를 expire - 캐시가 일정 시간이 지나면 노후화될 때 (동기화 필요할 때) 적절하다. |
expireAfter(Expiry) | - 변수 기간이 지나면 캐시를 expire - 항목 만료 시간이 외부 리소스에 의해 정해지는 경우 적절하다. |
사용 예시는 다음과 같다.
// expireAfterAccess (expireAfterWrie도 동일)
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
// expire policy 적용
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfter(new Expiry<Key, Graph>() {
public long expireAfterCreate(Key key, Graph graph, long currentTime) {
// Use wall clock time, rather than nanotime, if from an external resource
long seconds = graph.creationDate().plusHours(5)
.minus(System.currentTimeMillis(), MILLIS)
.toEpochSecond();
return TimeUnit.SECONDS.toNanos(seconds);
}
public long expireAfterUpdate(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
public long expireAfterRead(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
})
.build(key -> createExpensiveGraph(key));
여기서 주의할 점은, cache2k와 마찬가지로 expiration은 캐시를 만료만 시키지 바로 삭제하진 않는다는 것이다. 즉 정해진 시간이 지나면 캐시는 만료되어 더 이상 참조할 수 없으나 (원본 저장소로의 접근 발생) 메모리는 그대로 점유하고 있는 상태라는 의미.
실제로, spring boot에 카페인을 적용하고 spring actuator를 이용해 cache size를 체크해보니 expire 시간이 지난 후에도 entry size가 그대로 유지됨을 확인할 수 있었다.
공식 문서에 따르면, 캐시 처리량이 높을 경우 (high-throughput) 굳이 만료된 캐시를 즉시 정리할 필요는 없다. 처리량이 높다면 캐시 데이터의 읽기/쓰기 작업이 빈번하게 발생하는 것이니 굳이 clean up 처리를 해주지 않아도 자연스럽게 캐시가 정리되기 때문이다. 하지만 캐시가 거의 사용되지 않는다면 불필요한 메모리 점유가 지속되니, 메모리 개선이 필요하다면 만료 후 즉시 제거되도록 설정해야 한다.
Cache.cleanUp()을 이용해 외부스레드에서 명시적으로 캐시를 제거할 수도 있으나, 이 경우 TTL을 위해 직접 스케줄링 코드를 구현해야 하므로 비효율적이다. 카페인 개발자는 만료된 항목을 즉시 제거하고 싶다면 캐시를 설정할 때 scheduler 옵션을 추가할 것을 권장한다.
Caffeine.newBuilder()
.softValues()
.recordStats()
.maximumSize(type.getSize())
.scheduler(Scheduler.systemScheduler()) // 여기!
.expireAfterWrite(type.getExpireSeconds(), TimeUnit.SECONDS)
.build();
카페인 캐시는 디폴트로 캐시 스케줄러를 비활성화해둔다. Caffeine.scheduler() 메서드를 이용해 캐시 빌더에 스케줄링 스레드를 지정할 수 있다. scheduler 설정을 추가한 후 spring actuator metric에서 expire 기준 시간이 지나면 cache entry size가 줄어드는 것을 확인할 수 있었다.
Spring Actuator를 활용한 캐시 메트릭 수집 방법이 궁금하다면 아래 더보기를 참고하자.
1. gradle에 spring actuator dependency 추가
implementation("org.springframework.boot:spring-boot-starter-actuator")
2. application.yaml에 actuator 설정 추가 (자세한 설정 정보는 여기 참고)
# spring actuator
management:
server:
port: 9292
endpoint:
health:
show-details: always
endpoints:
web:
exposure:
include: "*"
3. 스프링 어플리케이션 실행 후 2에서 설정한 management.server.port를 이용해 메트릭 내역 확인
4. /actuator/metrics에서 cache와 관련된 메트릭들을 체크
5. (옵션) 만약 필요하다면 grafana, prometheus 등을 활용하여 메트릭을 시각화할 수 있다. 여기 포스팅에선 다루지 않음!
✅ Cache Eviction 정책
Size-based, Time-based, Reference-based 총 세 가지를 제공하는데, Time-based는 expiration 항목으로 분류했습니다.
Sized-based
아래 두 가지의 sized-base eviction 정책을 제공한다.
maximumSize | - 캐시가 특정 크기 이상으로 커지지 않기를 바랄 때 사용한다. - 만약 지정한 크기 이상으로 캐시 항목이 쌓이면 Window TinyLfu 정책을 이용해 참조 빈도와 최신성 기준으로 캐시를 삭제한다. |
maximumWeight | - 캐시 항목마다 가중치가 다른 경우에 사용한다. - Caffine.weigher로 가중치 함수를 정의하고, maximumWeight로 최대 캐시 가중치를 지정한다. - 단, 가중치는 항목 생성 및 업데이트 시점에만 계산된다. |
사용 예시는 다음과 같다.
// maximumSize
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.build(key -> createExpensiveGraph(key));
// maximumWeight
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumWeight(10_000)
.weigher((Key key, Graph graph) -> graph.vertices().size())
.build(key -> createExpensiveGraph(key));
Reference-based
카페인을 이용하면 key와 value에 weak 참조 혹은 soft 참조를 적용해, 캐시의 GC 동작 설정을 조정할 수 있다.
✔️ strong 참조 : Java의 기본 참조 유형.
✔️ soft 참조 : 객체에 대한 strong 참조가 없고, 메모리 여유가 없을 때(OOM) GC의 대상이 된다.
✔️ weak 참조 : 객체에 대한 strong 참조가 없을 때 GC의 대상이 된다. 메모리 여유가 있어도 즉시 수거된다.
아래 세 가지의 reference-based eviction 정책을 제공한다. GC는 equals() 대신 == 연산을 이용하여 값을 비교한다.
weakKeys | 약한 참조를 이용해 키를 저장한다. |
weakValues | 약한 참조를 이용해 값을 저장한다. |
softValues | - 소프트 참조를 이용해 값을 저장한다. - 성능에 영향을 미치므로 softValues 대신 예측 가능한 최대 캐시 크기를 사용할 것을 추천한다. |
사용 예시는 다음과 같다.
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> createExpensiveGraph(key));
단, AsyncCache를 사용할 때는 weakValues, softValues를 사용할 수 없다고 하니 참고하자.
한 눈에 비교하기
지금까지 알아본 내용을 표로 정리했다. 단, 성능의 경우 컴퓨팅 리소스와 테스트 환경 등에 따라 변수가 크므로 생략했다. 대신 각 캐시 라이브러리들이 자체적으로 진행한 벤치마크 링크를 첨부하니, 궁금하면 참고하자.
Ehcache | Cache2k | Caffeine | |
JCache 지원 여부 | O (단, Ehcache는 3버전만) | ||
Spring Boot와의 integration |
O | ||
설정 방법 | xml, 코드 베이스 두 가지 지원 | xml, 코드 베이스 두 가지 지원 | 코드 베이스만 지원 |
expiration | TTL, TTI 지원 | TTL만 지원 | TTL, TTI 지원 (+ 변수 기반 커스텀 가능) |
eviction | - 3버전은 선택 불가능 - 2버전은 LRU, LFU, FIFO, custom 지원 |
- sized-based (capacity & weight) |
- sized-based(capacity & weight) - referenced-based |
refresh | X | O | O |
비동기 지원 | X | △ (로딩만 지원) | O (로딩, 조회 모두 지원) |
통계값 지원 | △ (API만 지원. 이후 직접 구현 필요) |
O (API, JMX, Micrometer 지원) |
O (API, Prometheus, Micrometer, Dropwizard Metrics 지원) |
진행 중인 사이드 프로젝트에서는 최종적으로 Caffeine 캐시를 선택하여 사용 중이다 ☕️
레퍼런스
https://www.jcp.org/en/jsr/detail?id=107
https://www.ehcache.org/documentation/3.10/index.html
https://cache2k.org/docs/latest/user-guide.html#about
https://github.com/ben-manes/caffeine/wiki
'Develop > Etc' 카테고리의 다른 글
[객체지향] 일급 컬렉션 (First Class Collection) (0) | 2023.11.13 |
---|---|
[클린 코드] 클래스 응집도의 중요성, 그리고 완전 생성자와 값 객체 (0) | 2023.11.05 |
[Design Pattern] Facade 패턴과 이모저모 (2) | 2023.07.12 |