학습 목표
애플리케이션 캐시가 DB read 부하·connection·버퍼 풀 압력을 어떻게 줄이는지 설명할 수 있다.
Cache-aside·read-through·write-through 의 DB I/O·일관성 차이를 비교할 수 있다.
TTL·invalidation으로 stale read 창을 통제할 수 있다.
Cache stampede가 DB spike를 만드는 메커니즘과 완화를 설명할 수 있다.
DB = source of truth, 캐시 = 파생 — 복제 lag·캐시 중첩 stale을 설명할 수 있다.
문제 상황
- 상품 상세 API — PostgreSQL 동일
SELECT매 요청 · p99 380ms - Redis TTL 5분 — 가격 UPDATE 후 4분 구 가격 노출
- 캐시 무효화 없이 DB만 수정 — 영구 stale
- 프로모션 0시 — TTL 동시 만료 · DB connection pool 고갈
- write-through 도입 — 쓰기 p99 2배 · 읽기만 캐시면 됐음
- read replica + Redis — replica lag + cache TTL — 이중 stale
앞서 pool·ORM — 앱↔DB 연결·쿼리 최적화. 읽기를 DB 밖으로 빼는 캐시 계층과 DB 정합성을 본다.
1. DB 관점에서 캐시
캐시 — 자주 읽고 상대적으로 잘 안 바뀌는 데이터를 빠른 store(Redis·in-memory)에 복사.
| DB 없이 매번 | 캐시 hit 시 |
|---|---|
| Parser·plan·buffer pool·disk | 0 DB round-trip |
| Connection 점유 | pool 여유 |
| Replica·primary 부하 | 읽기 분산 |
Source of truth = PostgreSQL (primary)
Cache = disposable · rebuildable copy
Buffer pool = DBMS internal (앞서 디스크·페이지)
App cache = Redis / local — this episode
- Architecture 캐싱 전략 — MSA·Redis 배치; 여기서는 DB 부하·일관성
- NoSQL Redis — KV·TTL; RDB 앞 read-through 계층
2. Cache-aside (lazy loading)
Cache-aside — 앱이 cache·DB 순서 직접 제어. 가장 흔함.

Read
1. GET cache
2. hit → return (0 DB query)
3. miss → SELECT DB → SET cache + TTL → return
# code/cache-aside.example
def get_product(product_id: int):
key = f"catalog:product:{product_id}"
cached = redis.get(key)
if cached:
return json.loads(cached)
row = db.execute(
"SELECT id, name, price FROM products WHERE id = %s",
(product_id,),
).fetchone()
if row is None:
return None
payload = dict(row)
redis.setex(key, 300, json.dumps(payload))
return payload
| Hit | Miss | |
|---|---|---|
| DB | 없음 | 1 SELECT |
| 지연 | sub-ms | DB + cache write |
| 위험 | stale | miss burst → DB spike |
Write (일반)
1. UPDATE / DELETE in DB (source of truth)
2. DEL cache key (or version bump)
- Write 후 cache update보다 delete 안전 — partial field 실수 방지
- TX commit 후 invalidation — rollback 시 cache 지우면 불필요 miss
3. Read-through · Write-through

| 패턴 | DB 접점 | 일관성 | 적합 |
|---|---|---|---|
| Cache-aside | 앱이 각각 | delete+TTL 관리 | read-heavy 기본 |
| Read-through | miss 시 cache lib가 DB load | lib 의존 | 표준 wrapper |
| Write-through | write 동시 cache+DB | read-after-write 강 | 쓰기 적고 즉시 read |
| Write-behind | cache 먼저 · DB async | 손실·순서 리스크 | ingest burst |
Cache-aside write: COMMIT to PostgreSQL → DEL Redis key
Write-through: app → cache layer writes DB + cache together
- Write-through — DB write latency 그대로 + cache 오버헤드 — 남용 지양
- Write-behind — DB 장애 시 유실 — 원장 부적합
4. TTL과 invalidation
TTL — 자동 만료. invalidation 없어도 eventually 갱신.

| TTL only | Delete on write | |
|---|---|---|
| stale | 최대 TTL | commit 직후 없음 |
| DB load | 만료 시 spike | write 마다 miss 가능 |
| 적합 | 비핵심 · 통계 | 가격·재고 |
# code/cache-invalidation.example
def update_product_price(product_id: int, new_price: decimal.Decimal):
with db.transaction():
db.execute(
"UPDATE products SET price = %s WHERE id = %s",
(new_price, product_id),
)
# after COMMIT
redis.delete(f"catalog:product:{product_id}")
| 기법 | |
|---|---|
| Jitter | TTL ± random — 동시 만료 방지 |
| Version in key | product:42:v17 — immutable entry |
| CDC / outbox event | consumer DEL — multi-instance (CDC 편) |
| Negative cache | 없는 id 짧은 TTL — 반복 SELECT 방지 |
- Race — read miss가 commit 전 값 cache — 짧은 TTL + delete after commit
- Multi-key — product 상세·목록·검색 동시 — tag invalidation
5. Cache stampede
Cache stampede — 대량 miss·TTL 동시 만료 → DB 동일 쿼리 폭주.

| 증상 | DB에서 |
|---|---|
| Redis hit rate 급락 | 동일 SELECT 수백 |
| DB CPU 급등 | buffer pool·lock |
| Pool 대기 | 앞서 pool exhaustion |
# code/stampede-singleflight.example — one loader per key
_locks: dict[str, threading.Lock] = {}
def get_product_safe(product_id: int):
key = f"catalog:product:{product_id}"
cached = redis.get(key)
if cached:
return json.loads(cached)
lock = _locks.setdefault(key, threading.Lock())
with lock: # singleflight
cached = redis.get(key) # double-check
if cached:
return json.loads(cached)
row = load_from_db(product_id)
redis.setex(key, 300 + random.randint(0, 30), json.dumps(row))
return row
| 완화 | |
|---|---|
| TTL jitter | 만료 분산 |
| Singleflight | key당 1 loader |
| Probabilistic early refresh | 만료 전 백그라운드 reload |
| Prewarm | 배포·이벤트 전 cache 채움 |
| Mutex + short wait | miss 줄 서기 |
6. DB · replica · cache 일관성

| 계층 | 역할 | stale |
|---|---|---|
| Primary | source of truth | 없음 (TX 내) |
| Read replica | read 분산 | replication lag |
| Redis cache | hot read | TTL · invalidation 실패 |
| Local (Caffeine) | 초저지연 | pod마다 다름 |
Worst case: write primary → lag on replica → stale replica read
invalidate miss → old value in Redis until TTL
| 규칙 | |
|---|---|
| 강한 read | primary 또는 write-through |
| 약한 read | replica + cache · SLA 문서화 |
| Invalidate | primary commit 기준 |
| Cache down | fail open to DB · degraded 허용? |
- 앞서 replication lag — cache 없어도 stale · 겹치면 더 길어짐
- 앞서 MVCC — cache는 스냅샷 고정 — DB 버전 넘어섬
7. 버퍼 풀 vs 앱 캐시
| Buffer pool (DBMS) | App cache (Redis) | |
|---|---|---|
| 위치 | PostgreSQL 내부 | 앱 외부 |
| 단위 | page | object·JSON |
| 일관 | WAL·MVCC | 앱 책임 |
| 목적 | disk I/O ↓ | network·query ↓ |
- Buffer pool hit 높아도 parser·connection 남음 — app cache 여전히 유효
EXPLAIN (BUFFERS)— buffer vs app cache 별개 (실행 계획 편)
8. 체크리스트
| 항목 | 확인 |
|---|---|
| Truth | Primary only · cache 재구축 가능? |
| Pattern | read-heavy → cache-aside? |
| Write | COMMIT 후 DEL? |
| TTL | jitter · 도메인별 문서? |
| Stampede | singleflight · prewarm? |
| Replica | cache miss → 어느 DB? |
| Fail | Redis down → DB direct OK? |
| Metric | hit rate · miss latency · DB QPS 상관 |
9. 정리
- 캐시 — DB read·connection 보호 — 파생 복사
- Cache-aside — hit 0 query · miss 1 SELECT — delete on write
- Write-through — 일관 ↑ · DB write 비용 ↑
- TTL + invalidation — stale window 명시
- Stampede — DB spike — jitter·singleflight
- Primary = truth · replica·cache lag 겹침 주의
다음에 다룰 것
- CDC와 이벤트
- Debezium, outbox, 이벤트 순서
해당 내용은 Database System Concepts, 7/E (Avraham Silberschatz, Henry F. Korth, S. Sudarshan), Designing Data-Intensive Applications (Martin Kleppmann), Redis 공식 문서 의 내용을 기반으로 합니다.
'데이터베이스 따라잡기' 카테고리의 다른 글
| 슬로우 쿼리 진단 (0) | 2026.07.08 |
|---|---|
| CDC와 이벤트 (0) | 2026.07.07 |
| Connection Pool과 ORM (0) | 2026.07.05 |
| NoSQL 개요 (0) | 2026.07.04 |
| 분산 트랜잭션 개요 (0) | 2026.07.03 |