
개요
Caffeine은 Google이 개발한 고성능 로컬 캐시 라이브러리입니다. Spring Boot의 기본 캐시 백엔드로 사용되며, 메모리에 데이터를 저장해 데이터베이스 조회를 수백 배 줄일 수 있습니다.
Caffeine vs 다른 캐시
| Caffeine | 로컬 메모리 | ⚡⚡⚡ (1-5ms) | 적음 | 단일 서버 | 자주 조회되는 데이터, 빠른 응답 필요 |
| Redis | 원격 서버 | ⚡⚡ (10-50ms) | 크게 설정 가능 | 우수 | 여러 서버 간 공유, 세션 관리 |
| Memcached | 원격 서버 | ⚡⚡ (20-50ms) | 크게 설정 가능 | 우수 | 단순 KV 저장, 레거시 시스템 |
Caffeine 캐시를 도입했다면 이제 성능을 극대화할 차례입니다. 잘못된 설정은 캐시 히트율을 30%에 머물게 하지만, 올바른 최적화는 90% 이상의 히트율과 50배 향상된 응답 속도를 만들 수 있습니다.
이 가이드에서는 캐시 설정 옵션 이해, 캐시 키 설계, 메모리 효율 개선, 성능 모니터링을 통해 실전 환경에서 Caffeine의 진정한 가치를 끌어내는 방법을 배웁니다. 구체적인 코드 예제와 성능 측정 데이터로 각 최적화 기법의 효과를 확인할 수 있습니다.
Caffeine 설정 옵션 완벽 가이드
Caffeine의 성능은 설정에 달려 있습니다. 다음은 자주 사용하는 주요 옵션들입니다:
1. 크기 제한 (Size-based Eviction)
maximumSize(long size) - 캐시가 보유할 최대 항목 개수
// 최대 1000개 항목만 보관, 초과 시 LRU(가장 오래 사용하지 않은) 항목 삭제
Caffeine.newBuilder()
.maximumSize(1000)
.build()| 100~1000 | 작음 (5~50MB) | 소규모 데이터, 자주 변경되는 데이터 |
| 1000~10000 | 중간 (50~500MB) | 일반적인 데이터 (사용자, 상품) |
| 10000~ | 큼 (500MB+) | 대량의 데이터, 검색 결과 |
2. 시간 기반 만료 (Time-based Expiration)
expireAfterWrite(long duration, TimeUnit unit) - 마지막 쓰기 이후 경과 시간으로 삭제
// 데이터를 마지막으로 쓴 후 10분이 지나면 자동 삭제
Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build()사용 사례: 자주 변경되는 데이터 (사용자 정보, 주문 상태)
expireAfterAccess(long duration, TimeUnit unit) - 마지막 접근 이후 경과 시간으로 삭제
// 데이터에 마지막으로 접근한 후 5분이 지나면 자동 삭제
Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build()사용 사례: 접근 빈도가 낮은 데이터 자동 정리 (검색 결과, 임시 데이터)
3. 자동 갱신 (Refresh)
refreshAfterWrite(long duration, TimeUnit unit) - 쓰기 후 일정 시간 경과 시 백그라운드에서 자동 갱신
// 데이터를 쓴 후 5분이 지나면 백그라운드에서 자동으로 최신 데이터 로드
// (캐시는 계속 기존 데이터 반환, 갱신 완료 후 업데이트)
Caffeine.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES)
.build(key -> loadFromDatabase(key)) // CacheLoader 필요사용 사례: 항상 최신 데이터가 필요한 경우 (실시간 통계, 환율)
장점: 캐시 만료로 인한 지연 없음 (백그라운드 갱신)
4. 약한 참조 (Weak References)
weakKeys() - 키를 약한 참조로 저장 (GC 대상)
Caffeine.newBuilder()
.weakKeys() // 키가 GC되면 캐시 항목도 삭제
.build()주의: 일반적으로 String, Long 등 불변 객체는 약한 참조 불필요
weakValues(), softValues() - 값을 약한/소프트 참조로 저장
Caffeine.newBuilder()
.softValues() // 메모리 부족 시 GC에 의해 자동 삭제
.build()5. 통계 기록 (Statistics)
recordStats() - 캐시 통계 수집 (히트/미스 등)
Caffeine.newBuilder()
.recordStats() // 통계 활성화
.build()
// 나중에 통계 조회
CacheStats stats = cache.stats();
System.out.println("히트율: " + stats.hitRate());
System.out.println("히트 수: " + stats.hitCount());
System.out.println("미스 수: " + stats.missCount());6. 로더 (Cache Loader) - 자동 로드
build(CacheLoader<K, V> loader) - 캐시 미스 시 자동으로 값 로드
// 캐시 미스 시 자동으로 DB에서 조회
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(userId -> userRepository.findById(userId)); // CacheLoader
// 사용
User user = cache.get("user:1"); // 캐시에 없으면 자동 로드설정 조합 예제
Case 1: 사용자 정보 (변경 빈도 낮음, 자주 조회)
Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(30, TimeUnit.MINUTES) // 충분한 만료 시간
.recordStats()
.build()Case 2: 상품 목록 (대량 데이터, 접근 패턴 불균형)
Caffeine.newBuilder()
.maximumSize(20000)
.expireAfterAccess(10, TimeUnit.MINUTES) // 접근 기반 정리
.recordStats()
.build()Case 3: 실시간 통계 (항상 최신 필요)
Caffeine.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(1, TimeUnit.MINUTES) // 자동 갱신
.recordStats()
.build(key -> calculateStatistics(key))Case 4: 세션 데이터 (짧은 수명)
Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterAccess(15, TimeUnit.MINUTES)
.weakValues() // 메모리 부족 시 자동 정리
.recordStats()
.build()1단계: 캐시 키 설계로 히트율 향상
캐시 성능의 첫 번째 열쇠는 캐시 키 설계입니다. 잘못된 키는 같은 데이터를 여러 번 저장하고 미스를 유발합니다.
변경전: 비효율적인 캐시 키
// 변경전: 전체 객체를 키로 사용 (객체 동등성 문제)
@Cacheable(value = "users", key = "#user") // ❌ 같은 사용자도 매번 다른 키 생성
public User getUser(User user) {
return userRepository.findById(user.getId()).orElse(null);
}
// 문제: User(id=1, name="Alice") vs User(id=1, name="Alice", dept="Dev")
// 동일한 사용자지만 다른 객체 = 캐시 미스 발생!Output:
캐시 히트율: 30% (대부분 미스 발생)
불필요한 메모리 사용: 중복 저장
성능: 150ms ~ 200ms (DB 조회)변경후: 최적화된 캐시 키
// 변경후: 기본 식별자만 키로 사용 (일관성 보장)
@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
// 개선: 항상 동일한 ID = 안정적인 캐시 키 생성
// 추가 팁: 복잡한 파라미터는 조합 키 사용
@Cacheable(value = "products", key = "#userId + ':' + #category + ':' + #page")
public Page<Product> searchProducts(Long userId, String category, int page) {
return productRepository.findByUserIdAndCategory(userId, category, PageRequest.of(page, 20));
}Output:
캐시 히트율: 92% (안정적인 키 생성)
메모리 효율: 중복 제거로 50% 절감
성능: 2ms ~ 5ms (캐시 조회)
성능 개선: 100배 향상!핵심: ID, 사용자 식별자, 불변 값만 캐시 키로 사용하세요. 객체 전체를 키로 사용하면 해시코드 변동으로 캐시 미스가 급증합니다.
2단계: 메모리 효율 개선
무제한 캐시는 메모리 누수를 초래합니다. 설정을 통해 효율성을 극대화하세요.
변경전: 최적화되지 않은 설정
// 변경전: 설정 부족
CaffeineCacheManager cacheManager = new CaffeineCacheManager("users", "products");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10000) // ❌ 너무 크면 메모리 낭비
.expireAfterWrite(1, TimeUnit.HOURS)); // ❌ 너무 길면 오래된 데이터 적재
// 메모리 사용량: 500MB+
// 문제: 접근 빈도가 낮은 데이터도 1시간 동안 보관변경후: 데이터 특성별 최적화 설정
// 변경후: 데이터 특성에 따른 세분화 설정
@Configuration
@EnableCaching
public class OptimizedCacheConfig {
@Bean("userCacheManager")
public CacheManager userCacheManager() {
CaffeineCacheManager cache = new CaffeineCacheManager("users");
cache.setCaffeine(Caffeine.newBuilder()
.maximumSize(5000) // 자주 조회되는 데이터
.expireAfterWrite(30, TimeUnit.MINUTES) // 적당한 만료 시간
.recordStats());
return cache;
}
@Bean("productCacheManager")
public CacheManager productCacheManager() {
CaffeineCacheManager cache = new CaffeineCacheManager("products");
cache.setCaffeine(Caffeine.newBuilder()
.maximumSize(20000) // 많은 상품 데이터
.expireAfterAccess(10, TimeUnit.MINUTES) // 접근 기반 만료 (비활성 자동 정리)
.recordStats());
return cache;
}
@Bean("sessionCacheManager")
public CacheManager sessionCacheManager() {
CaffeineCacheManager cache = new CaffeineCacheManager("sessions");
cache.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000) // 세션은 적게
.expireAfterAccess(15, TimeUnit.MINUTES) // 짧은 만료 시간
.refreshAfterWrite(5, TimeUnit.MINUTES) // 주기적 갱신
.recordStats());
return cache;
}
}
// 메모리 사용량: 200MB (60% 절감)
// 효율: 자동 정리로 메모리 누수 방지Output:
user 캐시: 5000개 항목, 30분 만료 → 메모리 효율 최고
product 캐시: 20000개 항목, 접근 기반 정리 → 메모리 탄력적 관리
session 캐시: 1000개 항목, 자동 갱신 → 최신 데이터 유지
총 메모리 사용량: 150MB (최적화 전 500MB에서 70% 절감)3단계: 성능 모니터링 및 튜닝
통계 데이터 없이는 최적화할 수 없습니다. 캐시 통계를 수집하고 분석하세요.
@RestController
@RequestMapping("/api/cache")
public class CacheMonitorController {
@Autowired
private CacheManager cacheManager;
@GetMapping("/stats")
public Map<String, Object> getCacheStats() {
Map<String, Object> allStats = new HashMap<>();
// 모든 캐시의 통계 조회
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
if (cache instanceof CaffeineCache) {
CaffeineCache caffeineCache = (CaffeineCache) cache;
com.github.benmanes.caffeine.cache.Cache nativeCache =
caffeineCache.getNativeCache();
CacheStats stats = nativeCache.stats();
Map<String, Object> cacheData = new LinkedHashMap<>();
cacheData.put("hitRate", String.format("%.2f%%", stats.hitRate() * 100));
cacheData.put("hitCount", stats.hitCount());
cacheData.put("missCount", stats.missCount());
cacheData.put("evictionCount", stats.evictionCount());
cacheData.put("avgLoadPenalty", String.format("%.2f us",
stats.averageLoadPenalty() / 1000.0));
cacheData.put("size", nativeCache.estimatedSize());
allStats.put(cacheName, cacheData);
}
});
return allStats;
}
@GetMapping("/optimize-recommendation")
public String getOptimizationRecommendation() {
StringBuilder recommendation = new StringBuilder();
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
if (cache instanceof CaffeineCache) {
CaffeineCache caffeineCache = (CaffeineCache) cache;
CacheStats stats = caffeineCache.getNativeCache().stats();
double hitRate = stats.hitRate();
recommendation.append(cacheName).append(": ");
if (hitRate < 0.7) {
recommendation.append("⚠️ 히트율이 낮습니다. 캐시 키 설계 검토 필요\n");
} else if (hitRate >= 0.85) {
recommendation.append("✅ 우수한 성능 (히트율 ")
.append(String.format("%.0f%%", hitRate * 100)).append(")\n");
} else {
recommendation.append("⚡ 양호한 성능. 만료 시간 조정 고려\n");
}
}
});
return recommendation.toString();
}
}
// GET /api/cache/stats 응답:
// {
// "users": {
// "hitRate": "92.15%",
// "hitCount": 923,
// "missCount": 79,
// "evictionCount": 5,
// "avgLoadPenalty": "12.34 us",
// "size": 4856
// },
// "products": {
// "hitRate": "87.50%",
// "hitCount": 700,
// "missCount": 100,
// "evictionCount": 12,
// "avgLoadPenalty": "45.67 us",
// "size": 19342
// }
// }성능 분석 가이드
| Hit Rate | 80% 이상 | 캐시 효율성 | 낮으면 키 설계 검토 |
| Miss Count | 전체의 10~20% | 캐시 미스 빈도 | 높으면 만료 시간 증가 |
| Eviction Count | 저수준 | 메모리 부족으로 제거된 항목 | 높으면 maximumSize 증가 |
| Avg Load Penalty | 낮을수록 좋음 | 캐시 미스 시 조회 비용 | 느리면 DB 인덱스 확인 |
4단계: 실전 최적화 체크리스트
□ 캐시 키는 불변값(ID)만 사용하고 있나?
□ 데이터 특성별로 다른 CacheManager를 설정했나?
□ maximumSize와 만료 시간이 데이터 특성에 맞나?
□ @CacheEvict/@CachePut으로 데이터 일관성을 보장하나?
□ 매주 캐시 통계를 확인하고 있나?
□ 히트율이 80% 이상인가?맺음말
Caffeine 캐시 성능 최적화의 핵심은 세 가지입니다:
- 캐시 키 설계 - 안정적인 키가 고히트율의 기초
- 메모리 효율 - 데이터 특성에 맞춘 세분화 설정
- 지속적 모니터링 - 통계 기반의 점진적 튜닝
이 세 가지를 실행하면 30%의 초기 히트율에서 90% 이상으로 향상시킬 수 있으며, 응답 시간은 150ms에서 5ms 이하로 개선됩니다. 특히 중요한 것은 일회성 최적화가 아닌 지속적인 모니터링입니다. 매주 통계를 확인하고 필요에 따라 설정을 조정하세요.
'프로그래밍 PROGRAMMING > 자바 JAVA AND FRAMEWORKS' 카테고리의 다른 글
| [JAVA/SPRING] Java AOP Aspect 심화: 동적 프록시와 Pointcut 최적화 (0) | 2026.02.11 |
|---|---|
| [JAVA/SPRING] Spring AOP 이해하기: @Aspect로 횡단 관심사 처리하기 (0) | 2026.02.10 |
| [JAVA/SPRING] Git에 비밀번호 올리지 마세요: 스프링 부트 민감 정보 관리 전략 (Jasypt, Vault) (0) | 2026.02.07 |
| [JAVA/REDIS] Redis를 이용해서 중복요청 방지하는 방법 (1) | 2026.02.06 |
| [JAVA] Random vs SecureRandom vs ThreadLocalRandom: 자바 난수 생성의 모든 것 (0) | 2026.02.02 |