자바 언어·플랫폼 용어 사전
실무 사례thundering herd · cache stampede · 확률적 조기 만료 · 싱글플라이트

캐시 스탬피드

캐시가 동시에 만료되며 요청이 한꺼번에 원본으로 몰리는 현상. 캐시가 DB를 죽인다.

캐시가 만료되는 순간 모든 요청이 한꺼번에 원본으로 몰리는 현상.

  • 평상시 — 초당 1만 요청 → 캐시가 다 받아 냄 → DB 는 한가 만료 순간 1만 요청이 동시에 캐시 미스

    • 1만 개가 동시에 같은 쿼리를 DB 에 던진다
    • DB 커넥션 고갈 → 타임아웃 → 재시도 → 더 몰림 → 붕괴

캐시가 있어서 더 위험하다 — 평소에 DB 는 그 부하를 받아 본 적이 없다

세 가지 대응

  • ① 싱글플라이트(request coalescing)

    • 같은 키에 대한 동시 요청 중 하나만 원본에 가고 나머지는 그 결과를 기다린다
    • 1만 → 1 로 줄어든다. 가장 효과가 크다
  • ② 확률적 조기 갱신

    • 만료 전에 확률적으로 미리 갱신한다
    • 남은 수명이 짧을수록 갱신 확률을 높인다
    • 만료 시각이 흩어져 동시 미스가 사라진다
  • ③ TTL 지터

    • TTL 에 무작위 편차를 준다 (예: 300초 ± 30초)
    • 대량 워밍업으로 만료 시각이 정렬되는 것을 막는다

싱글플라이트 구현

Map<String, CompletableFuture<V>> inflight = new ConcurrentHashMap<>();

V get(String key) {
    return inflight.computeIfAbsent(key, k ->
            CompletableFuture.supplyAsync(() -> loadFromDb(k)))
        .whenComplete((v, e) -> inflight.remove(key))
        .join();
}

Caffeine 의 LoadingCache 는 이 동작이 기본이다 같은 키의 로딩이 진행 중이면 다른 스레드는 기다린다

  • 직접 만들기보다 검증된 구현을 쓰는 편이 안전하다

인접 현상 구분

  • 스탬피드 — 같은 키가 동시에 만료 → 한 키로 몰림
  • 캐시 관통 — 없는 키를 계속 조회 (존재하지 않는 ID 공격)
    • 빈 결과도 짧게 캐시하거나 블룸 필터로 차단
  • 캐시 붕괴 — 캐시 서버 자체가 죽음 → 전량이 원본으로
    • 로컬 캐시 2차 계층 · 원본에 동시 요청 상한

순서를 만들지 않으면 재시도가 사태를 키운다

  • 1차 붕괴 — 캐시 미스 → DB 과부하 → 타임아웃

  • 2차 붕괴 — 타임아웃 → 클라이언트 재시도 → 부하 2배

  • 3차 붕괴 — 재시도가 재시도를 부른다 → 회복 불가

  • 재시도에는 반드시 조건을 건다

    • 지수 백오프 + 지터
    • 재시도 총량 제한 (retry budget)
    • 서킷 브레이커로 아예 차단하고 폴백을 준다
  • "열심히 다시 시도하는 클라이언트" 가 장애를 길게 만드는 주범이다

캐시 갱신 전략과의 관계

  • Cache-Aside — 미스 시 애플리케이션이 채운다 — 스탬피드에 가장 취약
  • Read-Through — 캐시 계층이 채운다 — 싱글플라이트를 내장하기 쉽다 Write-Through 쓰기 시 함께 갱신 — 만료 자체가 드물어진다 Refresh-Ahead 만료 전에 미리 갱신 — 스탬피드가 구조적으로 안 생긴다

핫 키가 명확하다면 Refresh-Ahead 로 바꾸는 것이 가장 확실한 해법이다

면접 함정

  • "TTL을 늘리면 해결" → 만료 순간의 집중은 그대로다.
  • "캐시를 넣었으니 DB는 안전" → 미스가 몰리면 평소보다 훨씬 큰 부하가 간다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 실무 노하우·실제 사고 사례