
캐시를 붙이면 빨라진다. 그런데 캐시를 붙이고 나면 새로운 문제가 생긴다. 데이터가 갱신됐는데 옛날 값이 보이고, 캐시가 만료되는 순간 데이터베이스가 몰려드는 요청에 쓰러진다. 이 글은 캐시를 도입할 때 반드시 결정해야 하는 무효화 전략, 만료 설계, 동시 만료 대응 세 가지를 정리한다.
읽기 캐시의 세 가지 패턴
먼저 어디에 캐시를 두고 누가 채우는지를 정해야 한다.
Cache-Aside(Lazy Loading)가 가장 흔하다. 애플리케이션이 캐시를 먼저 보고, 없으면 데이터베이스에서 읽어 캐시에 넣는다.
def get_user(uid):
v = cache.get(f"user:{uid}")
if v is not None:
return v
v = db.query_user(uid)
cache.set(f"user:{uid}", v, ttl=300)
return v
장점은 단순하고, 실제로 요청된 것만 캐시된다는 것이다. 단점은 첫 요청이 항상 느리다는 것과, 캐시와 데이터베이스가 어긋날 수 있다는 것이다.
Write-Through는 쓸 때 캐시와 데이터베이스를 함께 갱신한다. 일관성이 좋지만 쓰기가 느려지고, 읽히지 않을 데이터까지 캐시에 쌓인다.
Write-Behind는 캐시에 쓰고 나중에 비동기로 데이터베이스에 반영한다. 가장 빠르지만 장애 시 데이터가 날아갈 수 있다. 유실을 감당할 수 있는 데이터에만 쓴다.
대부분의 웹 서비스에서는 Cache-Aside로 시작하는 것이 맞다. 나머지는 특정 문제가 생겼을 때 고려한다.
무효화는 삭제가 갱신보다 안전하다
데이터가 바뀌었을 때 캐시를 어떻게 할 것인가. 두 방법이 있다.
# 방법 1: 갱신
db.update_user(uid, data)
cache.set(f"user:{uid}", data, ttl=300)
# 방법 2: 삭제
db.update_user(uid, data)
cache.delete(f"user:{uid}")
삭제가 더 안전하다. 이유는 동시성이다. 두 요청이 거의 동시에 갱신하면, 캐시에 쓰는 순서가 데이터베이스에 쓰는 순서와 뒤바뀔 수 있다. 그러면 캐시에 오래된 값이 영구히 남는다. 삭제 방식은 다음 읽기에서 데이터베이스를 다시 읽으므로 이 문제가 없다.
순서도 중요하다. 데이터베이스를 먼저 쓰고 캐시를 지운다. 반대로 하면, 캐시를 지운 직후 데이터베이스 쓰기 전에 다른 요청이 들어와 옛 값을 다시 캐시에 넣을 수 있다.
그래도 완벽하지는 않다. 캐시 삭제가 실패하면 낡은 값이 남는다. 그래서 TTL은 항상 함께 둔다. 무효화가 실패해도 TTL이 지나면 자동으로 사라진다. TTL은 최후의 안전망이다.
동시 만료가 데이터베이스를 무너뜨린다
인기 있는 키의 캐시가 만료되는 순간, 그 키를 찾던 요청 수천 개가 동시에 데이터베이스로 간다. 캐시 스탬피드(cache stampede) 또는 dogpile 문제다.

대응은 세 가지다.
TTL에 무작위 값을 더한다. 300초 대신 300초에서 360초 사이로 흩어 놓으면 동시 만료가 사라진다. 가장 쉽고 효과가 크다.
ttl = 300 + random.randint(0, 60)
한 요청만 갱신하게 한다. 캐시 미스가 났을 때 분산 락을 잡고, 락을 못 잡은 요청은 잠시 기다렸다 다시 캐시를 본다. 데이터베이스로 가는 것은 한 요청뿐이다.
만료 전에 미리 갱신한다. TTL이 끝나기 전에 백그라운드로 새 값을 채운다. 사용자 요청은 항상 캐시 적중이 된다. 구현이 복잡하지만 효과가 확실하다.
무엇을 캐시하지 말아야 하는가
캐시를 붙이기 전에 확인할 것이 있다. 모든 것을 캐시할 필요는 없다.
자주 바뀌는 데이터는 캐시 효율이 낮다. 적중률이 낮으면 캐시 조회 비용만 추가된다. 사용자마다 다른 데이터도 마찬가지다. 사용자 백만 명의 개인화 데이터를 캐시하면 메모리는 쓰는데 적중률은 바닥이다.
정확성이 중요한 데이터는 신중해야 한다. 잔액, 재고 수량, 권한 정보는 낡은 값이 보이면 사고로 이어진다. 재고가 0인데 캐시에 10으로 남아 있으면 초과 판매가 된다. 이런 데이터는 캐시하지 않거나, TTL을 아주 짧게 잡고 결제 직전에는 반드시 원본을 확인한다.
반대로 캐시가 잘 맞는 것은 읽기가 압도적으로 많고, 잠깐 낡아도 괜찮은 데이터다. 상품 상세, 카테고리 목록, 설정값, 집계 통계 같은 것들이다.
먼저 쿼리를 고치는 것이 순서다
마지막으로 순서를 짚고 싶다. 느린 API에 캐시를 붙이는 것은 종종 문제를 덮는 일이 된다.
느린 원인이 인덱스를 안 타는 쿼리거나 N+1이라면, 그걸 고치는 것이 먼저다. 캐시를 붙이면 당장은 빨라지지만, 캐시가 안 듣는 경로(새 데이터, 캐시 장애, 배포 직후)에서 원래의 느림이 그대로 드러난다. 그때는 트래픽이 몰려 있으므로 더 나쁜 상황이 된다.

참고 자료
- Redis: Client-side caching — 캐시 무효화 방식과 일관성 고려사항
- AWS: Caching best practices — Cache-Aside, Write-Through 등 패턴 비교
- PostgreSQL: Using EXPLAIN — 캐시 전에 쿼리부터 확인할 때 쓰는 도구
'IT' 카테고리의 다른 글
| 구조화 로깅, 장애 때 찾을 수 있는 로그 남기기 (0) | 2026.10.15 |
|---|---|
| N+1 쿼리 문제, 발견하고 고치는 법 (0) | 2026.10.13 |
| 인덱스를 만들었는데 쿼리가 느린 이유 (0) | 2026.10.12 |
| AI 코딩 에이전트 시대의 코드 리뷰 (0) | 2026.10.11 |
| Git 브랜치 전략, rebase와 merge 중 무엇을 쓸까 (0) | 2026.10.10 |
