본문 바로가기

Spring Boot/Cache

캐시 모니터링과 문제 해결 : 일반적 캐시 문제와 해결 방법

 - 실무에서 자주 발생하는 캐시 문제의 유형 및 원인

 - 캐시 폭발(Cache Stampede), 캐시 누수(Cache Leak), 캐시 오염(Cache Pollution), 캐시 패싱(Infinite Caching)의 사례

 - 각 문제의 동작 원리를 시각적으로 이해하고, 방지 전략 익히기

 - TTL, Lock, Background Refresh 등 주요 방지법

 - 문제 상황을 사전에 예방하고, 운영 환경에서 캐시 안정성을 유지할 수 있는 역량을 갖추면 좋음

 


캐시 폭발(Cache Stampede)

익스플로전 아님

 

1. 개념 설명

다수의 요청이 동시에 캐시 미스(Cache Miss)를 발생시켜 DB나 원본 서버에 과도한 부하를 주는 현상을 말함

 

Cache Stampede

 

다수의 사용자가 요청 - > 근데 캐시 만료 시점임 - > 모두 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)을 유발하는 현상임

 

캐시 누수, Cache Leak

 

 

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. 개념

자주 사용되지 않는 데이터가 캐시에 저장되어, 자주 사용하는 데이터가 밀려나는 현상임

캐시 오염, Cache Pollution

 

 

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)이 설정되지 않아서 데이터가 영구적으로 저장되는 현상임

 

무한 캐싱, Infinite Caching

 

 

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