캐시란 무엇인가 — 웹 성능을 좌우하는 임시 저장의 원리

같은 웹페이지를 두 번째 열 때 훨씬 빠르게 뜨는 경험, 누구나 한 번쯤 해봤을 것이다. 그 속도 차이의 배후에는 대부분 캐시(cache)가 있다. 캐시는 한 번 가져온 데이터를 가까운 곳에 임시로 저장해 두었다가, 다음에 같은 요청이 오면 원본까지 다녀오지 않고 바로 돌려주는 기법이다. 단순해 보이지만, 브라우저·서버·데이터베이스·CPU까지 IT 시스템의 거의 모든 계층이 이 원리 위에서 돌아간다. 이 글은 캐시가 정확히 무엇이고, 어디에 어떻게 존재하며, 왜 “캐시 무효화가 컴퓨터 과학에서 가장 어려운 문제 중 하나”로 불리는지를 개발자 관점에서 정리한다.

캐시란 무엇인가 — 임시 저장으로 시간을 사는 기법

캐시의 핵심 아이디어는 한 문장으로 요약된다. 비싼 작업의 결과를 싼 저장소에 보관해 두고 재사용하는 것. 여기서 “비싸다”는 것은 시간이 오래 걸리거나(원격 서버 왕복, 디스크 읽기), 계산 비용이 크다(복잡한 DB 쿼리, 이미지 리사이징)는 뜻이다.

예를 들어 어떤 API가 데이터베이스에서 인기 상품 목록을 만드는 데 200밀리초가 걸린다고 하자. 이 목록이 1분에 한 번만 바뀐다면, 매 요청마다 DB를 두드릴 이유가 없다. 첫 계산 결과를 메모리에 담아 두고 1분 동안 재사용하면, 이후 요청은 1밀리초 이내에 응답할 수 있다. 이렇게 데이터를 임시로 담아 두는 저장 공간이 캐시이고, 그 안에 결과를 넣는 행위를 캐싱(caching)이라고 부른다.

캐시가 효과를 내는 이유는 실제 데이터 접근 패턴이 균일하지 않기 때문이다. 컴퓨터 과학에서는 이를 지역성(locality) 이라고 부른다. 방금 쓴 데이터를 곧 다시 쓸 가능성이 높고(시간 지역성), 어떤 데이터를 쓰면 그 근처 데이터도 함께 쓸 가능성이 높다(공간 지역성). 소수의 인기 데이터가 전체 요청의 대부분을 차지하는 현상 — 흔히 파레토 법칙에 비유되는 이 편향 — 덕분에, 작은 캐시만으로도 대다수 요청을 흡수할 수 있다.

캐시 히트와 캐시 미스

캐시의 성능은 두 가지 사건으로 측정된다.

  • 캐시 히트(cache hit): 요청한 데이터가 캐시에 이미 있어 그대로 돌려주는 경우. 빠르다.
  • 캐시 미스(cache miss): 캐시에 없어서 원본(origin)까지 가서 가져와야 하는 경우. 느리다. 그리고 대개 이때 가져온 데이터를 캐시에 새로 채운다.

전체 요청 중 히트가 차지하는 비율을 히트율(hit ratio) 이라 하며, 캐시 설계의 성패를 가르는 가장 중요한 지표다. 히트율이 95%라면 스무 번 중 열아홉 번은 원본을 건드리지 않고 응답한다는 뜻이다. Cloudflare 같은 CDN 사업자가 자사 성능을 이야기할 때 히트율을 앞세우는 것도 이 때문이다(Cloudflare, “What is caching?”).

캐시는 어디에나 있다 — 계층별 캐시 지도

캐시가 특별한 이유는 특정 제품이 아니라 아키텍처 전반에 반복해서 등장하는 패턴이라는 데 있다. 사용자의 클릭 한 번이 화면에 반영되기까지, 데이터는 여러 겹의 캐시를 통과한다. 아래는 하드웨어에 가까운 쪽부터 사용자에 가까운 쪽까지 훑은 캐시 지도다.

계층 캐시 예시 무엇을 캐싱하나
CPU L1·L2·L3 캐시 메인 메모리의 데이터·명령어
운영체제 페이지 캐시, 디스크 버퍼 디스크에서 읽은 파일 블록
애플리케이션 Redis, Memcached DB 쿼리 결과, 세션, 계산 결과
데이터베이스 버퍼 풀, 쿼리 캐시 자주 읽는 테이블·인덱스 페이지
리버스 프록시·CDN Nginx 캐시, 엣지 캐시 HTML·이미지·JS 등 정적 자원
브라우저 HTTP 캐시, 서비스 워커 이미 받은 웹 리소스
  • CPU 캐시: 프로세서 바로 옆에 있는 초고속 메모리다. 메인 메모리(RAM)는 CPU 입장에서 너무 느리기 때문에, 자주 쓰는 데이터를 L1·L2·L3라는 3단계 캐시에 미리 올려 둔다. 용량이 작을수록 빠르고 비싸다는 캐시의 근본 트레이드오프가 여기서 가장 극명하게 드러난다.
  • 애플리케이션 캐시: 서버 개발에서 가장 자주 손대는 계층이다. Redis나 Memcached 같은 인메모리 저장소에 DB 쿼리 결과나 세션 정보를 담아 둔다. Redis 공식 문서는 이런 인메모리 캐싱을 지연시간을 밀리초 이하로 낮추는 대표 기법으로 소개한다.
  • CDN·리버스 프록시 캐시: 사용자와 가까운 엣지 서버에 콘텐츠 사본을 두어, 원본 서버까지의 물리적 거리를 줄인다. 서울 사용자가 미국 원본 서버 대신 국내 엣지에서 이미지를 받으면 지연시간이 크게 줄어든다.
  • 브라우저 캐시: 사용자 기기에 저장되는 가장 마지막 캐시다. 한 번 받은 CSS·이미지·폰트를 로컬 디스크에 보관해, 재방문 시 네트워크 요청 자체를 생략한다.

같은 데이터가 여러 계층에 동시에 캐싱될 수 있다는 점이 중요하다. 하나의 이미지가 CDN 엣지에도, 브라우저에도 사본으로 존재할 수 있다. 이 다중성이 성능을 끌어올리는 동시에, 뒤에서 다룰 무효화 문제를 까다롭게 만든다.

웹 캐시의 핵심 — HTTP 캐싱과 Cache-Control

웹 개발자가 가장 직접적으로 다루는 캐시는 HTTP 캐시다. 웹은 처음부터 캐싱을 염두에 두고 설계됐다. 실제로 REST 아키텍처의 6대 제약 중 하나가 “캐시 가능성(cacheable)”일 만큼, 응답을 재사용 가능하게 만드는 것은 웹의 기본 원리다(관련 개념은 REST API란 무엇인가 글에서 자세히 다룬다).

HTTP에서 캐시 동작을 지시하는 핵심은 Cache-Control 응답 헤더다. 서버가 이 헤더로 “이 응답을 얼마나, 어떻게 저장해도 되는지”를 명시한다.

# 1시간 동안 캐싱 허용, 공용 캐시(CDN)에도 저장 가능
Cache-Control: public, max-age=3600

# 매번 서버에 재검증 요청 (저장은 하되 그냥 쓰지 말 것)
Cache-Control: no-cache

# 절대 저장 금지 (민감 정보)
Cache-Control: no-store

# 만료됐어도 일단 옛 응답을 주고, 뒤에서 몰래 갱신
Cache-Control: max-age=600, stale-while-revalidate=30

MDN의 HTTP 캐싱 문서는 이 디렉티브들을 정확히 이해하는 것이 웹 성능 튜닝의 출발점이라고 설명한다. 특히 자주 혼동되는 두 값을 짚어 둘 필요가 있다.

  • no-cache는 “캐싱하지 말라”가 아니다. 저장은 하되, 재사용 전에 서버에 “이거 아직 유효한가?”를 물어보라는 뜻이다.
  • 진짜로 저장을 막는 것은 no-store다. 로그인 후 개인정보 페이지처럼 절대 남으면 안 되는 응답에 쓴다.

조건부 요청과 재검증

캐시된 데이터가 만료됐다고 해서 항상 전체를 다시 받을 필요는 없다. HTTP는 조건부 요청으로 “바뀐 게 있을 때만 새로 달라”는 대화를 지원한다. 서버는 응답에 ETag(콘텐츠의 지문 역할을 하는 식별자)나 Last-Modified를 실어 보낸다. 브라우저는 다음 요청에서 If-None-Match로 그 값을 되돌려주고, 콘텐츠가 그대로면 서버는 본문 없이 304 Not Modified만 응답한다. 실제 데이터 전송 없이 “안 바뀌었음”만 확인하는 이 방식은 대역폭을 크게 아낀다(Google web.dev, “HTTP caching”).

캐시의 가장 어려운 문제 — 무효화와 TTL

넷스케이프의 엔지니어 필 칼튼(Phil Karlton)이 남긴 유명한 농담이 있다. “컴퓨터 과학에는 어려운 문제가 딱 두 개 있다. 캐시 무효화, 그리고 이름 짓기.” 반쯤 농담이지만, 캐시 무효화(cache invalidation) 가 실무에서 얼마나 골치 아픈지를 정확히 짚은 말이다.

문제의 본질은 이렇다. 캐시는 원본의 사본이다. 원본이 바뀌면 사본은 순간적으로 틀린 값이 된다. 이 “낡은 데이터(stale data)”를 언제, 어떻게 걷어낼 것인가가 무효화의 과제다. 너무 오래 들고 있으면 사용자가 옛 정보를 보고, 너무 자주 버리면 캐시가 무의미해진다.

가장 흔한 해법은 TTL(Time To Live) 이다. 각 캐시 항목에 유효기간을 정해 두고, 그 시간이 지나면 자동으로 만료시킨다. 위의 max-age=3600이 바로 TTL 지정이다. 구현이 단순하고 예측 가능하다는 게 장점이지만, “1시간 뒤 만료”는 곧 “최악의 경우 59분 낡은 데이터를 보여줄 수 있다”는 뜻이기도 하다. 데이터 성격에 맞는 TTL을 고르는 것 자체가 설계 판단이다.

쓰기 전략 — 원본과 캐시를 어떻게 맞추나

캐시에 담긴 데이터를 갱신하는 방식에도 여러 전략이 있다.

  • Write-through(쓰기 관통): 데이터를 쓸 때 캐시와 원본을 동시에 갱신한다. 항상 일치하지만 쓰기가 느리다.
  • Write-back(쓰기 지연): 캐시에만 먼저 쓰고 원본은 나중에 몰아서 반영한다. 빠르지만 캐시가 날아가면 데이터를 잃을 위험이 있다.
  • Cache-aside(캐시 우회): 애플리케이션이 직접 캐시를 확인하고, 없으면 DB에서 읽어 캐시를 채운다. 가장 널리 쓰이는 패턴으로, AWS의 캐싱 개요 문서도 대표 전략으로 소개한다.

여기에 하나 더, 실무에서 무섭게 다가오는 상황이 캐시 스탬피드(cache stampede) 다. 인기 항목의 캐시가 만료되는 순간, 수천 개의 요청이 동시에 캐시 미스를 겪고 한꺼번에 원본 DB로 몰려가 서버를 마비시키는 현상이다. 이를 막으려고 만료 시각을 살짝 흩뜨리거나(jitter), 하나의 요청만 원본을 다녀오게 잠금을 거는 기법을 쓴다.

캐시 전략, 무엇을 저울질해야 하나

캐시는 공짜 성능이 아니다. 도입하는 순간 일관성(consistency)과 성능 사이의 트레이드오프를 떠안게 된다. 캐시가 있는 시스템은 본질적으로 “약간 낡았을 수도 있는 데이터”를 허용하는 시스템이다. 그래서 캐시를 붙이기 전에 이런 질문을 먼저 던져야 한다.

  • 이 데이터는 얼마나 자주 바뀌는가? 자주 바뀌는 데이터에 긴 TTL을 걸면 사용자가 틀린 값을 본다.
  • 잠깐 낡은 값을 보여줘도 괜찮은 데이터인가? 상품 조회수는 괜찮지만, 계좌 잔액은 곤란하다.
  • 캐시가 통째로 사라져도 시스템이 버티는가? 캐시는 어디까지나 보조 장치여야 하며, 원본이 진실의 원천(source of truth)이어야 한다.

특히 실시간 이벤트를 다루는 웹훅 기반 알림이나, 매 요청마다 상태가 바뀌는 서비스라면 캐시 정책을 더 보수적으로 잡아야 한다. 반대로 쿠버네티스로 여러 인스턴스를 수평 확장하는 환경에서는, 각 노드가 제각각 캐시를 들고 있으면 일관성이 더 복잡해지므로 Redis 같은 공유 캐시를 두는 편이 낫다.

한 가지 솔직히 인정할 점은, 완벽한 캐시 전략은 없다는 것이다. 캐시는 성능을 벌어오는 대신 복잡성과 미세한 오차를 지불하는 거래다. 그래서 숙련된 팀일수록 “일단 캐시부터”가 아니라, 실제 병목을 측정한 뒤에 필요한 계층에만 캐시를 얹는다.

정리 — 캐시를 제대로 쓰기 위한 관점

캐시는 IT 인프라 전반에 스며 있는 가장 보편적인 성능 도구다. 핵심을 다시 짚으면 이렇다.

  1. 본질: 비싼 작업의 결과를 가까운 곳에 임시 저장해 재사용한다. 성능은 히트율로 측정한다.
  2. 편재성: CPU L1/L2/L3부터 애플리케이션(Redis)·CDN·브라우저까지, 데이터는 여러 캐시 계층을 통과한다.
  3. 제어: 웹에서는 Cache-Control·ETag·조건부 요청으로 캐시 동작을 세밀하게 지시한다.
  4. 난제: 무효화와 TTL이 캐시의 가장 어려운 부분이다. write-through/back, cache-aside, 스탬피드 방어를 상황에 맞게 골라야 한다.
  5. 트레이드오프: 캐시는 일관성을 조금 내주고 속도를 사는 거래다. 원본이 항상 진실의 원천이어야 한다.

결국 캐시를 잘 쓴다는 것은, 무작정 빠르게 만드는 게 아니라 “얼마나 낡은 데이터까지 허용할 수 있는가”를 데이터마다 판단하는 일에 가깝다. 여러분이 다루는 서비스에서 어떤 데이터가 캐싱하기 좋은 후보이고, 어떤 데이터는 절대 캐싱하면 안 되는지 한번 나눠 보면, 캐시라는 개념이 훨씬 구체적으로 잡힐 것이다. 다음 글에서는 이 캐시 원리가 웹 전체 규모로 확장된 CDN(콘텐츠 전송 네트워크) 을 다룬다.


참고 자료

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다