이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 캐시 정합성 사고에서 key 삭제보다 먼저 확인해야 할 것은 무엇인가?
- stale 값, 잘못된 값, 누락된 값을 어떻게 구분하는가?
- 캐시 정책이 성능, 정합성, 장애 격리, 운영 복잡도를 어떻게 바꾸는가?
캐시 사고의 핵심
캐시 정합성 문제는 Redis를 지우면 끝인 문제가 아니다.
사용자가 본 값이 오래된 값인지, 틀린 값인지, 권한 밖의 값인지에 따라 위험도가 달라진다.
예시는 상품 가격을 30,000원에서 27,000원으로 내렸는데 일부 화면에는 30,000원이 계속 보이는 상황이다.
가격 표시만 오래된 것과 결제 금액까지 오래된 것은 완전히 다른 사고다.
이 runbook은 읽기 성능을 높였던 선택이 정합성, 장애 격리, 운영 복잡도로 어떻게 되돌아오는지 다룬다.
- 성능: 캐시 hit rate와 origin 부하를 조절한다.
- 정합성: 어떤 화면에서 얼마나 오래된 값을 허용할지 정한다.
- 장애 격리: 캐시 장애가 DB 장애로 번지지 않게 한다.
- 운영 복잡도: key 추적, invalidation, rebuild, 검증 절차가 필요하다.
먼저 분류할 것
정합성 사고는 같은 말로 뭉뚱그리면 대응이 틀린다.
다음 세 가지를 구분한다.
| 분류 | 의미 | 즉시 위험 |
|---|---|---|
| stale value | 예전에는 맞았지만 지금은 오래된 값이다. | 가격, 재고, 상태 전환에서 고객 혼란이 생긴다. |
| wrong value | 애초에 잘못 계산되었거나 다른 객체 값이다. | 과금, 권한, 개인정보 문제로 커질 수 있다. |
| missing value | 값이 없어 fallback이나 default가 노출된다. | 품절, 권한 거절, 잘못된 추천으로 이어질 수 있다. |
가격이나 권한은 stale 허용 시간이 매우 짧다.
리뷰 수, 랭킹, 추천 문구는 더 긴 stale을 허용할 수 있다.
정합성 수준은 데이터 종류별로 정해야 한다.
초동 대응 순서
첫 단계는 캐시 삭제가 아니라 최종 진실의 원천 확인이다.
DB, 검색 색인, 외부 상품 관리 시스템, admin 입력값 중 무엇이 기준인지 먼저 정한다.
두 번째는 영향 key 범위를 찾는다.
전체 삭제는 마지막 수단이다.
세 번째는 사용자에게 더 나쁜 값을 막는 것이다.
가격이나 권한처럼 위험한 값은 캐시 우회나 기능 제한을 먼저 선택할 수 있다.
네 번째는 재생성 방법을 정한다.
삭제 후 miss storm이 생기면 DB가 무너지고 사고가 커진다.
다섯 번째는 검증이다.
DB 값, 캐시 값, API 응답, 화면 노출 값이 같은 기준으로 회복되었는지 비교해야 한다.
key 추적 예시
product:{productId}:summary:v3
product:{productId}:price:v2
product:{productId}:availability:v1key 이름은 운영자가 읽을 수 있어야 한다.
product:123 하나에 가격, 재고, 설명, 권한을 모두 넣으면 일부 값만 갱신하기 어렵다.
version suffix는 schema 변경과 rebuild를 분리하는 데 유용하다.
다만 version을 올릴 때 old key 만료 기간과 메모리 증가를 함께 계산해야 한다.
invalidation 판단
| 선택 | 장점 | 비용 | 적합한 상황 |
|---|---|---|---|
| 개별 key 삭제 | 영향이 작다. | key를 정확히 알아야 한다. | 특정 상품, 특정 사용자 문제 |
| prefix 기반 삭제 | 범위를 빠르게 줄인다. | 실수하면 넓게 날린다. | 이벤트 상품 묶음 |
| TTL 단축 | 점진적으로 회복된다. | stale이 일정 시간 남는다. | 위험도가 낮은 조회값 |
| cache bypass | 정합성을 즉시 회복한다. | origin 부하가 급증한다. | 가격, 권한, 결제 직전 확인 |
| version bump | 새 key로 안전하게 갈아탄다. | old key와 메모리 관리가 필요하다. | key schema 오류 |
정합성 사고에서는 빠른 회복과 origin 보호가 충돌한다.
따라서 모두 삭제보다 위험한 값만 우회하고 나머지는 TTL로 회복이 더 나은 경우가 많다.
cache bypass의 선
cache bypass는 강력하지만 위험하다.
모든 요청을 DB로 보내면 정합성은 회복되어도 성능 장애가 생긴다.
따라서 bypass는 다음처럼 좁혀야 한다.
- 특정 product id
- 특정 tenant
- 결제 직전 검증 API
- admin 변경 직후 5분
- 재고가 임계값 아래인 상품
운영 feature flag도 범위를 가져야 한다.
cache.off=true보다 priceCache.bypass.productIds=[...]가 안전하다.
write path 확인
캐시 정합성 문제는 읽기 경로보다 쓰기 경로에서 시작하는 경우가 많다.
admin에서 가격을 바꾼 뒤 DB commit 전에 invalidation event가 발행되면 캐시는 다시 옛값으로 채워질 수 있다.
DB commit은 성공했지만 event publish가 실패하면 캐시는 계속 오래된 값을 가진다.
outbox pattern이나 transaction 이후 publish 기준을 검토해야 하는 이유다.
수정 경로가 여러 개면 더 위험하다.
admin, batch, partner API가 같은 상품 값을 바꾼다면 각 경로가 같은 invalidation 규칙을 쓰는지 확인한다.
stampede 방지
캐시를 지운 뒤 모든 요청이 동시에 origin으로 몰리는 현상을 막아야 한다.
다음 선택지를 조합한다.
single_flight = same key rebuild is shared
jittered_ttl = ttl plus random spread
soft_ttl = stale served while one worker refreshes
negative_cache = not_found cached brieflysingle flight는 같은 key 재생성을 하나로 묶는다.
jittered TTL은 많은 key가 동시에 만료되는 일을 줄인다.
soft TTL은 조금 오래된 값을 주는 대신 origin을 보호한다.
negative cache는 없는 상품 조회가 DB를 반복해서 때리는 일을 줄인다.
검증 체크
복구 선언 전에는 네 곳을 비교한다.
DB row의 현재 값, Redis key의 값, API 응답, 실제 화면 노출이다.
중간에 CDN이나 application local cache가 있다면 그것도 포함한다.
가격 사고에서는 결제 직전 금액과 영수증 금액까지 확인한다.
권한 사고에서는 다른 사용자 데이터가 보였는지 access log로 확인한다.
검증이 끝나기 전에는 복구됨이 아니라 완화됨으로 기록한다.
재발 방지 설계
사고 후에는 TTL을 줄이는 것만으로 끝내지 않는다.
TTL을 줄이면 정합성은 나아질 수 있지만 origin 부하와 비용이 늘어난다.
오히려 데이터별 정책 분리가 더 중요할 수 있다.
가격은 짧은 TTL과 write-through invalidation을 쓰고, 리뷰 요약은 긴 TTL과 비동기 rebuild를 쓸 수 있다.
재고는 캐시로 표시하더라도 주문 확정은 DB나 재고 서비스에서 다시 검증한다.
위험 신호
- 최종 진실의 원천을 묻기 전에 Redis부터 삭제한다.
- key에 버전, tenant, 데이터 종류가 드러나지 않는다.
- 가격, 권한, 추천을 같은 TTL로 관리한다.
- invalidation 실패를 볼 metric이나 log가 없다.
- cache miss 증가가 DB 부하로 번지는지 보지 않는다.
- stale 허용 시간이 API 계약이나 화면 문구에 없다.
- old key 정리 계획 없이 version만 계속 올린다.
확인 질문
확인 질문
- 확인 질문: 캐시 정합성 사고에서 가장 먼저 확인할 것은 무엇인가?
- 답변: 사용자가 본 값의 위험도와 최종 진실의 원천이다.
- 확인 질문: 이 runbook이 바꾸는 설계 축은 무엇인가?
- 답변: 읽기 성능을 얻는 대신 정합성 정책, 장애 격리, key 운영 복잡도를 명시하게 만든다.
- 확인 질문: 전체 캐시 삭제가 위험한 이유는 무엇인가?
- 답변: origin 부하와 miss storm으로 성능 장애가 커지고, 어떤 값이 잘못되었는지 증거도 잃을 수 있기 때문이다.