캐시를 사용하기 전 고려해야 할 것
캐시는 붙이면 빨라지는 옵션이 아니라 정합성을 내주고 속도를 사는 거래다. 동작 원리부터 읽기/쓰기 전략, 눈사태·스탬피드 같은 장애 패턴, 캐시가 독이 되는 데이터까지 정리했다.
캐시를 사용하기 전 고려해야 할 것
캐싱이란 무엇인가
캐시는 조회나 리소스가 많이 드는 원본에 대한 접근을 줄이고 서비스를 원활하게 하기 위해 결과를 미리 저장해놓는 것이다.
핵심은 캐시가 원본의 대체물이 아니라 사본이라는 점이다.
- 원본은 어딘가에 항상 존재한다.
- 캐시는 언제든 비워져도 시스템이 (느려질 뿐) 정상 동작해야 한다.
- 이 전제가 깨지면 그건 캐시가 아니라 그냥 또 하나의 저장소다.
캐시가 필요한 이유
대부분의 시스템에서 비용은 균등하지 않다. 한 번 만든 결과를 다시 만드는 비용이 너무 크기 때문에 캐싱이 필요해진다. 비싼 작업의 예는 이렇다.
- DB 조인이 많은 무거운 쿼리
- 외부 API 호출 (네트워크 RTT + rate limit)
- 복잡한 연산 (집계, 직렬화, 암호화, 렌더링)
- 디스크/원격 스토리지 접근
캐싱이 효과적인 이유는 접근 패턴이 편향돼 있기 때문이다. 소수의 데이터가 대부분의 요청을 차지하므로 hit 비율이 높아진다. 모든 데이터가 균등하게 요청되면 캐시는 의미가 없다.
동작 원리: 지역성과 메모리 계층
캐싱이 통하는 근본 이유는 **두 가지 지역성(Locality)**이다.
- 시간 지역성(Temporal Locality) — 한 번 접근한 데이터는 곧 다시 접근될 가능성이 높다 (예: 인기 게시글, 로그인 유저 정보)
- 공간 지역성(Spatial Locality) — 어떤 데이터에 접근하면 그 근처도 곧 접근한다 (예: 게시글 목록 → 상세 진입)
컴퓨터는 본질적으로 속도·용량·가격이 반비례하는 계층 구조다.
| 계층 | 대략 지연시간 | 특징 |
|---|---|---|
| CPU 레지스터 / L1 | ~1ns | 매우 빠름, 매우 작음 |
| L2 / L3 | 수~수십 ns | |
| 메인 메모리 (RAM) | ~100ns | |
| 로컬 SSD | 수십~수백 μs | |
| 같은 DC 네트워크 (Redis 등) | 0.5~수 ms | |
| 디스크 / 원격 / 외부 API | 수~수백 ms | 느리고 큼 |
숫자는 정확한 벤치마크가 아니라 자릿수(order of magnitude) 감각으로 보면 된다. 핵심은 계층마다 대략 10~1000배씩 차이가 난다는 것이다.
캐싱은 결국 이 계층에서 더 위(빠른) 계층에 자주 쓰는 데이터를 끌어올려두는 것이다. CPU 캐시·Redis·CDN은 추상 수준만 다를 뿐 같은 원리다.
핵심 용어
- Cache Hit — 찾는 데이터가 캐시에 있음
- Cache Miss — 캐시에 없어 원본까지 가야 함
- Hit Ratio(적중률) =
hits / (hits + misses). 캐시 효과를 판단하는 1차 지표 - Eviction(축출) — 공간 부족으로 기존 항목을 내보내는 것
캐시는 어디에나 있다: 계층별 예시
주니어가 "캐시 = Redis"라고 생각하기 쉬운데, 실제로는 요청 경로 곳곳에 깔려 있다.
① 클라이언트 / 프론트엔드
- 브라우저 캐시 — 이미지·JS·CSS를 로컬 디스크에 저장. 네이버 로고는 매번 다운로드하지 않음
- HTTP 캐시 — 서버가
Cache-Control: max-age=3600헤더로 "1시간 재사용 OK"를 알려줌.ETag·Last-Modified로 변경 검증
② 네트워크 / 인프라
- CDN (Cloudflare, CloudFront) — 엣지 서버가 원본 콘텐츠를 캐싱. 서울 서버 이미지를 미국 유저가 요청해도 미국 엣지에서 바로 서빙
③ 애플리케이션 / 백엔드
- 분산 캐시 — Redis, Memcached. RAM 기반이라 매우 빠르고, 모든 인스턴스가 같은 값을 공유
- 실무 예: 공지사항 목록, 실시간 인기 검색어, 세션/토큰, 랭킹
- 로컬(프로세스 내) 캐시 — Caffeine, Guava,
ConcurrentHashMap. 네트워크가 없어 가장 빠름. 단 인스턴스마다 따로 존재
④ 저장소 내부
- DB 버퍼 풀 / shared_buffers — DB가 디스크 페이지를 메모리에 캐싱
- OS Page Cache — 파일 시스템 읽기 캐싱
⑤ 하드웨어
- CPU L1/L2/L3 캐시 — 가장 원초적인 캐싱
읽기/쓰기 전략 (Caching Patterns)
가장 중요한 부분이자 토론거리가 많은 부분이다. 어디서 캐시를 채우고, 쓰기는 누가 책임지느냐의 차이다.
Cache-Aside (Look-Aside): 가장 흔함
앱이 캐시를 직접 관리한다.
읽기:
1) 캐시 조회
2) hit → 반환
3) miss → DB 조회 → 캐시에 저장 → 반환
쓰기:
1) DB 업데이트
2) 캐시 무효화(삭제) 또는 갱신
- 단순하고, 캐시 장애 시에도 DB로 정상 동작한다(캐시 의존성 낮음)
- miss 시 지연(3단계)이 있고, 정합성 관리를 앱이 책임진다
- Spring
@Cacheable의 기본 동작이 이 형태에 가깝다
Read-Through
앱은 캐시만 보고, 캐시 라이브러리가 miss 시 알아서 DB를 로딩한다.
- 흐름은 Cache-Aside와 비슷하나 로딩 책임이 캐시 계층에 있다
- 코드가 깔끔해지지만 캐시 추상화에 강하게 결합된다
Write-Through
쓰기 시 캐시와 DB에 동시(동기적)로 기록한다.
- 캐시가 항상 최신이라 정합성이 좋다
- 쓰기 지연이 증가하고, 안 읽힐 데이터까지 캐싱하는 낭비가 있다
Write-Back (Write-Behind)
쓰기를 일단 캐시에만 하고 DB는 나중에 비동기로 모아서 기록한다.
- 쓰기가 매우 빠르고, 쓰기 폭주를 흡수한다(버퍼링)
- 캐시 유실 시 데이터 손실 — 정합성이 가장 위험하다
- DB 버퍼 풀, 일부 메시지/로그 파이프라인이 이 방식이다
Write-Around
쓰기는 DB로만 하고 캐시는 안 건드린다. 다음 읽기 때 비로소 캐싱된다.
- 한 번 쓰고 잘 안 읽는 데이터에 적합하다
스케줄링 정책 (Eviction)
캐시 공간은 유한하므로 무엇을 버릴지 정해야 한다.
| 정책 | 기준 | 특징 |
|---|---|---|
| LRU (Least Recently Used) | 가장 오래 안 쓴 것 | 가장 흔함, 시간 지역성 활용 |
| LFU (Least Frequently Used) | 가장 적게 쓴 것 | 인기 데이터 유지에 강함. 과거 인기에 갇히는 단점 |
| FIFO | 먼저 들어온 것 | 단순. 지역성 무시 |
| TTL 기반 | 만료된 것 | 시간 기준. 정합성 도구로도 쓰임 |
| Random | 무작위 | 의외로 나쁘지 않고 매우 쌈 |
| W-TinyLFU | LFU+LRU 혼합 | Caffeine 기본. 현대적이고 적중률 우수 |
Redis는 maxmemory-policy로 allkeys-lru, allkeys-lfu, volatile-ttl 등을 설정한다.
캐싱이 어려운 이유: 장애 패턴
캐싱의 난이도는 "넣기"가 아니라 언제 어떻게 무효화하고 방어하느냐에 있다.
캐시 관통 (Cache Penetration)
존재하지 않는 데이터를 계속 조회하면 캐시가 항상 miss라 매번 DB로 직행한다.
- 악의적 공격(없는 ID 대량 요청)에 취약하다
- 대응: null도 짧은 TTL로 캐싱(예: "없음"을 30초간 기억) / Bloom Filter로 "확실히 없는" 키를 사전 차단
캐시 눈사태 (Cache Avalanche)
대량의 키가 동시에 만료되거나 캐시 서버가 통째로 다운되면, 트래픽이 한꺼번에 DB로 몰려 DB가 과부하·장애에 빠진다.
- 대응: TTL에 랜덤 지터(jitter) 추가(예: 300초 ± 60초) / 다층 캐시(L1+L2) / 캐시 가용성 확보(클러스터, replica)
캐시 스탬피드 / Thundering Herd (Hot Key)
인기 단일 키가 만료되는 순간, 수많은 요청이 동시에 miss가 나서 전부 같은 DB 쿼리를 동시에 던진다.
- 대응: 분산 락/뮤텍스(첫 요청만 DB 조회, 나머지는 대기 후 캐시 사용) / Refresh-Ahead(만료 전에 미리 갱신) / 확률적 조기 만료(만료 임박 시 일정 확률로 미리 갱신)
정합성 (Consistency): 가장 근본적인 문제
캐시와 DB는 별개 저장소라 둘 사이에 반드시 시차가 생긴다. "DB 업데이트 → 캐시 삭제" 순서에서도 동시성 때문에 stale이 다시 캐싱될 수 있다.
대표적인 race condition은 이렇다.
A: DB read (old=1) ← A가 먼저 읽음
B: DB write (new=2)
B: cache delete
A: cache write(old=1) ← A가 오래된 값을 다시 박아버림 ❌
보통 캐시는 업데이트보다 삭제(invalidate)가 안전하다. 업데이트는 위 race에 더 취약하고, 어차피 안 읽힐 값을 계산하는 낭비도 있다. 삭제 후 다음 읽기에서 자연스럽게 채우는 게 단순하고 견고하다.
정합성을 다루는 현실적 패턴
완벽한 정합성은 비싸다. 실무는 **"얼마나 stale을 허용할 수 있는가"**를 먼저 정한다.
- TTL을 짧게 — 가장 단순. "최대 N초 오래될 수 있음"을 명시적으로 허용
- 쓰기 시 캐시 삭제 (Cache-Aside invalidation) — 가장 흔한 기본형
- Write-Through — 정합성이 중요하고 쓰기 빈도가 낮을 때
- 지연 이중 삭제 (Delayed Double Delete) — 쓰기 시 삭제 + 짧은 지연 후 한 번 더 삭제. race로 다시 박힌 stale 제거
- 변경 이벤트 기반 무효화 — DB 변경 → 메시지/CDC(Debezium) → 캐시 무효화. 다중 인스턴스 로컬 캐시 동기화에 유용
- 버전/세대(generation) 키 — 키에 버전을 붙여 갱신 시 키 자체를 바꿔버림 (
product:v3:42)
핵심은 *"이 데이터가 1초 / 10초 / 1분 오래되면 사용자·비즈니스에 무슨 일이 생기는가?"*를 먼저 질문하는 것이다.
- 잔액·재고: 민감 → 짧은 TTL + Write-Through + 락
- 추천·랭킹·조회수: 관대 → 긴 TTL + Write-Back/배치 OK
- 프로필·공지: 중간 → Cache-Aside + 쓰기 시 삭제
언제 캐싱하나: 적합·부적합 상황
캐시는 모든 문제를 해결해주진 않는다. 캐시를 무조건 빠른 것으로 외운 채 모든 곳에 박으면 오히려 시스템이 무너질 수 있다.
트레이드오프
캐시는 "속도"를 얻기 위해 "데이터 실시간 정확성"과 "메모리 비용·복잡도"를 맞바꾸는 트레이드오프 기술이다.
장점: 무엇을 얻나
| 장점 | 설명 |
|---|---|
| 극적 latency 감소 | 디스크/네트워크를 안 거치고 RAM에서 처리 (ms → μs) |
| DB 부하 감소 | 캐시가 앞에서 막아주는 방패. 커넥션 풀 고갈 예방 |
| 컴퓨팅 비용 절감 | 무거운 집계/조인을 매번 계산하지 않고 한 번 계산 후 재사용 |
| 일시적 가용성 확보 | DB가 잠깐 죽어도 캐시에 남은 값으로 일부 서비스 지속 |
단점: 무엇을 잃나
| 단점 | 설명 |
|---|---|
| 데이터 불일치 (가장 치명적) | 원본 변경 → 캐시 미반영 → stale 노출 |
| 메모리 비용·한계 | RAM은 SSD보다 비싸고 작음. 관리 못 하면 OOM |
| 시스템 복잡도 증가 | 무효화 로직·TTL·정합성 디버깅이 따라붙음 |
| Cold Start 위험 | 캐시 비었을 때 트래픽이 DB로 몰림 (스탬피드) |
적합한 경우: "Read Heavy, Write Light"
캐시 효율이 극대화되는 건 자주 읽히지만 잘 안 변하는 데이터다.
| 데이터 유형 | 예시 | 왜 |
|---|---|---|
| 반복 조회 + 변경 적음 | 공지사항, FAQ, 카테고리, 배너 | 한 번 캐싱하면 hit율 압도적 |
| 연산 비용이 큰 결과 | 어제 베스트셀러, 시간 단위 통계 대시보드 | 무거운 집계를 한 번만 |
| 일시적 stale 허용 | 실시간 인기 검색어 (1~2분 오차 OK) | "관대한 정합성"이 캐시와 궁합 |
| 세션·인증 정보 | JWT, 로그인 세션, 블랙리스트 | 페이지마다 조회 + 짧은 TTL 자연스러움 |
부적합한 경우: 캐시가 독이 되는 곳
물처럼 시시각각 변하거나, 절대적 정확성이 필요한 데이터다.
| 데이터 유형 | 예시 | 왜 안 되나 |
|---|---|---|
| Write Heavy | 배달 라이더 실시간 위치, 채팅 메시지 | 매번 무효화 → 캐시 오버헤드가 이득보다 큼 |
| 0원 오차 금지 (금융/결제) | 계좌 잔액, 주식 호가, 결제 승인 상태 | 캐시 불일치 = 잘못된 송금 = 대형 사고 |
| 일회성·극단적 개인화 | 마이페이지 "지난달 영수증" | 본인만 한 번 보고 끝 → hit 0, 메모리만 차지 |
| 보안 민감 정보 | 주민번호, 비밀번호, 카드 CVV | RAM 덤프 등 메모리 공격면이 더 큼 |
마치며
캐시는 "붙이면 빨라지는 옵션"처럼 소개되는 경우가 많은데, 정리하고 보니 실제로는 정합성이라는 비용을 내고 속도를 사는 거래에 가깝다. 그래서 캐시를 붙이기 전에 물어야 할 건 "얼마나 빨라지나"보다 "이 데이터가 얼마나 오래 낡아도 되나"였다. 그 답이 "1초도 안 된다"면, 위 표의 부적합 칸처럼 캐시가 아니라 다른 카드를 꺼내야 한다. 실제로 LMS 이력 조회에서 캐시 대신 파티셔닝으로 간 것도 정확히 그 이유였다. 조회 키가 흩어져 있어 hit율이 안 나오는 워크로드였다.