- 실무에서 자주 발생하는 캐시 문제의 유형 및 원인
- 캐시 폭발(Cache Stampede), 캐시 누수(Cache Leak), 캐시 오염(Cache Pollution), 캐시 패싱(Infinite Caching)의 사례
- 각 문제의 동작 원리를 시각적으로 이해하고, 방지 전략 익히기
- TTL, Lock, Background Refresh 등 주요 방지법
- 문제 상황을 사전에 예방하고, 운영 환경에서 캐시 안정성을 유지할 수 있는 역량을 갖추면 좋음
캐시 폭발(Cache Stampede)
익스플로전 아님
1. 개념 설명
다수의 요청이 동시에 캐시 미스(Cache Miss)를 발생시켜 DB나 원본 서버에 과도한 부하를 주는 현상을 말함
다수의 사용자가 요청 - > 근데 캐시 만료 시점임 - > 모두 Cache Miss - > 이 요청이 모두 DB에 집중됨 - > 서버 부하 급증
2. 원인과 영향
| 원인 | 설명 |
| TTL(Time to Live) 만료 | 캐시가 동시에 만료되어 모든 요청이 DB로 향함 |
| Lock 미적용 | 캐시가 비어있을 때 여러 요청이 동시에 DB에 접근 |
| 대용량 트래픽 | 인기 데이터의 만료 시점에 요청이 폭증함 |
* 결과적으로 DB 연결 수가 급증하며, 서버 응답이 느려지거나 다운될 수 있음
3. 방지 전략
1) 분산 만료(Randomized TTL)
TTL(Time to Live)을 랜덤하게 설정해서 캐시 만료 시점을 분산시킴
int ttl = 60 + new Random().nextInt(30); // 60 ~ 90초 범위의 TTL
cache.put(key, value, ttl, TimeUnit.SECONDS);
* 모든 캐시가 한번에 만료되지 않게끔 Random()을 넣어 폭발 위험을 완화시킬 수 있음
2) 캐시 재갱신 잠금(Lock)
하나의 요청에 대해서만 DB 접근을 허용하고, 나머지는 대기시킬 수 있음
synchronized(lock) {
if(cache.get(key) == null) {
value = db.load(key);
cache.put(key, value);
}
}
// ??
* 동시에 여러 스레드가 동일한 데이터를 갱신하지 않도록 방지할 수 있음
3) 백그라운드 캐시 갱신(Refresh Ahead)
캐시 만료전에 백그라운드에서 미리 데이터를 갱신함
CompletableFuture.runAsync(() -> {
if(isExpiringSoon(key)) {
cache.put(key, db.load(key));
}
});
* 사용자는 항상 최신 데이터에 빠르게 접근할 수 있음
캐시 누수(Cache Leak)
1. 개념
캐시가 더 이상 사용되지 않는 데이터를 계속 메모리에 보관하여 Heap 메모리 부족(OOM)을 유발하는 현상임
2. 원인 및 증상
| 원인 | 증상 |
| TTL 미설정 | 캐시 항목이 영구적으로 메모리에 남음 |
| 크기 제한 누락 | 캐시 최대 크기가 없어서 무한히 저장됨 |
| 약한 참조 미사용 | GC가 캐시 항목을 해제하지 못함 |
* 결과적으로 GC 빈도가 높아지고, 응답 지연 및 OutOfMemoryError가 발생하게 됨
3. 해결법
1) TTL 설정
Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
* 일정 시간이 지나면 자동으로 항목이 제거됨
2) 캐시 크기 제한하기
Caffeine.newBuilder().maximumSize(1000).build();
* 일정 개수 이상 저장되면 오래된 항목부터 제거된다
3) WeakReference 사용하기
GC가 참조되지 않는 객체를 자동으로 제거하도록 설정할 수 있음
Caffeine.newBuilder().weakKeys().weakValues().build();
* 불필요한 객체가 자동으로 해제되어 메모리 누수를 방지할 수 있음
캐시 오염(Cache Pollution)
1. 개념
자주 사용되지 않는 데이터가 캐시에 저장되어, 자주 사용하는 데이터가 밀려나는 현상임
2. 원인 및 영향
| 원인 | 설명 |
| LRU 정책의 한계 | 접근 빈도가 낮은 항목이 최근 접근으로 인해 유지됨 |
| 캐시 Key 설계의 오류 | 비효율적 Key 생성으로 동일 데이터 중복 저장 |
| 인기 데이터 식별 실패 | 중요한 데이터가 우선순위를 받지 못함 |
* Hit Ratio가 감소하고, 전체 성능이 저하함
3. 방지 전략
1) LFU(Least Frequently Used) 정책
Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES).build();
Caffeine에서는 내부적으로 LFU 기반의 접근 빈도 추적 알고리즘을 사용한다
* 자주 사용되는 항목은 오래 유지되고, 드문 데이터는 자동으로 제거되게끔 함
2) 캐시 Key 최적화
Key에 의미 있는 식별자를 포함시켜서 중복 캐싱을 방지하게끔 함
@Cacheable(value = "productCache", key = "#productId + '-' + #region")
* 동일한 제품이라도 지역별 데이터가 구분되어서 저장됨
무한 캐싱(Infinite Caching)
1. 개념
캐시 항목에 TTL(Time to Live)이 설정되지 않아서 데이터가 영구적으로 저장되는 현상임
2. 원인 및 문제점
| 원인 | 설명 |
| TTL 설정 누락 | 캐시 항목이 만료되지 않음 |
| 정적 데이터로 오인함 | 업데이트 주기가 존재하는 데이터에 TTL이 미적용됨 |
| 모니터링 부재 | 캐시 사용량 증가를 인지하지 못함 |
* 오래된 데이터가 유지되면서 데이터 불일치(Data Inconsistency)가 발생할 수 있음
3. 해결법
1) TTL(Time to Live) 정책 적용하기
Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build();
* 일정 시간이 지나면 캐시 항목이 자동으로 만료된다
2) 주기적 캐시 정리(Eviction Task)
@Scheduled(fixedRate = 300000) // 5분마다 실행됨
public void clearExpiredCache() {
cache.invalidateAll();
}
* 일정 주기로 캐시를 초기화하여 오래된 데이터를 제거할 수 있음
팁
* 캐시 문제는 대규모 트래픽 환경에서 지연(Latency)를 야기하기 쉽다!
* @CacheEvict(allEntries = ture)는 주의해서 사용해야함, 전체 캐시를 비우게 되면 일시적인 부하가 발생하기 때문
* Prometheus / Grafana와 연동해서 캐시 Hit / Miss 비율, Eviction 횟수, Memory Usage를 상시 모니터링 가능
* 캐시 관련 장애가 발생하면 먼저 TTL 정책과 Key 설계를 점검할 수 있음
정리
| 문제 유형 | 주요 원인 | 해결 전략 |
| 캐시 폭발(Cache Stampede) | TTL 동시 만료, 동시 접근 | Random TTL, Lock, Background Refresh |
| 캐시 누수(Cache Leak) | TTL 미설정, 크기 제한이 없음 | TTL 설정, Maximum size 제한, Weak Reference |
| 캐시 오염(Cache Pollution) | LRU 정책의 한계, Key 설정의 문제 | LFU 정책 활용, Key 최적화 |
| 무한 캐싱(Infinite Caching) | TTL 설정이 누락됨 | TTL 정책, 주기적 Eviction |
'Spring Boot > Cache' 카테고리의 다른 글
| 분산 캐시의 이해와 Redis : Redis 개요와 주요 특징 (0) | 2026.01.17 |
|---|---|
| 분산 캐시의 이해와 Redis : 분산 캐시의 필요성 (0) | 2026.01.17 |
| 캐시 모니터링과 문제 해결 : 캐시 성능 분석 (0) | 2026.01.15 |
| 캐시 모니터링과 문제 해결 : Spring Boot Actuator를 활용한 모니터링 (0) | 2026.01.15 |
| 로컬 캐시 구현과 최적화 : 로컬 캐시 성능 측정 (0) | 2026.01.12 |