캐싱 전략: 다층 캐시 아키텍처와 Redis 고급 활용
Cache-Aside, Write-Through, 스탬피드 방지, Redis Cluster 등 캐싱 전략 총정리
Contents
개요
캐싱(Caching)은 자주 접근하는 데이터를 원본보다 빠른 저장소에 임시 보관하여 응답 시간을 단축하고 원본 데이터 소스의 부하를 줄이는 기법이다. 잘 설계된 캐싱 전략은 데이터베이스 쿼리를 90% 이상 줄이고, 응답 시간을 수십 배 개선할 수 있다.
2026년 현재, Redis는 여전히 인메모리 캐시의 표준이며, 다층 캐시 아키텍처(Multi-Level Caching)와 엣지 캐싱(Edge Caching)을 결합한 전략이 주류이다. 캐시 스탬피드(Cache Stampede) 방지, 캐시 일관성(Cache Consistency) 유지가 대규모 시스템 설계의 핵심 과제로 남아 있다.
다층 캐시 아키텍처 (Multi-Level Caching)
캐시 계층 구조
사용자 요청
│
▼
L0: CDN / Edge Cache ← 글로벌 엣지 (지연 시간 < 50ms)
│ MISS
▼
L1: Reverse Proxy Cache ← NGINX, Varnish
│ MISS
▼
L2: API Gateway Cache ← Kong, AWS API Gateway
│ MISS
▼
L3: Application In-Process ← Node.js Map, Caffeine (JVM)
│ MISS
▼
L4: Distributed Cache ← Redis, Memcached
│ MISS
▼
L5: Primary Datastore ← PostgreSQL, MongoDB각 계층의 특성
| 계층 | 저장소 | 지연 시간 | 용량 | 범위 |
|---|---|---|---|---|
| L0 (CDN) | Cloudflare, CloudFront | < 50ms (글로벌) | 대규모 | 전 세계 |
| L1 (Reverse Proxy) | NGINX, Varnish | < 1ms | 중간 | 서버 단위 |
| L3 (In-Process) | 로컬 메모리 | < 0.1ms | 소규모 | 프로세스 단위 |
| L4 (Distributed) | Redis Cluster | < 5ms | 대규모 | 서비스 전체 |
| L5 (DB) | PostgreSQL | 10-100ms | 무제한 | - |
다층 캐시의 과제
스탬피드 이중 발생: In-Process 캐시(L3)와 Redis(L4)가 동시에 만료되면, 모든 인스턴스가 동시에 Redis를 거쳐 DB에 접근하는 이중 스탬피드가 발생할 수 있다.
해결: L3과 L4의 TTL을 다르게 설정하고, L4의 TTL을 L3보다 길게 유지한다.
L3 TTL: 30초 (빠른 만료, 자주 갱신)
L4 TTL: 300초 (L3 미스 시 안전망)캐싱 패턴 (Caching Patterns)
1. Cache-Aside (Lazy Loading)
가장 널리 사용되는 패턴. 애플리케이션이 캐시와 데이터베이스를 직접 관리한다.
읽기:
1. 캐시에서 조회 (GET key)
2. HIT → 캐시 데이터 반환
3. MISS → DB에서 조회 → 캐시에 저장 (SET key value EX ttl) → 데이터 반환
쓰기:
1. DB에 쓰기
2. 캐시 무효화 (DEL key)async function getUser(userId: string): Promise<User> {
// 1. 캐시 조회
const cached = await redis.get(`user:${userId}`);
if (cached) {
return JSON.parse(cached);
}
// 2. DB 조회
const user = await db.users.findById(userId);
if (user) {
// 3. 캐시에 저장 (TTL 5분)
await redis.set(`user:${userId}`, JSON.stringify(user), 'EX', 300);
}
return user;
}장점: 필요한 데이터만 캐싱, 캐시 장애 시 DB로 폴백 가능 단점: 첫 요청은 항상 느림(Cold Start), 캐시와 DB 간 불일치 가능
2. Read-Through
캐시 자체가 DB 접근을 관리한다. 애플리케이션은 캐시만 바라본다.
읽기:
1. 캐시에 요청
2. HIT → 반환
3. MISS → 캐시가 자체적으로 DB 조회 → 저장 → 반환- 장점: 애플리케이션 로직 단순화
- 단점: 캐시 라이브러리/미들웨어가 DB 접근 로직을 포함해야 함
3. Write-Through
데이터가 캐시와 DB에 동시에 기록된다.
쓰기:
1. 캐시에 쓰기
2. 캐시가 DB에 쓰기 (동기)
3. 양쪽 모두 완료 후 응답장점: 캐시와 DB가 항상 일관성 유지 단점: 쓰기 지연 시간 증가 (두 번 쓰기), 불필요한 데이터도 캐싱
4. Write-Back (Write-Behind)
데이터를 캐시에만 즉시 기록하고, DB에는 비동기로 나중에 기록한다.
쓰기:
1. 캐시에 쓰기 → 즉시 응답
2. 백그라운드에서 DB에 쓰기 (배치 또는 지연)장점: 매우 빠른 쓰기 성능, DB 부하 분산 단점: 데이터 손실 위험 (캐시 장애 시 미동기화 데이터 유실)
패턴 비교
| 패턴 | 읽기 성능 | 쓰기 성능 | 일관성 | 데이터 손실 위험 | 복잡도 |
|---|---|---|---|---|---|
| Cache-Aside | 좋음 | 보통 | 보통 | 낮음 | 낮음 |
| Read-Through | 좋음 | 보통 | 보통 | 낮음 | 보통 |
| Write-Through | 좋음 | 느림 | 높음 | 매우 낮음 | 보통 |
| Write-Back | 좋음 | 매우 빠름 | 낮음 | 높음 | 높음 |
캐시 무효화 (Cache Invalidation)
"컴퓨터 과학에서 어려운 것은 딱 두 가지: 캐시 무효화와 이름 짓기." - Phil Karlton
TTL (Time-To-Live) 기반
캐시 항목에 만료 시간을 설정하여 자동 무효화한다.
SET user:123 '{"name":"홍길동"}' EX 300 # 5분 후 자동 만료TTL 설정 가이드:
- 자주 변경되는 데이터: 짧은 TTL (30초 ~ 5분)
- 거의 변경되지 않는 데이터: 긴 TTL (1시간 ~ 24시간)
- 변경 시 즉시 무효화 필요: TTL + 이벤트 기반 무효화
이벤트 기반 무효화
데이터 변경 시 명시적으로 캐시를 삭제하는 방식이다.
async function updateUser(userId: string, data: UpdateUserDto) {
// 1. DB 업데이트
const updatedUser = await db.users.update(userId, data);
// 2. 캐시 무효화
await redis.del(`user:${userId}`);
// 3. 관련 캐시도 무효화
await redis.del(`user-profile:${userId}`);
await redis.del(`user-orders:${userId}`);
return updatedUser;
}삭제 vs 갱신
| 전략 | 동작 | 장점 | 단점 |
|---|---|---|---|
| 삭제 (Invalidate) | 캐시 키 삭제, 다음 조회 시 재캐싱 | 단순, 안전 | 다음 요청 시 DB 히트 |
| 갱신 (Update) | 캐시 값을 새 데이터로 직접 교체 | DB 히트 없음 | 경합 상태(Race Condition) 위험 |
권장: 대부분의 경우 삭제 전략이 더 안전하다.
캐시 스탬피드 방지 (Cache Stampede Prevention)
문제
인기 있는 캐시 키가 만료되는 순간, 수백~수천 개의 동시 요청이 모두 DB를 직접 조회하여 DB가 과부하에 빠지는 현상이다. "Thundering Herd" 또는 "Dog-Pile Effect"라고도 한다.
캐시 만료 → 동시 요청 1000개 → DB에 1000개 동일 쿼리 → DB 과부하방지 전략
1. 분산 잠금 (Distributed Locking)
하나의 요청만 DB를 조회하고, 나머지는 잠금이 해제될 때까지 대기한다.
async function getWithLock(key: string): Promise<any> {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const lockKey = `lock:${key}`;
// SET NX (존재하지 않을 때만 설정) - 원자적 잠금
const acquired = await redis.set(lockKey, '1', 'EX', 10, 'NX');
if (acquired) {
try {
const data = await fetchFromDatabase(key);
await redis.set(key, JSON.stringify(data), 'EX', 300);
return data;
} finally {
await redis.del(lockKey);
}
} else {
// 잠금 대기 후 재시도
await sleep(50);
return getWithLock(key);
}
}단순 잠금 키로 스탬피드를 방지하면 피크 시 DB 부하를 30-70% 절감할 수 있다.
2. 확률적 조기 만료 (Probabilistic Early Expiration)
TTL이 만료되기 전에 확률적으로 미리 갱신을 수행한다.
XFetch 알고리즘은 캐시 만료 전에 확률적으로 갱신을 시도하여 Stampede를 예방한다. delta는 캐시 재계산에 걸리는 시간이고, beta는 조기 갱신의 적극성을 조절하는 파라미터이다.
function shouldRefresh(ttlRemaining: number, delta: number, beta: number = 1.0): boolean {
// XFetch 알고리즘
const random = -delta * beta * Math.log(Math.random());
return ttlRemaining < random;
}
// ttlRemaining이 적을수록 갱신 확률이 높아짐
// delta: 캐시 재계산에 소요되는 시간
// beta: 조기 만료 정도 조절 (기본 1.0)3. Singleflight / Request Coalescing
동일한 키에 대한 동시 요청을 하나로 합쳐서 DB에 한 번만 조회한다.
// Go의 singleflight 패턴
group := &singleflight.Group{}
result, err, shared := group.Do("user:123", func() (interface{}, error) {
return fetchFromDatabase("user:123")
})
// shared == true이면 다른 호출자와 결과를 공유한 것4. 스탬피드 방지 전략 비교
| 전략 | 구현 복잡도 | 효과 | 적합한 상황 |
|---|---|---|---|
| 분산 잠금 | 보통 | 높음 | 대부분의 경우 |
| 확률적 조기 만료 | 보통 | 높음 | TTL이 긴 캐시 |
| Singleflight | 낮음 | 높음 | 같은 프로세스 내 동시 요청 |
| 캐시 워밍 | 낮음 | 보통 | 예측 가능한 패턴 |
Redis 고급 활용
Redis Cluster
데이터를 16,384개의 해시 슬롯에 분배하여 수평 확장한다.
슬롯 0-5460 → Node A (+ Replica A')
슬롯 5461-10922 → Node B (+ Replica B')
슬롯 10923-16383 → Node C (+ Replica C')Redis Sentinel
**고가용성(HA)**을 위한 모니터링 및 자동 장애 조치(Failover) 시스템이다.
[Sentinel 1] [Sentinel 2] [Sentinel 3]
│ │ │
└────────────┼────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
[Master] [Replica 1] [Replica 2]
Master 장애 감지 → Sentinel 투표 → Replica 1 승격 → 클라이언트 리다이렉트Redis 고급 데이터 구조
| 데이터 구조 | 활용 사례 | 예시 |
|---|---|---|
| Sorted Set | 실시간 리더보드, 랭킹 | ZADD leaderboard 1000 "player1" |
| HyperLogLog | 고유 방문자 수 추정 (UV) | 0.81% 오차로 2^64개까지 카운트, 12KB 메모리 |
| Stream | 이벤트 스트리밍, 메시지 큐 | Kafka 대안 (경량) |
| Bitmap | 사용자 활동 추적 (일별 로그인) | SETBIT daily:login:2026-02-10 userId 1 |
| Geospatial | 위치 기반 검색 (반경 내 가게) | GEORADIUS stores 126.97 37.56 5 km |
Redis 활용 예시: 실시간 리더보드
# 점수 추가/갱신
ZADD game:leaderboard 1500 "player_A"
ZADD game:leaderboard 2300 "player_B"
ZADD game:leaderboard 1800 "player_C"
# 상위 10명 조회 (점수 내림차순)
ZREVRANGE game:leaderboard 0 9 WITHSCORES
# 특정 플레이어 순위 조회
ZREVRANK game:leaderboard "player_A"
# 점수 범위 조회
ZRANGEBYSCORE game:leaderboard 1000 2000캐시 워밍 전략 (Cache Warming)
개념
서비스 시작 시 또는 배포 후 캐시가 비어있는 콜드 스타트(Cold Start) 상태에서 DB에 부하가 집중되는 것을 방지하기 위해, 사전에 캐시를 채워놓는 전략이다.
워밍 방법
- 스타트업 워밍: 서비스 부팅 시 인기 데이터를 미리 캐싱
- 스케줄 워밍: 크론 잡으로 주기적 캐시 갱신
- 이벤트 기반 워밍: 데이터 변경 이벤트(CDC) 발생 시 즉시 캐싱
- 점진적 전환: 신규 캐시 노드에 트래픽을 서서히 증가
// 스타트업 워밍 예시
async function warmCache() {
const popularProducts = await db.products.findPopular({ limit: 1000 });
const pipeline = redis.pipeline();
for (const product of popularProducts) {
pipeline.set(
`product:${product.id}`,
JSON.stringify(product),
'EX', 3600
);
}
await pipeline.exec();
console.log(`캐시 워밍 완료: ${popularProducts.length}개 상품`);
}마이크로서비스에서의 캐시 일관성
과제
마이크로서비스 환경에서는 여러 서비스가 동일 데이터를 캐싱할 수 있으며, 데이터 변경 시 모든 서비스의 캐시를 동기화해야 한다.
해결 방안
- 이벤트 기반 무효화: Kafka/Redis Pub/Sub으로 캐시 무효화 이벤트 브로드캐스트
- CDC 기반 자동 무효화: Debezium으로 DB 변경 감지 → 캐시 자동 갱신
- 짧은 TTL: 강한 일관성이 불필요하면 짧은 TTL로 최종 일관성 확보
- 캐시 태그 (Cache Tags): 관련 캐시 항목을 태그로 묶어 일괄 무효화
User Service가 사용자 정보 변경
↓
Kafka 이벤트: user.updated (userId: 123)
↓
├── Order Service: redis.del("user-orders:123")
├── Profile Service: redis.del("user-profile:123")
└── Recommendation Service: redis.del("user-recs:123")엣지 캐싱 (Edge Caching)
Cloudflare Workers와 엣지 캐싱
사용자에게 물리적으로 가장 가까운 엣지 서버에서 캐시를 제공하여 글로벌 지연 시간을 최소화한다. 엣지 캐싱을 활용하면 전 세계적으로 50ms 미만의 지연 시간을 달성할 수 있다.
사용자 (한국) → 서울 엣지 PoP → 캐시 HIT → 즉시 응답 (< 50ms)
사용자 (한국) → 서울 엣지 PoP → 캐시 MISS → 오리진 서버 → 캐싱 → 응답엣지 캐시 전략:
- 정적 콘텐츠: 이미지, JS/CSS → 긴 TTL (1일~1년)
- 동적 API 응답: Cache-Control 헤더 기반 → 짧은 TTL (1초~5분)
- 개인화 콘텐츠: 캐시 불가 또는 Vary 헤더 활용
참고 자료
- Designing Data-Intensive Applications - Martin Kleppmann (Chapter 5)
- Redis Documentation: https://redis.io/docs
- System Design Interview - Alex Xu
- Cloudflare Workers Documentation: https://developers.cloudflare.com/workers