Caching Patterns
캐싱은 Redis의 가장 일반적인 사용 사례입니다. 이 문서에서는 다양한 캐싱 패턴과 각각의 장단점, 사용 시나리오를 다룹니다.
Cache-Aside Pattern (Lazy Loading)
읽기 중심 워크로드를 위한 지연 로딩 패턴
개념
애플리케이션이 캐시를 보조 데이터 저장소로 취급하여 명시적으로 관리합니다. 캐시 미스 시 데이터베이스에서 조회한 후 캐시에 저장합니다.
동작 방식
- 캐시 먼저 확인: Redis에서 요청한 키 조회
- 캐시 히트 시: 즉시 캐시된 데이터 반환
- 캐시 미스 시: 데이터베이스 조회 후 Redis에 저장 (TTL 설정), 데이터 반환
sequenceDiagram
participant App as Application
participant Cache as Redis Cache
participant DB as Database
App->>Cache: GET user:123
alt Cache Hit
Cache-->>App: 캐시된 데이터
else Cache Miss
App->>DB: SELECT * FROM users WHERE id = 123
DB-->>App: 사용자 데이터
App->>Cache: SET user:123 "{...}" EX 3600
App-->>App: 데이터 반환
end
Redis 명령어
# 캐시 조회
GET user:123
# 캐시 미스 시 데이터베이스 조회 후 저장
SET user:123 "{...user data...}" EX 3600
장점
- 캐시 장애에 대한 복원력: Redis 장애 시 데이터베이스로 graceful degradation
- 메모리 효율성: 실제로 요청된 데이터만 캐시됨
- 단순성: 이해하고 구현하기 쉬움
단점
- 오래된 데이터 문제: 데이터베이스 업데이트 후 캐시가 TTL 만료될 때까지 오래된 데이터 유지
오래된 데이터 완화
데이터베이스 업데이트 성공 시 해당 캐시 키 삭제:
DEL user:123
다음 읽기에서 캐시 미스가 발생하고 최신 데이터를 조회합니다.
사용 시기
- 읽기 중심 워크로드
- 잠시 오래된 데이터가 허용됨
- 캐시 장애 시 graceful degradation 필요
- 캐시할 데이터에 대한 세밀한 제어 필요
사용 피해야 할 때
- 캐시와 데이터베이스 간 강한 일관성 필요
- 쓰기 후 즉시 읽기 패턴이 빈번함
- 캐시 미스 비용이 극도로 높음
Write-Through Caching Pattern
캐시-데이터베이스 강한 일관성 유지
개념
모든 쓰기 작업이 Redis와 데이터베이스 모두에 동기적으로 업데이트됩니다. 성공 확인 전 두 쓰기가 모두 완료되어야 합니다.
동작 방식
- 애플리케이션이 Redis에 데이터 쓰기
- 애플리케이션이 데이터베이스에 동일한 데이터 쓰기
- 두 쓰기 모두 성공해야 작업 완료
sequenceDiagram
participant App as Application
participant Cache as Redis Cache
participant DB as Database
App->>Cache: SET user:123 "{...}"
Cache-->>App: OK
App->>DB: UPDATE users SET ...
DB-->>App: OK
App-->>App: 성공 반환
Redis 명령어
SET user:123 "{...updated user data...}"
# 이어서 데이터베이스 쓰기 수행
장점
- 강한 일관성: 캐시가 항상 최신 데이터 반영
- 쓰기 후 빠른 읽기: 쓴 데이터가 즉시 캐시에서 사용 가능
- 간단한 캐시 무효화: 쓰기가 캐시를 통과하므로 별도 무효화 불필요
단점
- 쓰기 지연 증가: 두 쓰기 모두 완료될 때까지 대기
- 캐시 오염: 자주 읽히지 않는 데이터도 캐시 메모리 차지
- 부분 실패 복잡성: 데이터베이스 쓰기 실패 시 불일치 발생 가능
부분 실패 처리
가장 안전한 접근: 데이터베이스 먼저 쓰기
- 데이터베이스에 쓰기
- 성공하면 캐시에 쓰기
- 캐시 쓰기 실패 시 로그만 남기고 작업 실패 처리 안 함
사용 시기
- 캐시와 데이터베이스 간 강한 일관성 필요
- 쓰기 후 즉시 읽기 패턴이 빈번함
- 쓰기 지연 오버헤드가 수용 가능
- 데이터셋이 상대적으로 작음
Write-Behind (Write-Back) Caching Pattern
최대 쓰기 처리량을 위한 비동기 동기화
개념
애플리케이션은 Redis에만 쓰고 즉시 확인받습니다. 별도의 백그라운드 프로세스가 주기적으로 변경 사항을 데이터베이스에 동기화합니다.
동작 방식
- 애플리케이션이 Redis에 데이터 쓰기
- Redis가 즉시 확인 (클라이언트는 성공 인식)
- 백그라운드 프로세스가 주기적으로 변경된 데이터를 읽어 데이터베이스에 쓰기
sequenceDiagram
participant App as Application
participant Cache as Redis Cache
participant BG as Background Sync
participant DB as Database
App->>Cache: SET counter:page_views "15847293"
Cache-->>App: OK (즉시)
Note over BG: 주기적 실행
BG->>Cache: 변경된 키 스캔
Cache-->>BG: 변경된 데이터
BG->>DB: BATCH UPDATE
DB-->>BG: OK
Redis 명령어
SET counter:page_views "15847293"
INCRBY counter:page_views 1
장점
- 매우 낮은 쓰기 지연: Redis 확인 후 즉시 완료 (서브 밀리초)
- 높은 처리량: 데이터베이스가 쓰기 폭주로부터 보호됨
- 쓰기 병합: 동일 키에 대한 여러 업데이트가 단일 데이터베이스 쓰기로 병합됨
내구성 트레이드오프
- 데이터 손실 위험: Redis가 동기화 전에 실패하면 마지막 동기화 이후 쓰기 손실
- 결과적 일관성: 데이터베이스가 Redis보다 뒤처짐
데이터 손실 완화
- Redis 지속성:
appendfsync everysec로 AOF 활성화 (1초 내 손실 제한) - 짧은 동기화 간격: 손실 윈도우 최소화
- 복제: 복제본 실행으로 단일 노드 실패 대비
적합한 사용 사례
- 카운터 및 메트릭: 페이지 뷰, 좋아요, 노출 수 (일부 손실 허용)
- 고빈도 세션 업데이트: 빈번한 세션 터치
- 분석 수집: 고속 원격 측정 데이터 수집
- 게임 리더보드: 빠른 점수 업데이트
사용 금지 사례
- 금융 거래
- 주문 처리
- 사용자 자격 증명 또는 인증 데이터
- 규정 준수 요구 사항이 있는 데이터
- 데이터 손실이 용납되지 않는 모든 것
Cache Stampede Prevention
캐시 스탬피드(Thundering Herd) 방지
문제 설명
인기 있는 캐시 키가 만료되면 수천 개의 동시 요청이 동시에 데이터베이스를 쿼리하여 값을 재생성합니다.
graph LR
subgraph "Cache Stampede 시나리오"
A[인기 키 만료] --> B[수천 개의 동시 요청]
B --> C[모두 캐시 미스]
C --> D[동시 DB 쿼리]
D --> E[DB 과부하]
end
해결책 1: 확률적 조기 만료 (X-Fetch)
물리적 TTL을 필요보다 길게 설정하고, 만료에 가까워질수록 새로고침 확률이 증가합니다.
알고리즘
refresh_decision = current_time - (expiry_time - delta * beta * log(random()))
delta: 값 재생성에 필요한 시간beta: 튜닝 매개변수 (일반적으로 1.0)random(): 0과 1 사이의 무작위 값
결과
만료 시점에 수천 개의 동시 쿼리 대신, 단일 요청이 만료 직전에 캐시를 새로고침하고 나머지는 약간 오래된 데이터를 계속 제공합니다.
해결책 2: 뮤텍스 락킹
하나의 프로세스만 캐시 재생성을 허용하는 결정론적 접근입니다.
동작 방식
SET lock:mykey <token> NX PX 5000로 락 획득 시도- 락 획득 시 데이터베이스 쿼리 및 캐시 업데이트
- 다른 요청은 대기하거나 오래된/기본값 반환
- 캐시 채워진 후 락 해제
Redis 명령어
# 락 획득
SET lock:popular_key abc123 NX PX 5000
안전한 락 해제 (Lua 스크립트)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
return 0
대기 클라이언트 처리 옵션
- 폴링: 잠시 대기 후 캐시 다시 확인
- 오래된 데이터 반환: 오래된 복사본이 있으면 제공
- 기본값 반환: 기본 또는 저하된 응답 제공
- 빠른 실패: 즉시 오류 반환
비교
| 접근 방식 | 복잡성 | DB 쿼리 | 오래된 데이터 |
|---|---|---|---|
| 보호 없음 | 낮음 | N (요청당 1개) | 없음 |
| X-Fetch | 중간 | 1-2 | 짧은 윈도우 |
| 뮤텍스 락 | 높음 | 정확히 1 | 없음 |
권장사항
대부분의 애플리케이션에서는 뮤텍스 락킹으로 시작하는 것이 좋습니다. 이해하기 쉽고 강력한 보호를 제공합니다. 초고트래픽 키에서는 짧은 락 경쟁도 문제가 될 수 있으므로 X-Fetch를 고려하세요.
Server-Assisted Client-Side Caching
네트워크 RTT 제거를 위한 서버 지원 클라이언트 사이드 캐싱
개념
자주 액세스하는 키를 애플리케이션 메모리에 캐시하고, Redis 6+가 데이터 변경 시 자동으로 무효화 메시지를 보냅니다.
아키텍처
graph TB
subgraph "Multi-Tier Cache"
L1[L1: Local Memory - 나노초]
L2[L2: Redis - 마이크로초~밀리초]
L3[L3: Database - 밀리초]
end
L1 --> L2
L2 --> L3
추적 모드
Default Tracking
서버가 각 클라이언트가 요청한 키를 기억합니다. 키가 수정되면 무효화 메시지를 보냅니다.
CLIENT TRACKING ON
트레이드오프: 서버가 키→클라이언트ID 매핑 테이블 유지 필요
Broadcasting Mode
개별 키 액세스 대신 키 프리픽스를 구독합니다.
CLIENT TRACKING ON BCAST PREFIX user: PREFIX session:
트레이드오프: 실제 캐시하지 않은 키에 대한 무효화 메시지도 수신할 수 있음
Redis 명령어
# 기본 추적 활성화
CLIENT TRACKING ON
# 브로드캐스트 모드
CLIENT TRACKING ON BCAST PREFIX user:
# 무효화 리다이렉트
CLIENT TRACKING ON REDIRECT 123
# 추적 비활성화
CLIENT TRACKING OFF
# 자신의 수정에 대한 무효화 방지
CLIENT TRACKING ON NOLOOP
OPTIN/OPTOUT 옵션
CLIENT TRACKING ON OPTIN
CLIENT CACHING YES
GET user:123 # 이 키만 추적됨
CLIENT CACHING NO
GET temp:data # 이 키는 추적되지 않음
사용 시기
- 초고처리량 애플리케이션에서 네트워크 RTT가 중요한 경우
- 자주 액세스하는 “hot” 키
- 상대적으로 안정적인 데이터의 읽기 중심 워크로드
- 이미 로컬 캐시를 유지 중이고 무효화가 필요한 경우
고려사항
- 메모리 관리: 애플리케이션이 로컬 캐시 크기 관리 필요
- 복잡성: 무효화 메시지 처리를 위한 전용 연결 관리 필요
- 휘발성 데이터에 부적합: 데이터가 지속적으로 변경되면 무효화 트래픽이 이득을 초과
- RESP3 권장: Redis 6+ RESP3 프로토콜 사용 권장