이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- HTTP cache header가 브라우저, CDN, proxy에서 어떻게 다르게 해석되는가?
max-age,s-maxage,no-cache,no-store,private,public을 어떤 API에 적용해야 하는가?- 캐시 때문에 배포/로그인/개인화 응답이 이상해질 때 어떤 증거부터 확인하는가?
개요
HTTP cache는 같은 응답을 다시 계산하거나 다시 전송하지 않기 위한 표준 메커니즘이다. 백엔드 개발자에게 cache는 단순 성능 옵션이 아니라 API 응답 계약이다. 응답 header 하나가 브라우저, CDN, 사내 proxy, mobile WebView에서 서로 다른 장애 양상으로 나타날 수 있다.
- 정적 asset은 fingerprint와 긴 TTL이 잘 맞는다.
- HTML shell은 배포 전환을 위해 짧은 TTL이나 재검증이 필요할 수 있다.
- 인증/개인화 API는 공유 cache 저장을 기본적으로 의심해야 한다.
- cache HIT이면 origin 로그가 없을 수 있으므로 “앱에 안 찍힌다”가 곧 client 미요청은 아니다.
원리
HTTP cache 판단은 크게 세 질문이다.
- 이 응답을 저장할 수 있는가?
- 저장한 응답을 지금 재사용해도 신선한가?
- 신선하지 않다면 origin에 재검증할 수 있는가?
이 질문의 답은 Cache-Control, Expires, ETag, Last-Modified, Vary, 요청의 Cache-Control에 의해 결정된다.
Cache-Control 지시어
| 지시어 | 의미 | 백엔드 판단 |
|---|---|---|
max-age=60 | 60초 동안 fresh | 짧은 최신성 허용 API에 사용 가능 |
s-maxage=300 | shared cache에서만 300초 fresh | CDN은 길게, 브라우저는 짧게 둘 때 유용 |
public | 공유 cache 저장 가능 | 완전 공개 응답에만 사용 |
private | 개인 cache에만 저장 | 사용자별 응답에 사용 가능하지만 민감도 확인 |
no-cache | 저장 가능, 재사용 전 재검증 | HTML, 자주 바뀌는 API에 사용 가능 |
no-store | 저장 금지 | 인증 token, 결제, 개인정보 응답 |
must-revalidate | stale 재사용 제한 | 정합성이 중요한 응답 |
stale-while-revalidate | 오래된 응답을 잠시 주며 백그라운드 재검증 | 인기 공개 데이터에 유용 |
stale-if-error | origin 오류 때 stale 응답 허용 | 읽기 중심 공개 컨텐츠에 유용 |
리소스 유형별 출발점
| 리소스 | 예시 정책 | 이유 |
|---|---|---|
| fingerprint asset | public, max-age=31536000, immutable | 파일명이 바뀌면 새 리소스 |
| HTML entry | no-cache 또는 짧은 max-age | 배포 후 새 asset 참조 필요 |
| 공개 목록 API | public, s-maxage=60, stale-while-revalidate=30 | 짧은 stale 허용 가능 |
| 사용자 프로필 | private, no-cache 또는 no-store | 사용자별 응답 |
| 로그인/결제 응답 | no-store | 민감 정보와 재사용 위험 |
Vary의 역할
Vary는 요청 header에 따라 다른 응답이 만들어진다는 신호다. 예를 들어 gzip과 br 압축, 언어, origin별 CORS 응답은 같은 URL이라도 응답이 달라질 수 있다.
Vary: Accept-Encoding
Vary: OriginVary: Origin 없이 origin별 CORS 응답을 cache하면 A origin에 허용된 응답이 B origin에 재사용될 수 있다. Vary: Accept-Encoding 없이 압축 응답이 섞이면 클라이언트가 해석하지 못하는 body를 받을 수 있다.
코드와 명령으로 확인하기
Spring MVC에서 공개 cache header를 명시하는 예시다.
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(1)).cachePublic())
.eTag("\"products-v42\"")
.body(products);민감 응답은 저장을 막는다.
return ResponseEntity.ok()
.cacheControl(CacheControl.noStore())
.body(accountSummary);CLI로 응답을 확인한다.
curl -I https://www.example.com/
curl -I https://static.example.com/assets/app.8f3a1.js
curl -I -H 'Cache-Control: no-cache' https://api.example.com/products확인할 header는 Cache-Control, Age, ETag, Last-Modified, Vary, CDN별 cache status다.
인프라 협업 포인트
- CDN이 origin의
Cache-Control을 그대로 따르는지, 별도 rule로 override하는지 확인한다. s-maxage, CDN page rule, origin header 중 우선순위를 문서화한다.- 인증 cookie나 Authorization header가 있는 요청을 CDN이 cache하지 않도록 정책을 맞춘다.
Vary: Origin이나Vary: Accept-Encoding을 CDN cache key와 함께 검증한다.
주니어가 자주 하는 오해
no-cache를 저장 금지로 이해한다.- CDN HIT이면 앱 서버가 요청을 받지 않았다는 사실을 장애로만 본다.
public을 붙이면 더 빠르니 API에도 넓게 써도 된다고 생각한다.- DevTools에서 새로고침해 보이는 결과를 모든 사용자의 cache 상태로 일반화한다.
시니어의 설계 판단 기준
- endpoint별로 허용 가능한 stale 시간과 공유 cache 저장 가능성을 먼저 정한다.
- cache key가 사용자, 언어, origin, encoding, query string 차이를 반영하는지 본다.
- 개인화 응답은 성능보다 데이터 누출 위험을 우선한다.
- 기업 운영에서는 CDN rule override, purge 권한, 변경 승인, rollback 전략을 API 변경과 함께 본다.
실전 팁
- cache policy는 API 문서에 포함한다. “이 endpoint는 몇 초 stale을 허용하는가”가 설계 정보다.
no-store는 성능을 포기하는 대신 안전을 얻는 선택이다. 로그인, token, 결제, 계좌, 주문 상세는 보수적으로 시작한다.- 긴 TTL asset은 반드시 fingerprinted filename과 같이 쓴다.
/app.js에 1년 TTL을 주면 배포가 꼬인다. Age가 계속 증가하면 edge cache HIT 가능성이 높다. origin 로그가 없는 것은 정상일 수 있다.- 브라우저 DevTools의 “Disable cache”는 개발자 도구가 열린 상태에만 영향을 주는 경우가 있어 운영 재현 증거로는 충분하지 않다.
- 개인 프로젝트에서는
/assets/*는 길게,/api/*는 보수적으로 시작하고 응답 header를 배포 smoke test에 넣는다.
위험 신호!
- 개인화 API에
public이 붙어 있다. Cache-Control을 framework 기본값에 맡기고 endpoint별 정책표가 없다.- CDN rule이 origin header를 덮어쓰지만 앱 팀이 모른다.
Vary없이 origin별 CORS나 언어별 응답을 cache한다.
확인 질문
확인 질문
no-cache가 저장 금지가 아닌 이유는 무엇인가?
- 저장은 가능하지만 재사용 전에 origin에 재검증하라는 의미이기 때문이다.
s-maxage는 언제 유용한가?
- CDN 같은 shared cache는 길게 저장하고 브라우저 cache는 더 짧게 두고 싶을 때 유용하다.
- 사용자별 응답을 캐시할 때 가장 먼저 확인할 것은 무엇인가?
- 공유 cache에 저장되는지, cache key에 사용자 식별 요소가 들어가는지, 민감 정보가 포함되는지 확인해야 한다.