Zettelkasten

Pub:Sub 기반 캐시 무효화는 TTL을 fallback으로 함께 둬야 한다

·수정 2026.05.01·수정 1

요약

  • 프로세스 로컬 캐시를 Pub/Sub로 무효화할 때, 메시지 유실을 가정하고 TTL 만료를 항상 함께 둔다.
  • Pub/Sub은 "즉시성", TTL은 "최종 일관성"을 담당하는 이중 안전망 구조다.
  • Redis Pub/Sub은 at-most-once 보장이라 구독자 다운/네트워크 단절 구간의 메시지는 영원히 유실된다.

본문

문제 상황 django-waffle 같은 feature flag를 매 요청마다 DB에서 조회하면 부하가 크다. 그래서 워커 프로세스마다 인메모리 dict 캐시를 둔다. 그런데 어드민에서 스위치를 OFF로 바꿨을 때, 모든 워커의 로컬 캐시를 어떻게 즉시 무효화할 것인가?

나이브한 두 접근의 한계

접근 장점 한계
TTL만 사용 단순. 메시지 유실 걱정 없음 스위치 변경 후 최대 TTL만큼 stale value 사용
Pub/Sub만 사용 즉시 전파 구독자가 다운/재연결 중일 때 메시지 영구 유실 → 영원히 stale

해결: 둘 다 둔다

┌──────────────────────────────────────────────────┐
│  변경 발생 → publish (즉시 전파, best-effort)    │
│                    │                             │
│                    ▼ 유실 시                      │
│  TTL 만료 → 다음 조회 시 lazy reload (안전망)    │
└──────────────────────────────────────────────────┘
  • Pub/Sub = 즉시성. 정상 경로에선 거의 실시간 반영.
  • TTL = 최종 일관성. 메시지 유실되어도 결국 TTL 안에 복구.

Redis Pub/Sub은 at-most-once다

Redis Pub/Sub은 메시지 큐가 아니라 fire-and-forget 브로드캐스트다.

  • 구독자가 그 순간 연결되어 있지 않으면 메시지는 그대로 사라진다.
  • Redis Streams나 Kafka처럼 offset 기반 재처리가 없다.
  • 따라서 "Pub/Sub만으로 캐시를 무효화한다"는 설계는 구독자 재시작 구간에서 stale 보장.

TTL 길이는 수용 가능한 stale 시간

이중 안전망 구조에서 TTL은 "최악의 경우 얼마나 오래 stale 값을 받아도 되는가"의 답이다.

  • feature flag: 60분 (잘못 켜진 기능이 1시간 후엔 자동 차단)
  • 가격 정보: 분 단위
  • 권한 정보: 초~분 단위 (보안 민감)

적용 패턴

# 1. 변경 시 publish
@receiver(post_save, sender=Switch)
def on_change(instance, **kwargs):
    pubsub.publish("update", instance.name, instance.active)

# 2. 구독자는 set/delete
def handle(message):
    if action == "update":
        local_cache.set(name, value)
    elif action == "delete":
        local_cache.delete(name)

# 3. 조회 시 TTL 검사
def get(name):
    entry = cache.get(name)
    if not entry or entry.expired():  # ← TTL fallback
        return None
    return entry.value

왜 "Pub/Sub 메시지로 새 값 주입"인가

이 패턴에선 publish 메시지에 변경된 값까지 실어 보낸다({"action": "update", "value": True}). 구독자가 DB를 다시 조회할 필요가 없다.

  • 장점: DB 부하 0. 모든 워커가 동시에 DB로 몰리는 cache stampede 회피.
  • 단점: 메시지의 value가 source-of-truth와 다를 가능성(경합) → TTL fallback이 이걸 결국 교정.

대안 비교

  • Cache-aside만: 매 요청 DB 조회 → 부하
  • Write-through + 분산 캐시(Redis GET): 단일 Redis 의존, 매 조회 네트워크 호출 → 로컬 캐시보다 느림
  • Polling (주기적 SELECT): 간단하지만 N개 워커 × 주기 = DB 부하 선형 증가
  • Pub/Sub + TTL (이 패턴): 정상 시 즉시 반영, 장애 시 TTL로 자가 치유, DB 부하 최소

참고