FireDrago
[기술면접] 2. 캐시 스탬피드(Cache Stampede)에 대해 설명해주세요. 해결방법은요? 본문
암기 보단 사고과정
"캐시 스탬피드에 대해서 설명해주세요"
머릿속이 하얘졌다. 생전 처음 듣는 개념이었기 때문이다.
다행히 면접관은 예시를 통해 설명을 이어갔다.
"자 그럼 캐시 만료 시간을 1분으로 지정했다고 가정해보죠.
캐시 만료 이후, 폭발적 요청이 와서 db 부하가 폭증하는 상황이 오면 어떻게 해결할 수 있을까요?"
처음 접하는 문제였지만, 생각해보니 일단 '캐시 만료와 동시에 몰리는 요청'이 핵심이라고 생각했다.
우선 모니터링을 통해 적절한 캐시 만료 시간을 찾아 조정하는 방법이 떠올랐다.
그리고 여러 요청이 동시에 DB를 찌르는 것이 문제라면, 분산락을 도입해
하나의 요청만 DB에 접근해 캐시를 갱신하도록 통제할 수 있을 것이라 답변했다.
"분산락을 도입하면 DB 부하는 줄일 수 있을것 같네요. 근데 저라면 이 상황에서 분산락 도입은 안할 것 같은데 어떻게 생각하세요?"
분산락을 도입하면, 분명 DB 부하는 줄어들겠지만, 다른 요청이 DB요청을 대기할것이므로,
캐시의 목적중 하나인 빠른 응답속도면에서 손해가 있을것 같다고 답변했다.
그리고 2중 캐시 (Caffeine Cache + Redis)를 도입해서
한쪽이 캐싱 만료 되어도, 다른 쪽이 캐싱해 줄 수 있을것 같다는 생각이 번쩍 떠올라서 답변했고, 이 질문은 마무리됐다.
집에 돌아와서 캐시스탬피드가 정확히 뭔지, 어떻게 해결할지 공부했다.
캐시 스탬피드 (Cache Stampede)

위 이미지를 보면 쉽게 이해될것이다.
캐시 스탬피드 현상은 캐시 만료로 인해, DB 과부하가 걸리는 현상을 말한다.
[해결책 1] 분산락 (Distributed Lock)

여러 요청이 동시에 DB를 찌르는 것이 문제라면,
가장 먼저 떠오르는 방법은 단 하나의 요청만 DB에 다녀오게 통제하자는 것이다.
`Redis`의 `SETNX`, `Redison` 등을 활용해 분산락을 구현하면,
락을 획득한 딱 하나의 스레드만 DB에서 데이터를 조회해 캐시를 갱신하도록 만들 수 있다.
응답지연 및 스레드풀 고갈
분명 DB 부하를 줄여 DB를 보호할 수는 있다. 하지만 반드시 고려해야 할 점이 있다.
락을 얻지 못한 스레드들은 락이 풀릴 때까지 대기 상태(Blocking)에 빠지게 된다.
- 응답 지연과 UX 저하 : 캐시를 사용하는 가장 큰 목적은 빠른 응답 속도인데, 락 대기로 인해 사용자들의 로딩이 길어진다.
- 스레드 풀 고갈 : 대기하는 스레드들이 많아지면 애플리케이션의 스레드풀이 고갈 될 위험이 있다.
결국 분산락은 DB 보호의 목적은 달성하지만,
서비스 전체의 가용성과 성능을 깎아먹는 비효율적인 해결책이 될 수 있다.
[해결책 2] 지터 (무작위 지연시간)

분산락처럼 스레드들을 무작정 줄 세워 대기하게 만드는 것이 문제라면,
애초에 캐시가 동시에 만료되는 상황 자체를 막을 수는 없을까?
여기서 등장하는 개념이 지터(Jitter)다.
지터는 캐시 만료 시간에 무작위 지연시간을 더해주는 기법이다.
예를들어 모든 데이터의 TTL을 일괄적으로 딱 1분으로 설정하는 것이 아니라,
`60초 + (0~10초 사이의 랜덤 값)` 으로 설정하는 것이다.
위 이미지에서 볼 수 있듯, 지터를 적용하면
동일한 시점에 생성된 여러 데이터들의 만료 시간이 제각각으로 분산된다.
대규모 배치 작업이나 이벤트 등으로 인해
수많은 캐시 키가 동시에 만료되면서 DB에 부하가 집중되는 현상을 적은 비용으로 효과적으로 막을 수 있다.
핫 키 문제
지터는 여러개의 서로 다른 데이터가 만료되는 시간을 흩뿌리는 데는 유용하다.
하지만 단 하나의 핫키 (예: 메인 화면의 타임 특가 상품)가 만료되는 상황에서는 무용지물이다.
왜냐하면 만료 시간을 어떻게 분산시키든, 결국 그 핫 키 하나가 만료되는 찰나의 순간은 반드시 있고,
그 순간 수천개의 요청이 동시에 DB로 쏟아지는 캐시 스탬피드는 동일하게 발생한다.
[해결책 3] 2중 캐시

단일 핫 키가 만료되어 DB를 조회해 오는 그 짧은 공백.
지터로도 막을 수 없는 이 공백을 메우기 위해 2중 캐시(Look-aside Layered Cache)를 사용할 수 있다.
2중 캐시는 각 아키텍처의 장점을 결합한 구조다.
애플리케이션의 로컬 메모리를 사용하는 1차 캐시(예: Caffeine Cache)와
분산 환경에서 공유되는 2차 캐시(예: Redis)를 계층적으로 배치한다.
- 모든 요청은 먼저 가장 가까운 1차 로컬 캐시를 찌른다. 메모리 내부 조회이므로 네트워크 오버헤드 없이 가장 빠르다.
- 만약 로컬 캐시에 데이터가 없다면(Miss), 그때 2차 글로벌 캐시(Redis)를 조회한다.
- 만약 Redis 마저 비었다면, 그제서야 DB를 조회한다.
이 구조를 도입하면 캐시 미스 타이밍을 엇갈리게 관리 할 수 있다.
로컬캐시의 만료시간을 Redis의 만료시간 보다 약간 짧거나 길게 설정해 두면,
Redis의 핫 키가 잠시 비어있는 순간이 오더라도 각 서버 인스턴스에 로컬캐시를 통해 캐시 스탬피드를 차단 할 수 있다.
데이터 정합성
분산 환경에서 로컬 캐시가 함께 나올때 반드시 데이터 정합성을 고려해야한다.
Redis는 모든 서버가 공유하는 하나의 저장소이기에 데이터 정합성을 유지하기 쉽다.
하지만 로컬 캐시는 서버 인스턴스마다 각각 독립된 메모리 공간을 가진다.
만약 관리자가 DB에서 상품 가격을 수정했다면,
Redis의 데이터는 바로 무효화(Invalidation)할 수 있지만,
여러 대의 분산 서버가 각자 들고 있는 로컬 캐시 속 데이터까지 동시에 싱크를 맞추는 것은 매우 까다롭다.
이를 해결하기 위해 `MQ(Message Queue)`나 `Redis Pub/Sub`을 활용해 전송하는 구조를 추가할 수도 있지만,
이는 시스템의 복잡도를 크게 높인다.
[해결책 4] 논리적 만료와 백그라운드 갱신 (PER)

복잡한 동기화 문제를 피하면서도 사용자에게 지연 없는 응답을 제공하는 방법은,
캐시 데이터에 물리적 만료 시간(Redis TTL)과 논리적 만료 시간(Logical TTL)을 분리해서 부여하는 것이다.
핵심은 데이터가 캐시에서 완전히 삭제(물리적 만료)되어 스탬피드가 발생하기 전에,
애플리케이션 코드로 설정한 '논리적 만료 시간'을 먼저 지나게 만드는 것이다.
위 이미지의 흐름을 따라가 보면 이 과정이 아주 명확해진다.
- 기존 데이터 즉시 반환 : 사용자가 데이터를 요청했을 때, 캐시는 살아있지만 논리적 만료 시간이 지났음을 확인한다. 이때 스레드들을 락에 대기시키는 것이 아니라, 일단 기존의 데이터를 사용자에게 즉시 반환한다.
- 비동기 백그라운드 갱신 : 기존 데이터를 반환함과 동시에, 뒤에서는 별도의 백그라운드 스레드를 띄워 비동기적으로 DB를 조회하고 캐시를 최신화한다.
- 백그라운드 스탬피드 방지 : 수많은 요청이 동시에 들어와 무수히 많은 백그라운드 스레드가 생성되는 것을 막기 위해, 갱신 스레드를 띄우기 직전에만 짧게 분산락(또는 로컬락)을 건다. 중요한 점은, 락 획득에 실패한 스레드들은 대기(Blocking)하는 것이 아니라 과거 데이터를 반환하고 끝낸다.
데이터 최신성 vs 서비스 가용성
이 방식도 트레이드오프는 있다.
백그라운드에서 갱신이 완료되기 전 아주 짧은 찰나 동안, 사용자들은 최신이 아닌 과거의 데이터를 보게 된다
대부분의 대규모 B2C 서비스(예: 메인 화면의 타임 특가 상품 목록)에서는 과거의 데이터를 보여주는 것보다,
락 대기나 DB 부하로 인해 서버가 멈춰서 모든 사용자가 무한 로딩을 겪는 것을 훨씬 큰 장애로 여긴다.
즉, 약간의 데이터 정합성을 양보하는 대신 성능과 시스템 가용성을 챙긴 전략이다.
아키텍처에 은탄환은 없다.
처음 "캐시 스탬피드를 어떻게 해결할 것인가?"라는 질문을 받았을 때는 머릿속이 하얘졌지만,
꼬리에 꼬리를 무는 기술면접을 통해 많은 것을 깨달았다.
분산락, 지터, 2중 캐시, 그리고 논리적 만료까지. 어떤것도 완벽한 정답은 아니다.
'DB 부하 방어', '사용자 응답 속도', '데이터 정합성 유지', '시스템 복잡도 관리'라는 여러 가치 사이에서,
현재 우리 서비스의 비즈니스 요구사항에 가장 알맞은 기술을 선택하고 그 트레이드오프를 설명할 수 있는 것.
그것이 면접에서 요구하는 '사고 과정'이자 백엔드 엔지니어의 역량이었음을 느꼈다.
단순히 새로운 기술 용어를 지식으로 암기하는 것을 넘어,
평소에 내가 도입하는 기술의 이유와 한계를 명확히 정리하고 나만의 근거를 만드는 훈련을 꾸준히 해야겠다.
'인프라' 카테고리의 다른 글
| [Docker] Docker를 왜 써야하는데? (0) | 2025.07.20 |
|---|