Java

[Java] 로컬 캐시 3대장 완벽 비교: Caffeine vs Ehcache vs Guava (2)

hyuk0309 2026. 2. 16. 14:00

 

저번글에서 Caffeine, Ehcache, Guava에 대한 기본 개념과 성능 차이를 알아봤다.

이번 글에서는 좀 더 나아가, 왜 이런 차이가 발생하는지 정리하겠다.

 

Guava

 

Guava는 3 캐시 중 성능이 가장 안 나오는 캐시다. 

구조는 Java의 ConcurrentHashMap과 동일하다.

A Cache is similar to ConcurrentMap, but not quite the same. The most fundamental difference is that a ConcurrentMap persists all elements that are added to it until they are explicitly removed.

 

ConcurrentMap과 동일하게, 여러 Segment로 나눠서 데이터를 저장해둔다.

 

 

캐시에 데이터를 읽어오는 과정은 다음과 같다.

1. key를 이용해 segment 찾기

 

2. segment에서 value 찾기

 

4. 사용했다고 표시 (cache eviction을 위한 값)

 

5. 데이터 정리 작업이 필요하면 정리 (데이터 정리가 필요하면, 해당 segment를 잠그고 정리한다.)

 

 

데이터를 쓰는 과정은 아래와 같다.

1. Segment 찾기

 

2. Segment 잠금

 

3. 사전 데이터 정리

 

4. 데이터 저장

 

5. 캐시 Eviction 필요하면, 수행

 

 

참고 : https://github.com/google/guava/wiki/CachesExplained

 

 

정리해보면, 기본적인 구조는 ConcurrentMap 과 비슷하고, 읽기 또는 쓰기 작업을 하는 스레드에서 캐시 메모리 관리를 위한 작업들(접근 시간 기록, 메모리 정리)을 하기 때문에, 많은 요청을 처리하지 못하는게 이해간다.

 

EhCache

 

EhCache에서 Heap에만 저장하도록 설정한다면, 내부족으로 ConcurrentHashMap에 데이터를 저장한다.

(어디에 저장할지, 어떤 데이터를 저장할지에 따라 데이터 저장에 사용되는 구현체가 달라진다.)

 

EhacheManager의 getStore 메서드를 보면, 자세한 내용을 확인할 수 있다.

 

 

OnHeap을 사용하기로 설정했다면, 내부적으로 ConcurrentHashMap을 사용해 데이터를 저장한다.

 

캐시에 데이터를 읽어오는 과정은 다음과 같다.

 

1. 캐시 데이터를 조회할 수 있는 상황인지 판단

 

2. ConcurrentHashMap에 저장되어 있는 데이터 조회

 

3. 데이터 만료 여부 판단

 

4. 데이터 최신 접근 시간 갱신

 

데이터를 쓰는 과정은 아래와 같다.

 

1. 데이터 유효성 확인

 

2. 데이터 쓰기

내부적으로 ConcurrentHashMap의 compute 이용

 

3. 캐시 eviction 필요 여부 체크

 

4. eviction

모든 원소를 탐색하지 않고, SAMPLE_SIZE(8개) 데이터만 무작위로 탐색해서, Eviction 실행.

 

EhCache OnHeapStore 구현체의 evicti 메소드

 

ConcurrentHashMap의 getEvictionCandidate 메소드 (무작위 8개 선택 후, prioritizer 함수에 의해 결정)

 

ConcurrentHashMap에 넘기는 후보 선정 함수. (LFU)

 

정리해보면, 데이터를 저장할때는 ConcurrentHashMap을 사용하고, Cache Eviction 시에는 모든 원소를 보는게 아니라, 8개를 무작위로 뽑아 가장 오래된 데이터를 지운다.

 

Guava와 가장 큰 차이는 데이터 저장 방식이다. Guava는 데이터를 Segment단위로 저장하고, Ehcache는 Node 단위로 저장한다. 쉽게 말하면 Guava는 하나의 Segement가 잠겨 있으면 해당 세그먼트에 포함된 데이터에 접근하지 못한다. 하지만 Ehcache는 Node 단위로 저정하기 때문에 그렇지 않다. 그리고 Eviction도 Ehcache는 8개를 무작위로 뽑아 오랜된 데이터를 삭제하지만, Guava는 queue를 통해 관리해 가장 오래된 데이터를 엄격하게 삭제한다. 이 두 부분이 성능에 큰 차이를 만든 것 같다.

 

참고 : https://github.com/ehcache/ehcache3

 

Caffeine

 

데이터는 내부적으로 ConcurrentHashMap을 사용한다. 그리고 이를 논리적으로 나눈다. 

Window : 새로 들어온 노드들이 대기하는 LRU 리스트

Probation : 탈락 위기 노드들이 대기하는 LRU 리스트

Protected : 보호받는 스타 노드들이 대기하는 LRU 리스트

 

추가로, CountMinSketch 를 이용해 적은 공간을 이용해 데이터 사용 빈도를 저장한다.

 

Node의 queueType으로 데이터가 어떤 리스트에 속한지 알 수 있다.

 

캐시에 데이터를 읽어오는 과정은 다음과 같다.

 

1. 데이터 조회

ConcurrentHashMap에서 데이터 조회

 

2. 만료 체크

 

3. Ring Buffer에 기록 남김

 

4. 반환

 

 

데이터를 쓰는 과정은 아래와 같다.

 

1. 데이터 저장

ConcurrentHashMap에 데이터 저장

 

2. Writer Buffer에 기록

 

Ring Buffer가 정리되는 과정

Caffeine Cache는 이전 캐시들과 달리 메모리 관리를 위한 작업들이 비동기로 처리된다. 그리고 이 작업의 시작점은 Ring Buffer다.

Ring Buffer는 특정 시간이 지나거나, 임계치 이상의 값이 쌓이면 백그라운드 스레드에 의해 정리 작업을 시작한다.

 

readBuffer를 먼저 처리하고, 그 다음 writeBuffer를 처리한다.

 

ReadBuffer 처리 과정에서 데이터들의 논리적 저장위치(Window, Probation, Protected)가 변경된다.

readBuffer가 처리되는 메소드

 

accessPolicy는 기본적으로 onAccess 호출

 

실제로 readBuffer에 있는 Node를 꺼내서 처리하는 부분

 

writeBuffer가 처리될때는 Node를 가져와 Node에 논리적 저장위치를 지정해주거나, 업데이트해준다. cache의 용량이 초과한 경우 cache eviction도 여기서 처리한다.

 

Cache Eviction이 발생하면, Window에 있던 Node가 Main 구역으로 이동한다.

 

Main 구역이 꽉찬 경우 가장 오래된 Node가 제거된다. 그리고 이때 CountMinSketch를 활용한다.

 

 

정리해보면, Caffeine 캐시도 Ehcache와 동일하게 ConcurrentHashMap 기준이다. 하지만, 성능을 위한 최적화 전략들이 많이 들어가있다. ConcurrentHashMap을 논리 구역으로 나누기, Node 최적화, 비동기 처리 등.. 이 때문에 Ehcache보다 더 좋은 성능을 보여주는 것 같다.

 

 

참고 : https://github.com/ben-manes/caffeine/wiki/Design

 

Design

A high performance caching library for Java. Contribute to ben-manes/caffeine development by creating an account on GitHub.

github.com

 

지금까지 Guava, Ehcache, Caffeine의 성능 차이가 왜 발생하는지 코드를 통해 분석해봤다.

결과적으로 Caffeine Cache가 최적화 전략들이 가장 많이 적용되어 있어 가장 좋은 성능을 보여주는 것 같다.