상품 가격을 DB에서 9000원으로 바꿨는데 사용자 화면엔 한참 동안 10000원이 보입니다. 캐시에 남은 옛 값(stale)이 갱신되지 않은 것입니다. "컴퓨터 과학에서 어려운 두 가지는 캐시 무효화와 이름 짓기"라는 말이 있을 만큼, 캐시를 언제·어떻게 비우느냐는 까다롭습니다. 캐시 무효화 전략은 DB가 바뀌었을 때 캐시를 정합성 있게 맞추는 규칙입니다.
세 가지 기본 접근
| 전략 | 쓰기 시 동작 | 장점 | 약점 |
|---|---|---|---|
| Cache-aside + invalidate | DB 갱신 후 캐시 키 삭제 | 단순, 가장 흔함 | 다음 읽기 때 미스 발생 |
| Write-through | DB와 캐시를 같이 갱신 | 읽기 즉시 최신 | 쓰기 경로 복잡·느림 |
| TTL 만료 | 일정 시간 뒤 자동 폐기 | 코드 단순 | TTL 동안 stale 허용 |
실무에서 가장 널리 쓰는 기본형은 읽을 때 캐시를 채우고(cache-aside), 쓸 때는 갱신이 아니라 삭제(invalidate) 하는 조합입니다. 캐시를 직접 새 값으로 덮으면 동시 쓰기 사이에 오래된 값이 끼어들 수 있어, 삭제 후 다음 읽기에서 다시 채우는 쪽이 어긋남이 적습니다.
# 쓰기: DB 먼저, 그다음 캐시 삭제
db.update_price(product_id, 9000)
cache.delete(f"product:{product_id}")
순서와 경쟁 조건
순서를 거꾸로 해 캐시를 먼저 지우고 DB를 갱신하면, 그 틈에 다른 요청이 읽기를 하며 옛 DB 값을 다시 캐시에 채워 넣는 경쟁 조건이 생깁니다. 그래서 "DB 갱신 → 캐시 삭제" 순서를 지키고, 더 엄격히는 삭제를 한 번 더 거는 지연 이중 삭제(delayed double delete)를 씁니다.
진단할 때는 이렇게 봅니다.
- stale가 보이면 먼저 TTL이 지나치게 긴지 확인합니다. 가격처럼 정합성이 중요한 데이터에 하루 TTL은 위험합니다.
- 쓰기 코드에 캐시 삭제 호출이 빠졌는지 봅니다. 가장 흔한 원인입니다.
- 삭제는 하는데 DB보다 먼저 지우는 순서 버그가 아닌지 봅니다.
TTL은 안전망으로 둔다
invalidate가 누락되거나 실패할 수 있으므로, TTL을 함께 둬서 최악의 경우에도 일정 시간 뒤엔 stale가 사라지게 합니다. 정합성 요구가 강하면 짧게(수십 초~수 분), 거의 안 바뀌는 데이터면 길게 둡니다. 또 대량 키가 동시에 만료돼 DB로 요청이 쏠리는 캐시 스탬피드를 막으려면 TTL에 약간의 무작위 편차(jitter)를 더합니다.
import random
ttl = 300 + random.randint(0, 60) # 5분 ± jitter
cache.setex(key, ttl, value)
요점 정리
- 기본은 cache-aside + 쓰기 시 삭제(덮어쓰기 아님).
- 순서는 항상 "DB 갱신 → 캐시 삭제", 경쟁 조건엔 지연 이중 삭제.
- TTL은 invalidate 누락에 대비한 안전망으로 두고, 정합성 요구에 맞춰 길이를 정한다.
- 동시 만료 쏠림은 TTL jitter로 완화한다.
캐시와 DB 정합성을 직접 깨뜨려 보고 고치는 실습은 데이터베이스 트랙에서 회원가입 없이 무료로 해볼 수 있습니다.