캐싱은 RAM 기반의 빠른 저장소를 활용해 DB 부하를 줄이고 응답 속도를 높이는 기술이다. RAM은 DB보다 수십~수백 배 빠르지만 용량이 제한적이기 때문에 어떤 데이터를 어떻게 저장하고 갱신할지에 대한 전략이 필요하다.
기본 용어:
캐싱 전략은 읽기 전략과 쓰기 전략으로 나뉘며, 실제로는 조합해서 쓴다.
애플리케이션이 캐시와 DB를 직접 관리하며, 요청 시 캐시를 먼저 확인하고 미스 시에만 DB에서 읽어오는 방식이다. Lazy Loading이라고도 부른다.
캐시 Hit:
클라이언트
↓
[Cache-Aside Logic]
↓
캐시 조회 → Hit!
↓
캐시된 데이터 반환
캐시 Miss:
클라이언트
↓
[Cache-Aside Logic]
↓
캐시 조회 → Miss
↓
DB 조회 (비용)
↓
데이터 캐시 적재 (TTL 설정)
↓
데이터 반환
쓰기:
클라이언트 (Update 요청)
↓
DB 업데이트
↓
캐시 무효화 (Delete)
↓
다음 읽기 요청 시 자동 갱신
쓰기에서는 DB가 신뢰할 수 있는 소스(Source of Truth)가 되고, 캐시는 보조 역할만 수행한다.
캐시 라이브러리(또는 캐시 프로바이더)가 DB 동기화를 담당하는 방식이다. 애플리케이션은 항상 캐시에서만 읽고, 미스 발생 시 캐시 계층이 자동으로 DB를 조회해 캐시를 채운 뒤 반환한다.
클라이언트
↓
캐시 조회
↓ (Miss)
[캐시 계층이 자동으로]
↓
DB 조회
↓
캐시에 저장
↓
데이터 반환
애플리케이션 코드 입장에서는 항상 캐시에서 데이터를 가져오는 것처럼 보인다.
쓰기 요청이 들어오면 캐시와 DB를 동시에(동기) 업데이트하는 방식이다. 항상 두 곳이 같은 데이터를 가진다.
클라이언트 (Write 요청)
↓
캐시에 저장
↓
즉시 DB에 저장 (동기)
↓
완료 응답
캐시에 먼저 쓰고, 일정 시간 이후 배치 작업으로 DB에 비동기 반영하는 방식이다. 캐시가 임시 큐 역할을 한다.
클라이언트 (Write 요청)
↓
캐시에 저장 (즉시 응답)
↓
[비동기 - 일정 시간 후]
↓
배치로 DB에 반영
모든 쓰기를 DB에만 저장하고, 캐시는 읽기 요청 시 미스가 발생할 때만 채워지는 방식이다.
클라이언트 (Write 요청)
↓
DB에 직접 저장 (캐시 우회)
↓
캐시는 갱신 안 함
클라이언트 (Read 요청)
↓
캐시 조회 → Miss
↓
DB 조회
↓
캐시에 저장
단일 전략보다는 읽기/쓰기 전략을 조합해서 사용하는 것이 일반적이다.
| 전략 | 읽기 성능 | 쓰기 성능 | 데이터 정합성 | DB 부하 | 구현 복잡도 | 캐시 장애 시 |
|---|---|---|---|---|---|---|
| Look-Aside | 중간 (첫 요청 느림) | 보통 | 약함 (Stale 위험) | 높음 | 낮음 | 서비스 유지 (성능 저하) |
| Read-Through | 높음 | 보통 | 높음 | 낮음 | 중간 | 서비스 중단 |
| Write-Through | 보통 | 낮음 (2배 처리) | 최고 | 낮음 | 중간 | 데이터 안전 |
| Write-Behind | 높음 | 최고 | 약함 (비동기) | 최저 | 높음 | 데이터 유실 위험 |
| Write-Around | 중간 | 최고 | 약함 (캐시 우회) | 높음 | 낮음 | 서비스 유지 |
서비스 시작 직후나 캐시 리셋 후 모든 요청이 캐시 미스가 되어 응답 시간이 크게 증가하는 현상이다.
해결 방법:
DB는 업데이트됐는데 캐시가 아직 만료되지 않아 구 데이터를 제공하는 현상이다.
17:00 - 상품 가격 1,000원으로 업데이트
→ 캐시 무효화 완료
17:00:01 - 사용자 A 조회 → 캐시 Miss → DB에서 1,000원 조회, 캐시 갱신
17:00:59 - 사용자 B 조회 → 캐시 Hit → 1,000원 반환 ✅
만약 캐시 무효화를 놓쳤다면?
17:00 - 상품 가격 1,000원으로 업데이트 (캐시 무효화 안 함!)
17:00:01 ~ 17:00:59 - 모든 사용자가 구 가격 900원을 받음 ❌
17:01:00 - TTL 만료 → 다음 요청부터 1,000원 반환
해결 방법:
TTL이 만료되는 순간, 많은 스레드/서버가 동시에 캐시 미스를 감지하고 모두 DB를 조회하려는 현상이다.
10,000명의 사용자가 동시에 같은 상품 조회
↓
TTL 만료로 모두 캐시 미스 감지
↓
10,000개의 DB 쿼리가 동시 실행 ← Duplicate Read
↓
10,000개의 캐시 쓰기 동시 발생 ← Duplicate Write
↓
DB 연결 풀 고갈 / CPU 스파이크 / 타임아웃
해결 방법:
첫 번째 요청: 캐시 미스 감지 → Lock 획득 → DB 조회 → 캐시 저장 → Lock 해제
다른 요청들: 캐시 미스 감지 → Lock 대기 → Lock 해제 후 캐시에서 읽기
안전하지만, Lock 대기 시간에 따라 응답 지연 발생 가능.
TTL을 두 부분으로 나눔
- TTL = 100초
- Soft TTL = 80초 (80% 시점)
80초 경과 후 일정 확률(예: 5%)로 백그라운드 갱신 시작
→ 대부분의 요청은 캐시 히트, 극소수만 DB 조회
→ 캐시 폭풍 가능성 대폭 감소
가장 단순한 방법. TTL이 너무 짧으면 자주 만료되고 스탬피드 위험이 커진다. 데이터 특성을 고려해 충분히 길게 설정.
메모리가 부족할 때 어떤 데이터를 먼저 제거할지에 대한 정책이다.
| TTL | 문제점 |
|---|---|
| 너무 짧음 | 데이터가 빨리 제거됨 → 캐시 효율 낮음, Stampede 위험 |
| 너무 긺 | 오래된 데이터 계속 제공 → 메모리 부족, 정합성 문제 |
원칙: 데이터 특성에 따라 차등 설정
| 정책 | 설명 | 적합 시나리오 |
|---|---|---|
| LRU (Least Recently Used) | 최근에 사용하지 않은 것부터 제거 | 일반적인 캐시 |
| LFU (Least Frequently Used) | 사용 빈도가 낮은 것부터 제거 | 핫 데이터 유지가 중요할 때 |
| Random | 무작위 제거 | 균등한 접근 패턴 |
| TTL 기반 | 만료 임박한 항목부터 제거 | TTL이 명확히 설정된 캐시 |
| noeviction | 제거 안 함 → 메모리 부족 시 에러 반환 | 데이터 유실이 절대 안 될 때 |
여러 애플리케이션 인스턴스가 동일한 캐시에 동시 접근할 때 발생하는 문제다.
서버 A: 캐시 조회 → Miss → DB 조회 시작
서버 B: 캐시 조회 → Miss → DB 조회 시작 (동시)
서버 A: DB에서 v1 읽어서 캐시 저장
서버 B: DB에서 v1 읽어서 캐시 저장 (중복 쓰기)
→ 중복 DB 조회 발생 (Duplicate Read)
Optimistic Lock (낙관적 잠금)
Pessimistic Lock (비관적 잠금)
캐시는 제한된 메모리 자원이므로 무엇을 캐싱할지 선택이 중요하다.
"전체 요청의 80%는 전체 데이터의 20%에 집중된다." 상위 20%의 핫 데이터만 캐싱해도 대부분의 요청을 커버할 수 있다.
올리브영 기술 블로그의 "MSA 환경에서 도메인 데이터 연동 전략"에서 소개한 사례:
변경이 적은 데이터에 Cache-Aside(Look-Aside) 패턴 적용
상품 정보, 카테고리, 옵션 데이터 (변경 빈도: 하루 1~2회)
↓
Redis Cache-Aside + TTL 1시간 설정
↓
첫 접근 시 캐시 미스 → DB 조회 (비용 발생)
다음 59분간 → 캐시 히트 (매우 빠름)
↓
변경 발생 시 명시적으로 캐시 키 삭제
다음 요청 시 자동으로 최신 데이터 캐싱
실시간성이 필요한 데이터는 별도로 Kafka 이벤트 기반으로 처리해 불필요한 API 호출을 줄이면서도 최신 데이터를 보장했다.
관련 문서: MSA 환경에서 도메인 데이터 연동 전략
"캐시는 성능 향상을 위한 것이지, 데이터 영속성을 보장하는 곳이 아니다."