응답에 max-age=3600이 있으면 앞으로 1시간 동안 무조건 새것처럼 보일까요? 사실은 중간 캐시가 이미 얼마 동안 들고 있었는지도 같이 봐야 해요.
CDN, Cache, 그리고 Edge Delivery에서는 사용자 가까이에 복사본을 두면 오리진까지 매번 가지 않아도 된다는 큰 그림을 봤어요. 그리고 브라우저 waterfall에서는 같은 요청이라도 캐시에서 끝난 것과 네트워크로 나간 것이 다르게 보일 수 있다는 감각을 잡았죠.
이번에는 그 복사본이 아직 믿고 써도 되는지를 응답 헤더에서 읽어볼게요.
실제 운영 중에는 이런 장면을 자주 만나요.
max-age=3600만 눈에 들어와요.
“아, 1시간 캐시구나.”맞아요. 그런데 여기서 끝내면 반쪽만 읽은 거예요.
Age: 1240은 이 응답이 공유 캐시 안에서 이미 1240초 정도 나이를 먹었다는 힌트거든요. 오늘의 질문은 이거예요.
“이 응답은 지금 새 원본일까요, 아직 신선한 복사본일까요, 아니면 곧 다시 확인해야 할 사본일까요?”HTTP 캐시의 기준 동작은 RFC 9111: HTTP Caching에 정리돼 있어요. 이 글에서는 표준 용어를 바닥에 두되, 브라우저와 CDN 앞에서 어떤 순서로 헤더를 읽으면 덜 헷갈리는지에 집중할게요.
여기서는
Cache-Control과 Age를 중심으로 fresh, stale, shared cache, private cache, revalidation 감각을 잡아요. ETag, Last-Modified, If-None-Match처럼 다시 확인하는 조건부 요청은 다음 글에서 더 자세히 열어볼게요.빵집 진열대에도 만든 시간과 판매 가능 시간이 같이 붙어 있어요
동네 빵집에서 빵을 산다고 해볼게요.- 빵에는 만든 시간이 있어요.
- 진열대에는 오늘 안에 판매 가능 같은 규칙이 있어요.
- 손님은 빵이 막 나왔는지, 몇 시간 지났는지 같이 봐요.
- 같은 빵이라도 냉장 진열대와 카운터 진열대의 규칙이 다를 수 있어요.
핵심은
Cache-Control이 얼마나 오래 믿어도 되는지를 말하고, Age가 이미 얼마나 오래 캐시에 있었는지를 말한다는 점이에요.
이 그림에서 max-age는 전체 유효 시간표이고, Age는 그 시간표에서 이미 지나간 시간이에요. 그래서 둘을 따로 읽으면 캐시 상태를 오해하기 쉬워요.
먼저 Cache-Control을 큰 방향으로 읽어요
Cache-Control은 하나의 값처럼 보이지만, 안쪽에는 여러 directive가 쉼표로 붙어요.
여기서 fresh는 “캐시가 아직 새것으로 취급해도 되는 상태”예요. 반대로 stale은 “저장된 사본은 있지만, 그대로 쓰기 전에 다시 확인해야 할 수 있는 상태”예요.
max-age는 “지금부터”가 아니라 “응답이 만들어진 뒤부터”예요
많이 헷갈리는 지점이 여기예요.Date, 로컬 시계, 전송 지연, 저장 시간 같은 값을 함께 써서 현재 나이를 계산할 수 있어요. 그래서 max-age에서 Age를 빼는 값은 운영자가 남은 freshness를 빠르게 가늠하는 근사치이지, 캐시의 정확한 시간 회계를 그대로 재현한 값은 아니에요.
s-maxage는 공유 캐시에 따로 주는 시간표예요
이번에는 이런 응답을 볼게요.s-maxage의 s는 shared cache 쪽으로 보면 좋아요. CDN, 프록시처럼 여러 사용자가 함께 쓰는 캐시가 s-maxage를 이해하면, 공유 캐시에서는 max-age보다 s-maxage가 우선이에요.
이런 설정은 꽤 현실적이에요. 브라우저에는 짧게 저장하게 하고, CDN에는 조금 더 오래 들고 있게 해서 오리진 부담을 줄이고 싶을 수 있거든요.
이 그림은 같은 응답이라도 어느 캐시가 읽느냐에 따라 시간표가 달라질 수 있다는 점을 보여줘요. 그래서 브라우저 Network 탭만 보고 CDN의 판단까지 단정하면 안 돼요.
no-cache는 “저장 금지”가 아니에요
이름 때문에 가장 많이 헷갈리는 directive가no-cache예요.
no-cache는 보통 저장은 할 수 있지만, 재사용하기 전에는 반드시 다시 확인해야 한다는 뜻으로 읽어야 해요.
그래서 개인정보, 결제 화면, 민감한 API 응답에서는
no-cache만 보고 안심하면 부족할 수 있어요. 정말 저장 자체를 피해야 한다면 no-store가 필요한 장면이 많아요.
public과 private은 누가 써도 되는 사본인지 가르는 힌트예요
캐시에는 크게 두 감각이 있어요.- private cache: 한 사용자만 쓰는 브라우저 캐시 같은 곳
- shared cache: 여러 사용자가 함께 거치는 CDN, 프록시 같은 곳
여기서
Vary는 다음 글에서 더 크게 볼 주제예요. 같은 URL이라도 요청 헤더가 다르면 다른 사본으로 봐야 할 수 있거든요.
브라우저 Network 탭에서는 이렇게 좁혀봐요
브라우저에서 캐시가 의심될 때는 응답 헤더만 보지 말고, 요청 행의 상태와 크기도 같이 봐요.어떤 서비스는
cf-cache-status, 어떤 서비스는 x-cache, x-cache-status, cdn-cache-control 같은 다른 이름을 쓸 수 있어요. 이름보다 HIT인지 MISS인지, BYPASS인지, Age와 서로 말이 맞는지를 먼저 보세요.예시로 세 응답을 같이 읽어볼게요
1. 아직 신선한 정적 파일
Age가 하루나 됐지만, max-age가 1년이라면 아직 fresh로 볼 수 있어요. 해시 파일명까지 붙어 있다면 꽤 자연스러운 설정이에요.
읽는 순서는 이래요.
2. 곧 다시 확인해야 할 HTML
3. 저장은 되지만 매번 확인해야 하는 응답
ETag 같은 validator로 “이 사본 그대로 써도 되나요?”를 다시 물어봐야 해요.
이 흐름에서 캐시는 본문을 다시 내려받지 않을 수 있지만, 오리진 확인 자체는 필요해요. 그래서 no-cache 응답은 “항상 네트워크 비용 0”이 아니에요.
잘못 읽기 쉬운 함정
Age가 있으면 무조건 CDN HIT라고 보기
Age는 공유 캐시를 거친 힌트가 될 수 있지만, 제품별 헤더와 로그를 같이 봐야 해요. CDN이 Age를 갱신하는 방식, 중간 프록시, 브라우저 캐시 표시가 섞일 수 있어요. 가능하면 CDN 상태 헤더와 함께 읽으세요.
Age가 없으면 캐시가 안 됐다고 단정하기
브라우저 메모리 캐시나 디스크 캐시는 Network 탭에 (memory cache), (disk cache)처럼 보일 수 있고, 응답 헤더에 항상 기대한 형태의 Age가 남지는 않을 수 있어요. 공유 캐시와 브라우저 캐시는 관측 신호가 달라요.
no-cache와 no-store를 같은 뜻으로 보기
둘은 달라요. no-cache는 재사용 전 재검사, no-store는 저장 금지에 가까워요. 민감한 응답에서는 이 차이가 보안과 개인정보 노출로 이어질 수 있어요.
긴 max-age를 HTML에 아무 생각 없이 붙이기
해시가 붙은 정적 파일은 긴 캐시가 잘 맞지만, HTML은 배포와 라우팅의 시작점일 때가 많아요. HTML을 너무 오래 공유 캐시에 두면 새 JavaScript 파일 경로나 새 설정이 늦게 퍼질 수 있어요.
public이면 모든 응답이 안전하다고 보기
public은 공유 캐시에 저장될 수 있다는 강한 힌트예요. 사용자별 내용, 쿠키 영향, 인증 헤더, Vary 조건을 제대로 보지 않으면 다른 사람에게 보여주면 안 되는 응답이 재사용될 수 있어요.
장애나 이상 증상을 만나면 이렇게 좁혀봐요
실전에서는 아래 질문을 남기면 좋아요.자, 정리해볼까요?
Cache-Control은 캐시가 응답을 저장해도 되는지, 얼마나 fresh로 볼지, 언제 다시 확인할지를 알려줘요.Age는 공유 캐시 안에서 응답이 이미 얼마나 오래 있었는지 보여주는 중요한 힌트예요.max-age=3600은 “브라우저가 지금 받은 순간부터 3600초”가 아니라, 응답 freshness lifetime을 말해요. 중간 캐시의 Age를 같이 봐야 해요.s-maxage는 CDN 같은 shared cache에서 max-age보다 우선할 수 있어요.no-cache는 저장 금지가 아니라 재사용 전 재검사에 가깝고, no-store는 저장 자체를 피해야 하는 장면에 가까워요.정적 파일, HTML, 로그인 응답, API 응답은 캐시 전략을 다르게 읽어야 해요.이어서 보면 좋은 글
- CDN, Cache, 그리고 Edge Delivery — 캐시 히트와 미스, 오리진과 엣지의 큰 그림으로 잠깐 돌아가고 싶을 때 좋아요.
- 브라우저 waterfall은 어디부터 읽어야 할까요? — 캐시된 요청과 네트워크 요청이 페이지 로딩 시간표에서 어떻게 보이는지 같이 읽을 수 있어요.
- HTTP/1.1 메시지는 왜 빈 줄 하나가 중요할까요? — 응답 시작 줄, 헤더, 본문 경계를 더 기본 구조부터 보고 싶을 때 좋아요.
이어서 볼 질문
다음에는 같은 URL처럼 보여도 왜 캐시가 서로 다른 사본을 만들 수 있는지,Vary와 캐시 키를 중심으로 이어서 볼 수 있어요.