Caffeine Cache: 고성능 Java 로컬 캐시 가이드¶
Caffeine은 Java 8 이상을 위한 고성능 캐싱 라이브러리입니다. Google Guava 캐시를 영감으로 삼아 재설계되었으며, 현재 Java 생태계에서 가장 빠르고 효율적인 로컬 캐시로 평가받습니다.
1. Caffeine Cache의 핵심 특징¶
- 압도적 성능: 인메모리 캐시 중 거의 이론적 한계에 가까운 속도를 제공합니다.
- W-TinyLFU 알고리즘: 기존의 LRU(Least Recently Used)나 LFU(Least Frequently Used)보다 진화된 알고리즘을 사용하여 캐시 적중률(Hit Rate)을 극대화합니다.
- 다양한 만료 정책: 시간 기반, 크기 기반, 참조 기반 만료를 지원합니다.
- 통계 수집: 캐시 적중률, 에러율 등 운영에 필요한 다양한 지표를 제공합니다.
2. 핵심 알고리즘: W-TinyLFU¶
대부분의 캐시는 LRU(최근에 안 쓴 것 삭제)를 사용하지만, Caffeine은 W-TinyLFU를 사용합니다.
- TinyLFU: 적은 메모리로 각 항목의 접근 빈도를 추적합니다.
- Window: 최근에 들어온 데이터가 바로 쫓겨나지 않도록 보호하는 구역을 둡니다.
- 효과: 빈도가 높으면서 최근성도 유지하는 데이터 위주로 캐시를 유지하여 성능을 높입니다.
3. 기본 사용법 (Java)¶
3.1 의존성 추가 (Gradle)¶
3.2 캐시 생성 예시¶
Cache<String, DataObject> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5)) // 쓰기 후 5분 뒤 만료
.maximumSize(10_000) // 최대 1만 개 항목 유지
.recordStats() // 통계 수집 활성화
.build();
// 값 저장
cache.put("key1", dataObject);
// 값 조회 (없으면 null)
DataObject data = cache.getIfPresent("key1");
// 값 조회 (없으면 생성하여 반환 - 추천 방식)
DataObject data = cache.get("key2", k -> createData(k));
4. 실전 활용 예제: DB 데이터 캐싱 (Get-or-Compute)¶
캐시에 데이터가 있으면 반환하고, 없으면 DB에서 조회하여 캐시에 저장한 뒤 반환하는 전형적인 패턴입니다. Caffeine의 get 메서드는 이 과정을 원자적(Atomic)으로 처리해 줍니다.
4.1 구현 코드¶
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class UserDataService {
// 1. 캐시 초기화 (최대 1000개, 10분 후 만료)
private final Cache<String, List<String>> userCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
public List<String> select(String key) {
// 2. get(key, mappingFunction) 호출
// - key가 존재하면 즉시 반환
// - key가 없으면 람다식(DB 조회) 실행 후 결과를 캐시에 넣고 반환
return userCache.get(key, this::fetchFromDatabase);
}
private List<String> fetchFromDatabase(String key) {
System.out.println("🔍 DB에서 데이터를 조회합니다: " + key);
// 실제 DB 조회 로직 (예시 데이터)
if ("admin".equals(key)) {
return List.of("ROLE_ADMIN", "ROLE_USER", "DASHBOARD_ACCESS");
}
return List.of("ROLE_USER");
}
public static void main(String[] args) {
UserDataService service = new UserDataService();
// 첫 번째 호출: DB 조회 발생
System.out.println("Result 1: " + service.select("admin"));
// 두 번째 호출: 캐시 히트 (DB 조회 발생 안 함)
System.out.println("Result 2: " + service.select("admin"));
}
}
4.2 주요 이점¶
- 원자성 보장: 동일한 키로 여러 스레드가 동시에 접근할 때, DB 조회가 중복으로 발생하지 않도록 Caffeine이 내부적으로 제어합니다.
- 간결함:
if (cache.get() == null)같은 복잡한 분기문 없이 한 줄로 로직을 완성할 수 있습니다.
5. Spring Boot 연동¶
Spring Boot에서는 @Cacheable 추상화와 함께 사용하여 매우 간편하게 적용할 수 있습니다.
4.1 설정 (CacheManager)¶
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("users", "products");
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(500));
return cacheManager;
}
}
4.2 서비스 적용¶
@Service
public class UserService {
@Cacheable(cacheNames = "users", key = "#id")
public User getUserById(Long id) {
// 실제 DB 조회 로직 (캐시 히트 시 실행 안 됨)
return userRepository.findById(id).orElseThrow();
}
}
5. 로컬 캐시 사용 시 주의사항¶
- 메모리 관리: 로컬 캐시는 JVM 힙(Heap) 메모리를 사용하므로
maximumSize를 적절히 설정하여 OOM(Out Of Memory)을 방지해야 합니다. - 데이터 일관성: 분산 환경(여러 대의 서버)에서는 각 서버마다 캐시 내용이 다를 수 있습니다. 데이터의 엄격한 일관성이 중요하다면 Redis 같은 분산 캐시를 고려해야 합니다.
6. 결론¶
빠른 응답 속도가 중요하고 서버 간의 데이터 불일치가 큰 문제가 되지 않는 영역(예: 설정 정보, 빈번한 공통 코드 조회 등)에서 Caffeine은 가장 강력한 무기입니다.