HTTP 캐싱은 같은 리소스를 반복해서 내려받지 않도록 응답을 저장해 두는 메커니즘입니다. 잘 설정하면 응답 속도가 빨라지고, 서버 부하와 대역폭이 줄며, 오프라인 대응도 수월해집니다. 캐싱은 크게 두 축으로 이해하면 쉽습니다: 얼마나 오래 신선한가(freshness)와 오래됐을 때 여전히 유효한지 확인하는 법(validation)입니다.
Cache-Control: 신선도 제어
Cache-Control 응답 헤더가 캐싱 정책의 중심입니다. 자주 쓰는 지시어는 다음과 같습니다.
max-age=SECONDS: 응답을 몇 초 동안 신선한 것으로 간주할지. 이 시간 안에는 네트워크 요청 없이 캐시에서 바로 제공합니다.no-cache: 캐시에 저장은 하되, 재사용 전 반드시 서버에 유효성을 재확인. '캐시 금지'가 아님에 주의.no-store: 아예 저장하지 말 것. 민감한 개인정보 응답에 사용.private/public:private은 브라우저 같은 특정 사용자 전용,public은 CDN·프록시 등 공유 캐시도 저장 가능.immutable: 콘텐츠가 절대 바뀌지 않으니 신선한 동안 재검증하지 말 것(해시가 붙은 정적 자산에 이상적).
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
ETag: "a1b2c3d4"
Content-Type: text/css
재검증: ETag와 Last-Modified
신선 기간이 지나면 캐시는 저장된 응답을 곧바로 버리지 않고 '아직 유효한가?'를 서버에 묻습니다. 이 조건부 요청에는 두 가지 방식이 있습니다. ETag는 리소스 내용의 지문(버전 식별자)이고, Last-Modified는 마지막 수정 시각입니다. 브라우저는 저장해 둔 ETag 값을 If-None-Match에, Last-Modified 값을 If-Modified-Since에 실어 보냅니다.
GET /style.css HTTP/1.1
If-None-Match: "a1b2c3d4"
HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4"
Cache-Control: public, max-age=3600
내용이 바뀌지 않았다면 서버는 본문 없이 304 Not Modified만 돌려줍니다. 클라이언트는 이미 가지고 있던 캐시를 재사용하고, 전송량은 헤더 몇 줄로 최소화됩니다. 내용이 바뀌었다면 서버는 새 ETag와 함께 200으로 전체 본문을 내려줍니다.
브라우저 캐시와 CDN, 그리고 캐시 무효화
캐시는 여러 계층에 존재합니다. 사용자의 브라우저 캐시, 그 앞의 CDN·역방향 프록시 같은 공유 캐시입니다. public으로 표시된 응답은 CDN이 저장해 전 세계 사용자에게 가까운 위치에서 빠르게 제공할 수 있습니다. 문제는 긴 max-age를 걸어 두면 콘텐츠를 바꿔도 사용자가 옛날 버전을 계속 본다는 점입니다.
이때 쓰는 표준 기법이 캐시 버스팅(cache busting)입니다. 파일 이름이나 쿼리 문자열에 내용 해시를 넣어(app.9f3a2b.js) 콘텐츠가 바뀔 때마다 URL 자체가 달라지게 만드는 방식입니다. URL이 새로우니 캐시에 없는 것으로 취급되어 새 파일이 내려가고, 바뀌지 않은 파일은 max-age=31536000, immutable로 1년간 마음껏 캐시할 수 있습니다.
Vary: 같은 URL, 다른 응답
같은 URL이라도 요청 헤더에 따라 다른 응답을 줄 수 있습니다. 예를 들어 Accept-Encoding에 따라 gzip 압축본과 원본이 갈리고, Accept-Language에 따라 언어가 달라집니다. Vary 응답 헤더는 '이 응답은 나열된 요청 헤더 값에 따라 달라진다'고 캐시에 알려서, 캐시가 잘못된 변형을 다른 사용자에게 제공하지 않도록 막습니다. Vary: Accept-Encoding을 빠뜨리면 압축을 지원하지 않는 클라이언트에게 gzip 응답이 잘못 전달되는 사고가 날 수 있습니다.