매물 오퍼 가격 조회 API에 읽기가 몰려서 Redis 캐시를 붙이기로 했다. 그런데 회의에서 "쓸 때마다 캐시도 같이 갱신하면 항상 최신 아니냐"(write-through), "아예 캐시로 쓰기를 받고 DB엔 모아서 내리자"(write-behind)는 말까지 나왔다. 읽기 경로, 쓰기 경로, 그리고 무효화를 TTL로 할지 이벤트로 할지 정해야 하는 상황.
언제: 거의 항상 이게 시작점이다. 읽기가 쓰기보다 압도적으로 많고, 몇 초~몇 분 오래된 값을 보여줘도 비즈니스가 괜찮다고 할 때. 캐시가 통째로 날아가도 DB에서 읽어 다시 채우면 되니까 장애 시나리오가 단순하고, 무효화도 '시간 지나면 알아서 사라짐'이라 코드가 제일 적다.
비용: TTL 동안은 stale 읽기를 감수해야 하고, 미스난 첫 요청은 DB까지 내려가서 느리다. 인기 키가 만료되는 순간 요청이 한꺼번에 DB로 몰리는 스탬피드도 따로 막아야 한다(single-flight 잠금, TTL 지터).
언제: write-through는 오퍼 가격처럼 방금 바꾼 값이 바로 읽혀야 할 때 — 쓰기 경로에서 캐시까지 같이 갱신해 미스와 stale을 동시에 없앤다. write-behind는 설비 센서 카운터처럼 쓰기가 폭주하는데 유실돼도 재집계로 복구되는 데이터일 때 — 캐시에 먼저 쓰고 DB엔 모아서 내린다. 이벤트 무효화는 변경이 드물지만 바뀌는 즉시 화면에 반영돼야 할 때.
비용: write-through는 모든 쓰기가 캐시·DB 두 곳을 거쳐 느려지고, 한쪽만 성공하는 부분 실패 처리가 새로 생긴다. write-behind는 캐시가 죽으면 아직 DB에 안 내려간 쓰기가 그대로 유실된다 — 돈·오퍼 상태 데이터엔 못 쓴다. 이벤트 무효화는 무효화 메시지 유실·순서 꼬임이라는 새 버그 클래스를 평생 안고 간다.
기본은 cache-aside에 짧은 TTL이다. 틀려도 TTL만큼만 틀리고, 캐시 장애가 데이터 유실로 이어지지 않는 유일한 조합이기 때문이다. "방금 바꾼 값이 아직 옛날로 보인다"는 불만이 실제로 들어오는데 TTL을 더 줄이면 히트율이 무너지는 지점에 오면, 그 키에만 이벤트 무효화나 write-through를 얹는다. write-behind는 쓰기 폭주가 실측으로 DB를 누르고 있고 유실돼도 복구 가능한 집계성 데이터일 때만 꺼내는 마지막 카드다.
- TTL을 60초에서 5초까지 줄였는데도 '방금 수정한 오퍼 가격이 옛날 값으로 보여요' 문의가 주 3건 이상 — TTL로는 못 맞추는 신선도 요구라는 신호, 그 키만 이벤트 무효화로 전환
- 인기 키 만료 순간마다 DB QPS가 평소의 3배로 튀는 스탬피드가 하루에도 몇 번씩 반복 — single-flight 잠금을 넣거나, 그 키는 write-through로 미스 자체를 없앤다
- 쓰기가 초당 5천 건인데 대부분이 같은 키의 카운터 증가(센서 집계류)라서 DB 쓰기 p99가 100ms를 넘김 — write-behind 후보, 단 유실돼도 재집계되는 데이터만
- write-behind를 이미 쓰고 있다면: flush 큐 적체가 1만 건을 넘거나 flush 지연이 30초 이상 — 캐시 장애 한 번에 유실될 쓰기 폭이 그만큼 커져 있다는 뜻
🔧Redis (SET EX + SETNX single-flight)Caffeine (로컬 L1)Kafka / Redis pub-sub (이벤트 무효화)Debezium CDC (DB 변경 → 무효화 이벤트)Grafana (hit rate, DB QPS, flush 큐 깊이)
“I default to cache-aside with a short TTL; I only add event invalidation when stale reads actually hurt.”
checking microphone…