이 글에서 얻는 것
- Redis 캐시를 “붙여서 빨라짐” 수준이 아니라, 패턴(읽기/쓰기/무효화/락) 으로 선택할 수 있습니다.
- Cache-Aside/Write-Through/Write-Behind의 트레이드오프(정합성/지연/장애 시나리오)를 정리할 수 있습니다.
- TTL/키 스키마/스탬피드/침투 같은 운영 이슈를 설계 단계에서 예방할 수 있습니다.
0) 캐시의 기본 전제: 정합성 요구사항부터 확정
캐시는 “정답”이 아니라, 정합성과 성능 사이의 선택입니다.
- 약간의 stale이 허용된다 → TTL 기반 캐시가 단순하고 효과적
- 변경 즉시 반영이 필요하다 → 무효화/갱신이 필요(복잡도 증가)
1) Cache-Aside: 가장 흔한 기본 패턴
읽기 시 캐시를 먼저 보고, 없으면 DB에서 읽어서 캐시에 적재합니다.
String getUser(String key) {
String cached = redis.get(key);
if (cached != null) return cached;
String data = db.findUser(key);
redis.setex(key, 3600, data);
return data;
}
장점:
- 구현이 단순하고, 캐시 장애 시에도 DB로 폴백 가능
주의:
- 스탬피드(만료 순간 동시 요청)와 침투(없는 키 반복 요청)에 대비가 필요
2) Write-Through / Write-Behind: 쓰기 경로를 바꿔서 얻는 것/잃는 것
Write-Through(정합성 우선)
- DB 쓰기 직후 캐시도 갱신합니다.
- 읽기는 빠르고 정합성은 높지만, 쓰기 경로가 느려지고(두 번 쓰기) 장애 전파가 커질 수 있습니다.
Write-Behind(성능 우선)
- 캐시에만 쓰고, 나중에 비동기로 DB에 반영합니다.
- 쓰기는 빠르지만 유실/중복/순서 문제가 생길 수 있어 “로그/큐/재처리” 설계가 사실상 필수입니다.
3) 분산락(Distributed Lock): 필요한 경우에만, 짧게
boolean acquired = redis.set(lockKey, uuid, "NX", "PX", 3000);
try {
if (acquired) {
// critical section
}
} finally {
// LUA 스크립트로 본인 락만 해제
}
실무 포인트:
- 락은 반드시 TTL이 있어야 합니다(무한 락 금지).
- unlock은 “내가 잡은 락만” 풀어야 합니다(UUID 비교 + Lua).
- 분산락은 만능이 아닙니다. DB 유니크 제약/트랜잭션으로 해결 가능한지 먼저 검토하는 편이 안전합니다.
4) 운영에서 자주 터지는 문제와 대응
캐시 스탬피드(Cache Stampede)
핫 키가 만료되는 순간 DB로 동시 요청이 몰립니다.
대응:
- TTL에 지터(jitter)로 만료 분산
- “하나만 로드”하도록 락/싱글플라이트(환경에 따라)
- 핫 키 워밍업
캐시 침투(Cache Penetration)
없는 데이터 요청이 반복되면 캐시가 항상 miss → DB로 계속 갑니다.
대응:
- 없음을 캐시(null 캐싱, 짧은 TTL)
- 입력 검증/차단
키 스키마/무효화
- 키 스키마를 고정하세요:
prefix:entity:version:id - 대량 무효화가 필요하면 “버전 키(네임스페이스)”로 한 번에 바꾸는 전략도 고려합니다.
5) 자주 하는 실수
- TTL 없이 캐시를 넣어 stale 데이터가 영구히 남는 경우
- 키 스키마가 제각각이라 운영에서 추적/무효화가 어려운 경우
- 락 TTL 없이 분산락을 걸어 장애 시 영구 락이 되는 경우
연습(추천)
- Cache-Aside에 TTL 지터를 적용해 스탬피드가 줄어드는지 관찰해보기
- “없는 데이터” 요청이 반복될 때 DB 부하가 어떻게 변하는지 보고, null 캐싱으로 개선해보기
- 분산락을 적용해 “중복 처리”를 막는 예제를 만든 뒤, DB 유니크 제약으로도 해결 가능한지 비교해보기
💬 댓글