HTTP 캐시, 빠른 응답 뒤의 제어 구조
HTTP 캐시는 응답 속도만 높이는 장치가 아니라 서버 부하, 네트워크 비용, 장애 대응, 보안 경계를 함께 다루는 인프라 레이어다. Cache-Control, ETag, Vary, stale 전략, 무효화 방식을 기준으로 실무에서 무엇을 캐시하고 무엇을 캐시하면 안 되는지 정리한다.
Script Companion
오디오와 함께 스크립트 보기
- 01
HTTP 캐시는 단순히 빠르게 하는 기술이 아니다. 가까운 캐시에서 응답하면 서버까지 왕복하지 않아 수백 ms를 줄일 수 있고, 동일 요청을 Origin 서버가 반복 처리하지 않아 서버 부하도 줄어든다. CDN 캐시 히트가 나면 데이터 전송 비용도 줄어들며, stale-if-error를 쓰면 Origin 장애 중에도 오래된 캐시로 응답할 수 있다. 다만 캐시 전략을 잘못 잡으면 배포한 변경 사항이 보이지 않거나, 개인정보가 공유 캐시에 저장되는 사고로 이어질 수 있다.
- 02
캐시를 도서관으로 생각하면 구조가 잡힌다. 책상 위의 책은 브라우저 캐시처럼 가장 빠르고 나만 볼 수 있는 저장소다. 열람실 공용 책장은 CDN 캐시처럼 여러 사용자가 함께 쓰는 가까운 저장소이고, 본관 서가는 진짜 원본이 있는 Origin 서버다. 책이 최신인지 확인하는 과정이 캐시 유효성 검사이고, 새 책을 가져오는 과정이 캐시 갱신이다. 여기서 캐시 히트는 저장된 응답을 그대로 반환하는 경우이고, 캐시 미스는 저장된 응답이 없거나 만료되어 다음 계층으로 요청이 넘어가는 경우다.
- 03
프론트엔드에서 자주 헷갈리는 지점은 빌드 캐시와 HTTP 캐시의 차이다. webpack, Vite, Next.js의 빌드 캐시는 개발이나 CI/CD에서 빌드 시간을 줄이는 장치이고, .next/cache나 node_modules/.cache 같은 곳에 저장된다. 반면 HTTP 캐시는 브라우저와 CDN이 HTTP 응답을 받은 뒤 저장하는 장치이며, Cache-Control과 ETag 같은 응답 헤더로 제어한다. 두 캐시는 목적과 시점이 다르지만, app.a1b2c3.js 같은 빌드 결과물을 브라우저가 요청할 때 HTTP 캐시가 적용되면서 연결된다.
- 04
Cache-Control은 HTTP 캐시 제어의 중심이다. max-age는 몇 초 동안 fresh로 볼지 정하고, s-maxage는 CDN 같은 공유 캐시에만 적용된다. public은 브라우저와 공유 캐시 모두 저장 가능하다는 뜻이고, private은 브라우저 같은 개인 캐시에만 저장하라는 뜻이다. no-cache는 캐시 금지가 아니라 저장은 하되 매번 서버에 유효성 검사를 하라는 의미다. 변경이 없으면 304 Not Modified로 본문 없이 빠르게 응답할 수 있다. no-store는 저장 자체를 금지하므로 민감한 금융 정보나 개인정보 응답에 맞다.
- 05
캐시를 설정하지 않는 것도 하나의 위험한 선택이다. Cache-Control이 없는 응답이라도 브라우저는 Last-Modified 등을 보고 자체 휴리스틱으로 캐싱할 수 있다. 문서에서는 API 서버에서 Cache-Control을 전혀 설정하지 않으면 CloudFront는 TTL=0으로 캐시를 거부해도, 브라우저는 휴리스틱으로 캐시할 수 있다고 짚는다. 그래서 API 응답에는 명시적인 Cache-Control이 필요하다. Expires는 HTTP/1.0 시절의 레거시 헤더라 서버와 클라이언트 시계 차이, HTTP date 포맷 파싱 문제, max-age와의 우선순위 충돌을 고려해야 한다.
- 06
검증 기반 캐시는 만료된 응답을 전부 다시 받지 않기 위한 장치다. ETag는 리소스 버전을 식별하는 해시값이고, 클라이언트는 If-None-Match로 이 버전을 서버에 다시 보낼 수 있다. 버전이 같으면 서버는 304 Not Modified를 보내고 본문 전송을 생략한다. Last-Modified와 If-Modified-Since도 비슷하지만 수정 시간을 기준으로 하므로 1초 안에 두 번 수정되는 경우를 감지하지 못할 수 있다. ETag가 있으면 ETag가 우선이고, 없을 때 Last-Modified가 폴백으로 쓰인다.
- 07
Vary 헤더는 캐시 키를 넓히는 장치다. 기본 캐시 키는 URL이지만, Vary를 추가하면 특정 요청 헤더 값까지 캐시 키에 포함된다. 예를 들어 같은 URL이라도 Accept-Encoding에 따라 gzip, br, raw 응답이 나뉠 수 있다. 다만 CDN마다 처리 차이가 있다. CloudFront는 Cache Policy 설정이 필요하고, Cloudflare는 Vary를 완전히 무시하는 경우가 있어 Cache Key Rules가 필요할 수 있다. Vary에 여러 헤더를 넣으면 조합 수만큼 캐시 엔트리가 생기는 combinatorial explosion이 생긴다.
- 08
stale-while-revalidate는 만료 직후의 대기 시간을 줄이는 전략이다. max-age=3600이면 1시간 동안 fresh로 보고, stale-while-revalidate=600이면 그 뒤 10분 동안은 stale 응답을 즉시 제공하면서 백그라운드에서 Origin 갱신을 시도한다. CloudFront는 2023년 5월 공식 지원을 시작했지만, Cache Policy TTL과 s-maxage가 우선될 수 있어 명시적 설정이 필요하다. stale-if-error=86400을 함께 쓰면 Origin 서버 장애 시 최대 24시간 동안 stale 응답으로 서비스를 유지할 수 있다.
- 09
무효화 전략은 무엇을 캐시하느냐에 따라 달라진다. JS, CSS, 이미지 같은 정적 파일은 파일명에 해시를 붙이는 Cache Busting이 가장 효율적이다. URL이 바뀌므로 max-age=31536000과 immutable을 써도 새 파일을 가져올 수 있다. HTML 진입점은 항상 최신 JS와 CSS URL을 참조해야 하므로 no-cache나 max-age=0이 어울린다. 파일명을 바꾸기 어려운 API 응답이나 동적 페이지는 CloudFront Invalidation을 쓸 수 있지만, 전파에 수 분이 걸리고 매월 1,000개 경로를 넘으면 경로당 $0.005 과금된다.
- 10
실무 장애는 대개 캐시 키와 저장 범위에서 시작된다. 배포 후 변경 사항이 반영되지 않는다면 Cache Busting 없이 같은 파일명에 max-age=86400을 준 경우를 의심해야 한다. 로그인 사용자 개인 데이터가 다른 사용자에게 보이면 개인정보 API 응답이 public이거나 Cache-Control이 없어 CDN 공유 캐시에 저장된 상황일 수 있다. CloudFront에서 항상 Miss from cloudfront가 나온다면 no-store나 no-cache, 쿠키가 포함된 캐시 키, Authorization 헤더가 캐시 키에 들어간 구성을 확인해야 한다. 정리하면 HTTP 캐시는 Cache-Control로 저장 위치와 유효 시간을 지시하고, ETag로 본문 전송을 줄이며, Cache Busting과 CDN Invalidation으로 변경 반영을 제어하는 레이어다.
같은 레이어
L2에서 이어 듣기
- 오디오 파일
- /podcasts/l2-http-cache.mp3