Cache2026년 6월 5일9분 읽기

캐시를 사용하기 전 고려해야 할 것

캐시는 붙이면 빨라지는 옵션이 아니라 정합성을 내주고 속도를 사는 거래다. 동작 원리부터 읽기/쓰기 전략, 눈사태·스탬피드 같은 장애 패턴, 캐시가 독이 되는 데이터까지 정리했다.

#Cache#Redis#정합성#성능#Backend#Architecture

캐시를 사용하기 전 고려해야 할 것

캐싱이란 무엇인가

캐시는 조회나 리소스가 많이 드는 원본에 대한 접근을 줄이고 서비스를 원활하게 하기 위해 결과를 미리 저장해놓는 것이다.

핵심은 캐시가 원본의 대체물이 아니라 사본이라는 점이다.

  • 원본은 어딘가에 항상 존재한다.
  • 캐시는 언제든 비워져도 시스템이 (느려질 뿐) 정상 동작해야 한다.
  • 이 전제가 깨지면 그건 캐시가 아니라 그냥 또 하나의 저장소다.

캐시가 필요한 이유

대부분의 시스템에서 비용은 균등하지 않다. 한 번 만든 결과를 다시 만드는 비용이 너무 크기 때문에 캐싱이 필요해진다. 비싼 작업의 예는 이렇다.

  • 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): 가장 흔함

앱이 캐시를 직접 관리한다.

text
읽기:
  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-TinyLFULFU+LRU 혼합Caffeine 기본. 현대적이고 적중률 우수

Redis는 maxmemory-policyallkeys-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은 이렇다.

text
A: DB read (old=1)              ← A가 먼저 읽음
B: DB write (new=2)
B: cache delete
A: cache write(old=1)           ← A가 오래된 값을 다시 박아버림  ❌

보통 캐시는 업데이트보다 삭제(invalidate)가 안전하다. 업데이트는 위 race에 더 취약하고, 어차피 안 읽힐 값을 계산하는 낭비도 있다. 삭제 후 다음 읽기에서 자연스럽게 채우는 게 단순하고 견고하다.


정합성을 다루는 현실적 패턴

완벽한 정합성은 비싸다. 실무는 **"얼마나 stale을 허용할 수 있는가"**를 먼저 정한다.

  1. TTL을 짧게 — 가장 단순. "최대 N초 오래될 수 있음"을 명시적으로 허용
  2. 쓰기 시 캐시 삭제 (Cache-Aside invalidation) — 가장 흔한 기본형
  3. Write-Through — 정합성이 중요하고 쓰기 빈도가 낮을 때
  4. 지연 이중 삭제 (Delayed Double Delete) — 쓰기 시 삭제 + 짧은 지연 후 한 번 더 삭제. race로 다시 박힌 stale 제거
  5. 변경 이벤트 기반 무효화 — DB 변경 → 메시지/CDC(Debezium) → 캐시 무효화. 다중 인스턴스 로컬 캐시 동기화에 유용
  6. 버전/세대(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, 메모리만 차지
보안 민감 정보주민번호, 비밀번호, 카드 CVVRAM 덤프 등 메모리 공격면이 더 큼

마치며

캐시는 "붙이면 빨라지는 옵션"처럼 소개되는 경우가 많은데, 정리하고 보니 실제로는 정합성이라는 비용을 내고 속도를 사는 거래에 가깝다. 그래서 캐시를 붙이기 전에 물어야 할 건 "얼마나 빨라지나"보다 "이 데이터가 얼마나 오래 낡아도 되나"였다. 그 답이 "1초도 안 된다"면, 위 표의 부적합 칸처럼 캐시가 아니라 다른 카드를 꺼내야 한다. 실제로 LMS 이력 조회에서 캐시 대신 파티셔닝으로 간 것도 정확히 그 이유였다. 조회 키가 흩어져 있어 hit율이 안 나오는 워크로드였다.

#Cache#Redis#정합성#성능#Backend#Architecture

황호민

Backend Engineer · Java/Kotlin · Spring Boot · Next.js