ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

캐시 (Cache)

도입

원본 데이터를 대체하는 것이 아니라, 자주 읽히거나 다시 계산하기 비싼 값을 더 빠른 계층에 복사해 지연 시간과 원본 저장소 부하를 동시에 줄이는 데 있다

캐시는 단순히 “빨리 꺼내 쓰는 임시 저장소” 정도로 설명되곤 하지만, 실제로는 시스템 성능과 운영 안정성에 직접 영향을 주는 설계 요소입니다.

데이터베이스를 매번 직접 조회하면 지연이 커지고, 트래픽이 몰릴 때 원본 저장소가 병목이 되기 쉽습니다. 이때 캐시는 같은 요청을 더 빠르게 처리하고, 원본 시스템이 감당해야 할 읽기 부하를 줄이며, 사용자 체감 속도까지 개선합니다. 다만 그 대가로 신선도, 일관성, 무효화, 메모리 한도 같은 새로운 문제가 따라옵니다.

필요성

캐시를 이해하면 시스템이 왜 빨라지는지뿐 아니라, 왜 더 빨라졌는데도 가끔 틀린 값을 돌려주거나 업데이트 반영이 늦는지까지 함께 설명할 수 있다

캐시는 보통 읽기 성능을 개선하기 위해 도입합니다. 자주 조회되는 데이터를 메모리나 더 가까운 위치에 복사해 두면, 같은 요청을 훨씬 더 싸고 빠르게 처리할 수 있기 때문입니다.

하지만 캐시는 단순한 가속기가 아닙니다. 캐시가 도입되면 데이터 흐름이 “애플리케이션 → 원본 저장소”라는 단순 경로에서 “애플리케이션 → 캐시 → 원본 저장소” 구조로 바뀌고, 그 순간부터 성능 외에도 캐시 적중률, 만료 정책, 삭제 시점, 정합성, 메모리 압박을 함께 관리해야 합니다.

캐시가 특히 강한 상황
  • 같은 데이터가 반복해서 읽히는 읽기 중심 워크로드
  • 원본 조회나 계산 비용이 큰 API / 페이지 / 집계 결과
  • 트래픽 스파이크를 원본 DB가 직접 받으면 위험한 구조
  • 사용자와 가까운 곳에서 응답을 재사용할 수 있는 정적/반정적 콘텐츠
  • 짧은 시간 동안 같은 결과를 여러 번 재사용해도 큰 문제가 없는 데이터

정의

캐시는 원본 데이터를 대체하는 저장소가 아니라, 더 빠른 매체에 원본의 복사본을 보관해 같은 요청을 더 빠르게 처리하려는 가속 계층이다

캐시는 보통 애플리케이션과 원본 데이터 저장소 사이에 위치합니다. 요청이 들어오면 먼저 캐시에 데이터가 있는지 확인하고, 있으면 즉시 반환하며, 없으면 원본 저장소에서 가져와 캐시에 넣은 뒤 반환합니다.

이때 중요한 용어가 두 가지 있습니다. cache hit는 캐시에 원하는 데이터가 있어 바로 반환된 경우이고, cache miss는 캐시에 없어 원본 저장소까지 내려가야 하는 경우입니다. 결국 캐시 성능은 “얼마나 빨리 처리하느냐”만큼이나 “얼마나 자주 hit를 만들 수 있느냐”에 달려 있습니다.

핵심 문장
캐시는 정답 저장소가 아니라 가속 계층입니다. 따라서 “빠르다”와 “항상 최신이다”는 같은 말이 아닙니다.

핵심 원리

캐시가 통하는 이유는 최근에 읽었거나 자주 읽는 데이터가 다시 요청될 가능성이 높다는 가정 위에서, 그 반복 비용을 더 싼 계층으로 옮기기 때문이다

캐싱은 결국 반복을 이용하는 최적화입니다. 같은 데이터를 많은 사용자가 반복해서 보고, 같은 API가 짧은 시간 안에 여러 번 호출되고, 같은 계산 결과가 쉽게 변하지 않는다면, 그 결과를 매번 다시 만들 필요가 없습니다.

다만 이 최적화는 전제 조건이 있습니다. 읽기 재사용성이 있어야 하고, 캐시된 값이 잠시 오래되어도 서비스가 감당 가능해야 하며, 캐시를 두는 비용이 원본을 직접 치는 비용보다 낮아야 합니다. 그래서 캐시는 모든 데이터에 만능으로 적용하는 장치가 아니라, 적합한 데이터에만 효과적인 선택적 가속기입니다.

기본 구조

캐시는 어디에 두느냐에 따라 성격이 크게 달라지며, 로컬 메모리 캐시·분산 캐시·HTTP/CDN 캐시는 같은 “캐시”라도 운영 포인트가 다르다
종류 위치 장점 주의점
로컬(in-process) 캐시 애플리케이션 프로세스 내부 메모리 가장 빠르고 네트워크 홉이 없음 인스턴스마다 값이 달라질 수 있어 정합성 관리가 어려움
분산 캐시 별도 캐시 서버 또는 클러스터 여러 인스턴스가 같은 캐시를 공유 가능 네트워크 비용이 있고 장애 지점이 하나 더 생김
HTTP / CDN 캐시 브라우저, 프록시, 엣지 사용자와 가까운 위치에서 응답 재사용 가능 헤더 정책, purge, stale 응답 관리가 중요함
기본 흐름
1) 요청이 들어온다
2) 캐시에서 키를 조회한다
3) 있으면 hit → 즉시 반환
4) 없으면 miss → 원본 저장소 조회
5) 결과를 캐시에 저장
6) 다음 요청부터는 hit를 기대

패턴 1. Cache-Aside

Cache-Aside는 애플리케이션이 캐시를 직접 관리하는 가장 대표적인 전략으로, 캐시에 없을 때만 원본 저장소에 내려가고 가져온 값을 다시 캐시에 채워 넣는다

가장 널리 쓰이는 전략은 Cache-Aside입니다. 흔히 lazy loading이라고도 부릅니다. 이 패턴에서는 애플리케이션이 캐시와 원본 저장소를 모두 알고 있고, 먼저 캐시를 조회한 뒤 miss일 때만 원본 저장소에서 읽어 와 캐시에 채웁니다.

장점은 단순하고 효과가 빠르다는 점입니다. 실제로 요청된 데이터만 캐시에 올라가므로 공간을 낭비하지 않고, 서비스 코드에 점진적으로 도입하기 쉽습니다. 단점은 초기 miss 때는 캐시와 DB를 모두 거쳐야 하므로 첫 응답 비용이 더 크고, 쓰기 이후 무효화를 직접 설계해야 한다는 점입니다.

String key = "product:v1:" + productId;

Product cached = cache.get(key);
if (cached != null) {
    return cached; // cache hit
}

Product product = repository.findById(productId);
if (product != null) {
    cache.set(key, product, 300); // TTL 300초
}
return product;
실전 팁
대부분의 백엔드 서비스에서 캐시를 처음 도입할 때 가장 무난한 출발점은 Cache-Aside입니다. 다만 이 전략은 정합성 보장을 자동으로 주지 않으므로, 쓰기 이후 삭제 또는 갱신 규칙을 반드시 함께 설계해야 합니다.

패턴 2. Read-Through / Write-Through / Write-Behind

캐시 전략을 제대로 이해하려면 읽기 미스를 누가 처리하는지, 쓰기 시점에 캐시와 원본 저장소를 동기식으로 맞출지 비동기식으로 넘길지를 구분해야 한다
전략 읽기 / 쓰기 방식 장점 단점
Read-Through 캐시가 miss 시 원본을 대신 읽어 채움 애플리케이션 코드가 단순해짐 캐시 계층이 더 많은 책임을 가짐
Write-Through 원본 저장소 업데이트 직후 캐시도 동기식으로 갱신 캐시 최신성 유지에 유리함 자주 안 읽는 데이터까지 캐시에 쌓일 수 있음
Write-Behind 먼저 캐시에 쓰고, 원본 반영은 비동기식으로 지연 쓰기 성능이 좋고 원본 저장소 부하를 줄이기 쉬움 장애 시 유실 / 지연 반영 리스크가 커짐

실무에서는 한 가지 전략만 고집하기보다 조합하는 경우가 많습니다. 예를 들어 읽기는 Cache-Aside로 처리하고, 일부 핵심 엔터티만 Write-Through로 동기화하는 방식이 흔합니다.

핵심은 성능만 볼지, 정합성까지 같이 볼지, 그리고 캐시가 애플리케이션 로직 바깥에서 자동으로 동작할지 여부를 먼저 정하는 것입니다.

패턴 3. TTL, Eviction, Invalidation

캐시 운영에서 가장 많이 헷갈리는 세 개념은 TTL, eviction, invalidation이며, 이 셋은 모두 “캐시가 사라진다”는 결과는 비슷해 보여도 원인과 의미가 완전히 다르다
개념 언제 일어나나 핵심 목적 대표 예시
TTL / Expiration 정해 둔 시간이 지나면 오래된 데이터가 영원히 남지 않도록 하기 5초, 5분, 1시간 만료
Eviction 메모리 한도나 정책 조건을 넘으면 메모리 압박 해소 LRU, LFU, TTL-shortest 우선 제거
Invalidation / Purge 데이터가 바뀌었을 때 명시적으로 오래된 복사본 즉시 제거 또는 교체 키 삭제, 태그 purge, 버전 키 변경

TTL은 시간 기반 만료이고, eviction은 메모리 압박 시 정책에 따른 제거이며, invalidation은 데이터 변경에 맞춘 명시적 삭제입니다. 이 셋을 섞어 생각하면 “왜 분명 TTL이 남아 있는데 키가 없어졌지?”, “왜 만료 안 됐는데 새 값이 안 보이지?” 같은 혼란이 생깁니다.

운영 관점에서 자주 쓰는 정책
  • LRU : 최근에 덜 쓰인 키 제거
  • LFU : 자주 쓰이지 않은 키 제거
  • short TTL : 빠르게 변하는 데이터는 짧게 유지
  • versioned key : product:v2:123처럼 버전으로 새 키 사용
  • purge : CDN/HTTP 캐시는 URL, 태그, 호스트 기준으로 제거
실전 팁
TTL을 전혀 두지 않으면 무효화 버그가 장기 잠복하기 쉽고, TTL을 지나치게 짧게 두면 캐시가 있으나 마나한 상태가 됩니다. 캐시 정책은 “짧을수록 안전”도 아니고 “길수록 이득”도 아닙니다.

계층형 캐시

현실의 시스템은 캐시를 한 곳에만 두지 않고, 브라우저·CDN·애플리케이션 메모리·분산 캐시처럼 여러 층으로 쌓아 각 층에서 반복 비용을 줄인다

캐시는 보통 한 겹으로 끝나지 않습니다. 사용자 브라우저의 HTTP 캐시가 먼저 응답을 재사용하고, 그다음 CDN이 엣지에서 응답을 제공하고, 애플리케이션은 로컬 메모리나 Redis 같은 분산 캐시를 보고, 마지막에야 원본 DB까지 내려가는 구조가 흔합니다.

이 구조의 장점은 각 계층이 서로 다른 비용을 줄인다는 점입니다. 브라우저와 CDN은 네트워크와 원본 서버 부하를 줄이고, 애플리케이션 캐시는 DB 읽기를 줄입니다. 다만 계층이 많아질수록 어느 층의 오래된 값이 보이는지 추적하는 것은 더 어려워집니다.

예시
브라우저 캐시
→ CDN / 프록시 캐시
→ 애플리케이션 로컬 캐시
→ 분산 캐시(Redis 등)
→ 원본 DB / 원본 API

한계와 대가

캐시는 거의 항상 빠름을 주지만, 그 대가로 stale data, 정합성 차이, cold start, 메모리 비용, stampede 같은 운영 문제를 새로 만든다

캐시의 가장 큰 단점은 오래된 값입니다. 원본은 이미 갱신되었는데 캐시가 아직 살아 있다면, 사용자는 이전 값을 보게 됩니다. Cache-Aside에서는 외부 프로세스가 원본을 바꾼 경우 캐시가 자동으로 따라가지 못할 수도 있습니다.

또 다른 문제는 cache stampede입니다. 인기 키가 만료되는 순간 많은 요청이 동시에 miss를 내고 한꺼번에 원본 저장소를 때리면, 캐시가 오히려 장애를 유발할 수 있습니다. 이 경우에는 request coalescing, lock, early refresh, stale-while-revalidate, prewarm 같은 보호 전략이 필요합니다.

마지막으로, 모든 데이터가 캐시에 적합한 것도 아닙니다. 매우 민감한 데이터, 즉시 최신성이 중요한 데이터, 거의 재사용되지 않는 데이터는 캐싱 이득보다 위험이나 비용이 더 클 수 있습니다.

캐시의 대표적인 부작용
  • stale data로 인한 최신성 문제
  • local cache 간 값 불일치
  • cold start 시 hit rate 급락
  • 키 폭증과 메모리 비용 증가
  • stampede / thundering herd
  • 잘못 설계된 키로 인한 데이터 누수 또는 오염

자주 하는 실수

캐시 문제의 대부분은 기술 선택보다도 키 설계, 만료 전략, 무효화 규칙, 계층 구분을 가볍게 본 데서 시작된다
  • 캐시를 원본처럼 취급해서 stale data를 과소평가함
  • TTL을 두지 않음 또는 TTL을 감으로만 잡음
  • evictionexpiration을 같은 개념으로 이해함
  • 사용자별·권한별 데이터를 같은 키로 캐시해서 섞어 버림
  • 쓰기 후 invalidation을 빼먹어 오래된 값을 오래 노출함
  • 모든 데이터를 캐시하려고 하다 메모리만 낭비함
  • local cache를 여러 인스턴스에 쓰면서 값 불일치를 무시함
  • 인기 키 만료 시 stampede를 전혀 대비하지 않음

실무 루틴

캐시를 도입할 때는 “어디에 넣을까”보다 먼저 어떤 데이터가 반복되고, 정답 저장소가 어디이며, 최신성이 얼마나 중요한지를 정리하는 순서가 맞다
  1. 반복 조회가 많고 원본 비용이 큰 데이터부터 찾는다.
  2. 그 데이터의 source of truth가 어디인지 분명히 한다.
  3. 로컬 캐시, 분산 캐시, HTTP/CDN 캐시 중 어디에 둘지 정한다.
  4. Cache-Aside인지 Write-Through인지 전략을 고른다.
  5. TTL, invalidation, eviction policy를 함께 정한다.
  6. cache key에 버전 / 사용자 / locale / tenant 같은 구분자가 필요한지 확인한다.
  7. hit rate, latency, DB load, evictions, stale incidents를 모니터링한다.

디버깅

캐시 문제를 디버깅할 때는 “캐시가 있다/없다”보다 먼저 hit인지 miss인지, 어떤 계층의 캐시인지, 오래된 값인지, 메모리 압박으로 날아간 것인지를 분리해서 봐야 한다
1
먼저 문제가 난 요청이 cache hit였는지 cache miss였는지 확인한다.
2
브라우저 / CDN / 앱 메모리 / 분산 캐시 중 어느 계층에서 응답이 나온 것인지 나눈다.
3
값이 오래되었다면 TTL 만료 전인지, 쓰기 후 invalidation 누락인지, local cache 불일치인지를 본다.
4
키가 사라졌다면 TTL 만료인지 eviction인지 구분한다. 둘은 운영 대응이 다르다.
5
같은 키에 동시에 많은 miss가 났다면 stampede 가능성을 의심하고 lock 또는 coalescing이 있는지 확인한다.
자주 보는 지표
hit rate = hits / (hits + misses)
miss rate = misses / (hits + misses)

함께 봐야 할 것
- p95 / p99 latency
- DB read QPS
- cache evictions
- expired keys
- stampede 발생 키
- stale data 관련 오류 수

요약

캐시의 본질은 원본 데이터를 버리는 것이 아니라 반복 비용을 더 빠른 계층으로 옮겨 지연과 부하를 줄이는 데 있으며, 실제 설계에서는 패턴 선택보다 TTL·eviction·invalidation·정합성·계층 구분을 함께 다루는 것이 더 중요하다
  • ✅ 캐시는 더 빠른 계층에 복사본을 둬 같은 요청을 더 빠르게 처리하는 가속 장치다.
  • ✅ cache hit / miss를 기준으로 성능과 부하가 갈린다.
  • ✅ Cache-Aside는 가장 흔한 기본 전략이다.
  • ✅ Read-Through, Write-Through, Write-Behind는 읽기/쓰기 책임을 어디에 둘지에 대한 설계 선택이다.
  • ✅ TTL, eviction, invalidation은 모두 다르며 반드시 구분해서 설계해야 한다.
  • ✅ 캐시는 브라우저, CDN, 애플리케이션 메모리, 분산 캐시처럼 여러 층으로 쌓일 수 있다.
  • ✅ stale data, stampede, key design, invalidation 누락이 가장 흔한 운영 리스크다.
  • ✅ 좋은 캐시는 “빠른 캐시”가 아니라 “빠르면서도 운영 가능한 캐시”다.
728x90