데이터베이스 조회는 느리다. 같은 데이터를 반복해서 읽는 경우, 매번 DB를 거치는 것은 낭비다. Caching은 자주 읽히는 데이터를 빠른 저장소(메모리)에 임시로 두어 응답 속도를 높이는 전략이다. 전략마다 읽기·쓰기 경로가 다르고, 일관성과 성능 사이의 트레이드오프도 다르다.
가장 널리 쓰이는 패턴이다. 애플리케이션이 캐시를 직접 관리한다.
[Client]
│
▼
[App] ──→ Cache Hit? ──Yes──→ return
│ │No
│ ▼
└────→ [DB] ──→ write to Cache ──→ return
읽기 요청이 들어오면 캐시를 먼저 확인(Cache Hit)하고, 없으면(Cache Miss) DB에서 읽어 캐시에 저장한다.
Cache Hit: 캐시에 데이터가 존재하는 경우. Cache Miss: 캐시에 없어 원본 저장소를 조회해야 하는 경우.
장점: DB 장애 시 캐시만으로 서비스 유지 가능. 실제로 읽힌 데이터만 캐시에 적재된다.
단점: 첫 요청은 항상 Cache Miss. 캐시와 DB 사이에 데이터 불일치가 생길 수 있다.
Cache Aside와 흐름은 같지만, 캐시 미스 시 캐시 레이어가 직접 DB를 조회한다. 애플리케이션은 항상 캐시만 바라본다.
[App] ──→ [Cache] ──Miss──→ [DB]
▲ │
└────────────────┘
(캐시가 자동 적재)
코드 단순화가 장점이지만, 캐시 제공자가 DB 접근을 지원해야 한다.
데이터를 쓸 때 캐시와 DB를 동시에 갱신한다.
[App] ──→ [Cache] ──→ [DB]
캐시와 DB가 항상 동기화되므로 일관성이 높다. 단, 쓰기 지연이 두 번 발생하고, 읽히지 않을 데이터도 캐시에 적재된다는 단점이 있다.
쓰기를 캐시에만 먼저 반영하고, DB 반영은 비동기로 지연한다.
[App] ──→ [Cache] ──→ (비동기) ──→ [DB]
쓰기 성능이 매우 높아지지만, 캐시 장애 시 미반영 데이터가 유실될 위험이 있다. 쓰기 빈도가 극히 높은 시스템(게임 점수, 클릭 수 집계 등)에 적합하다.
| 전략 | 읽기 경로 | 쓰기 경로 | 일관성 | 쓰기 성능 |
|---|---|---|---|---|
| Cache Aside | App → Cache → DB | App → DB (캐시 무효화) | 낮음 | 보통 |
| Read Through | App → Cache(→DB) | App → DB | 낮음 | 보통 |
| Write Through | App → Cache | App → Cache → DB | 높음 | 낮음 |
| Write Back | App → Cache | App → Cache (비동기 DB) | 중간 | 높음 |
캐시는 메모리 공간이 한정되므로 두 가지 메커니즘으로 항목을 제거한다.
TTL(Time To Live) 은 각 항목에 만료 시간을 부여한다. TTL이 지나면 자동으로 무효화되어 다음 요청에서 DB를 다시 조회한다.
await redis.set("user:42", JSON.stringify(user), { EX: 300 }); // 5분 TTL
Eviction Policy는 메모리가 가득 찼을 때 어떤 항목을 제거할지 결정한다.
| 정책 | 설명 |
|---|---|
| LRU (Least Recently Used) | 가장 오래 사용되지 않은 항목 제거 |
| LFU (Least Frequently Used) | 사용 빈도가 가장 낮은 항목 제거 |
| TTL-based | 만료 시간이 가장 짧게 남은 항목 우선 제거 |
LRU: 최근 사용 시각 기준. LFU: 누적 접근 횟수 기준. Redis의 기본값은 noeviction이며, allkeys-lru가 범용적으로 많이 쓰인다.
캐시 전략 선택의 핵심은 일관성과 성능 중 무엇을 우선하느냐다. 읽기가 많고 쓰기가 적다면 Cache Aside, 강한 일관성이 필요하다면 Write Through, 쓰기 처리량이 중요하다면 Write Back을 고려한다.