← 블로그 목록

Developer Knowledge

개발자가 알아야 할 지식: HTTP Cache-Control, 빠르게 보여주면서 오래된 데이터를 피하는 법

브라우저와 CDN 캐시가 응답을 저장하고 재사용하는 방식을 Cache-Control, ETag, max-age, no-cache, no-store 관점에서 설명하고 실무 적용 기준을 정리한다.

IETF HTTP Working Group
  • 개발자가 알아야 할 지식
  • Software Engineering
  • HTTP
  • Caching
  • Web Performance

왜 개발자가 알아야 하나

웹 서비스가 느릴 때 가장 먼저 떠올리는 해결책은 서버를 늘리거나 쿼리를 최적화하는 것이다. 물론 둘 다 중요하다. 하지만 많은 화면과 API는 이미 한 번 계산한 응답을 다시 전달하는 데 상당한 시간을 쓴다. 로고, 번들 파일, 상품 이미지, 공개 문서, 설정 메타데이터, 자주 조회되는 목록처럼 매 요청마다 원본 서버까지 갈 필요가 없는 데이터가 많다. HTTP 캐시는 이런 반복 작업을 줄여 지연 시간과 서버 부하를 동시에 낮추는 기본 장치다.

문제는 캐시가 “그냥 빠르게 해주는 기능”으로만 이해될 때다. 캐시 정책을 잘못 잡으면 사용자는 오래된 HTML을 보고, 새 JavaScript와 옛 CSS가 섞이고, 로그인한 사용자의 응답이 CDN에 남고, 운영자는 배포 후 브라우저 새로고침을 안내하게 된다. 반대로 모든 응답에 no-store를 붙이면 신선도 문제는 줄어들지만, 브라우저와 CDN이 줄 수 있는 성능 이점을 버리게 된다. 캐시는 성능 최적화이면서 동시에 데이터 신선도와 개인정보 보호의 계약이다.

HTTP 캐싱의 핵심 언어가 Cache-Control이다. 이 헤더는 응답을 어디에 저장할 수 있는지, 얼마나 오래 신선하다고 볼 수 있는지, 신선하지 않을 때 재검증이 필요한지, 아예 저장하면 안 되는지를 알려준다. RFC 9111은 HTTP 캐시가 응답을 저장하고 재사용하고 재검증하는 규칙을 정리한다. 개발자가 이 규칙을 알면 “왜 내 로컬에서는 바뀌었는데 고객 브라우저에는 안 바뀌지?” 같은 질문을 추측이 아니라 정책으로 설명할 수 있다.

실무에서 이 지식이 중요한 이유는 캐시 계층이 하나가 아니기 때문이다. 브라우저 캐시, 서비스 워커, 프록시, 사내 게이트웨이, CDN 엣지, 프레임워크의 데이터 캐시가 서로 다른 위치에서 응답을 보관할 수 있다. 같은 URL이라도 어떤 헤더를 붙였는지, 요청에 인증 정보가 있는지, CDN이 별도 캐시 규칙을 적용하는지에 따라 동작이 달라진다. 개발자가 Cache-Control을 이해하면 프론트엔드 배포, API 설계, CDN 설정, 장애 대응에서 훨씬 덜 흔들린다.

핵심 개념

HTTP 캐시는 응답을 저장했다가 이후 요청에 재사용하는 중간 저장소다. 캐시에 저장된 응답이 아직 사용할 수 있는 상태라면 fresh, 즉 신선하다고 부른다. 신선한 응답은 원본 서버에 다시 묻지 않고 바로 사용할 수 있다. 신선하지 않은 응답은 stale, 즉 오래된 응답이다. 오래된 응답도 무조건 버려지는 것은 아니지만, 일반적으로는 원본 서버에 재검증하거나 명시적으로 허용된 조건에서만 재사용해야 한다.

max-age는 응답이 생성된 뒤 몇 초 동안 신선한지 알려주는 대표 지시어다. 예를 들어 Cache-Control: public, max-age=3600은 공유 캐시와 브라우저가 응답을 1시간 동안 신선하다고 볼 수 있다는 뜻이다. 이 기간 안에는 같은 요청이 다시 와도 원본 서버까지 가지 않을 수 있다. 정적 이미지나 해시가 붙은 번들 파일처럼 내용이 URL에 의해 고정되는 리소스에 잘 맞는다.

publicprivate은 저장 위치를 구분한다. public은 공유 캐시, 예를 들어 CDN이나 프록시도 저장할 수 있음을 뜻한다. private은 특정 사용자에게만 맞는 응답이므로 공유 캐시에는 저장하면 안 되고, 브라우저 같은 개인 캐시에만 저장할 수 있다는 뜻이다. 사용자 이름, 장바구니 요약, 개인화된 대시보드 HTML처럼 로그인 상태에 따라 달라지는 응답은 기본적으로 공유 캐시 대상이 아니다.

no-cache는 이름 때문에 가장 많이 오해된다. 이 지시어는 “저장하지 말라”가 아니라 “저장해도 되지만 재사용하기 전에는 원본 서버에 재검증하라”에 가깝다. 브라우저가 응답을 보관할 수는 있지만, 다음에 같은 URL을 열 때 ETag나 Last-Modified를 이용해 서버에 아직 유효한지 확인해야 한다. 매번 바뀔 수 있는 HTML 문서에 no-cache가 자주 쓰이는 이유가 여기에 있다. 저장은 허용하되, 배포 후 오래된 HTML을 그대로 쓰지 않게 만들기 좋다.

no-store는 더 강하다. 응답을 캐시에 저장하지 말라는 지시다. 결제 정보, 민감한 개인정보, 일회성 토큰, 의료 정보, 보안상 저장 자체가 곤란한 응답에 사용한다. no-cacheno-store를 섞어서 쓰는 예제를 자주 보지만, 둘의 의미는 다르다. 신선도 때문에 재검증이 필요한 것인지, 저장 자체를 피해야 하는 것인지 먼저 구분해야 한다.

재검증에는 보통 validator가 쓰인다. 서버가 응답에 ETag를 주면 클라이언트는 다음 요청에서 If-None-Match로 그 값을 보내고, 서버는 내용이 바뀌지 않았다면 304 Not Modified를 반환할 수 있다. 이 경우 본문을 다시 내려보내지 않아도 되므로 네트워크 비용을 줄인다. Last-ModifiedIf-Modified-Since도 비슷한 역할을 하지만, 초 단위 시간보다 ETag가 더 정밀한 변경 식별자로 쓰일 때가 많다.

공유 캐시에서는 s-maxage도 중요하다. s-maxage는 CDN 같은 shared cache에 적용되는 신선도 시간을 따로 지정한다. 예를 들어 브라우저에는 짧게 저장시키고 CDN에는 더 오래 저장시키고 싶을 수 있다. Cache-Control: public, max-age=60, s-maxage=600처럼 쓰면 브라우저는 1분, 공유 캐시는 10분을 기준으로 삼을 수 있다. 다만 실제 CDN은 제품별 설정과 오리진 헤더 우선순위가 있으므로 배포 환경 문서를 함께 확인해야 한다.

작은 예시 또는 체크리스트

다음은 흔히 쓰는 캐시 정책을 리소스 성격별로 나눈 예시다.

  • 해시가 붙은 정적 자산: Cache-Control: public, max-age=31536000, immutable
  • 배포 때마다 바뀔 수 있는 HTML: Cache-Control: no-cache
  • 공개 API 목록이 1분 정도 늦어도 되는 경우: Cache-Control: public, max-age=60, stale-while-revalidate=300
  • 사용자별 대시보드 응답: Cache-Control: private, no-cache
  • 토큰, 결제, 민감한 개인정보 응답: Cache-Control: no-store
  • CDN에는 캐시하고 브라우저에는 짧게 유지할 공개 데이터: Cache-Control: public, max-age=30, s-maxage=300

프론트엔드 배포를 예로 들어보자. app.8f3a2c.js처럼 파일명에 콘텐츠 해시가 들어간 자산은 내용이 바뀌면 URL도 바뀐다. 이런 파일은 1년 캐시와 immutable을 줘도 안전하다. 사용자가 예전 파일을 갖고 있어도, 새 HTML은 새 해시 파일을 참조하기 때문이다. 반면 /index.html은 같은 URL에서 새 번들을 참조하도록 바뀐다. HTML을 오래 캐시하면 사용자는 새 배포를 받지 못하거나, 예전 HTML이 더 이상 존재하지 않는 번들을 가리킬 수 있다. 그래서 HTML은 보통 no-cache로 재검증하게 만든다.

API 응답은 비즈니스 의미를 기준으로 나눠야 한다. 공개 문서, 환율 고시처럼 일정 시간 늦어도 되는 데이터는 짧은 max-age와 ETag를 조합할 수 있다. 사용자별 권한, 잔액, 주문 상태처럼 즉시성이 중요한 데이터는 공유 캐시를 피하고 재검증 또는 저장 금지 정책을 둔다. 캐시 가능 여부는 “GET인가?”만으로 결정되지 않는다. 같은 GET이라도 사용자와 시점에 따라 민감도가 다르다.

실무에서 자주 생기는 오해

  • no-cache는 캐시 금지가 아니다. 저장은 가능하지만 재사용 전 재검증이 필요하다는 뜻이다. 저장 자체를 막고 싶다면 no-store를 검토해야 한다.
  • max-age=0은 항상 no-store와 같지 않다. 즉시 stale로 만들 뿐이며, 재검증을 통해 다시 사용할 수 있다.
  • URL이 같으면 캐시도 같은 응답으로 볼 수 있다. 언어, 인증, 압축, 디바이스별 응답이 다르다면 Vary 헤더나 URL 설계를 신중히 다뤄야 한다.
  • CDN 캐시와 브라우저 캐시는 다르다. CDN에서 purge했다고 해서 이미 사용자 브라우저에 저장된 응답이 즉시 사라지는 것은 아니다.
  • 캐시 무효화는 배포 전략과 함께 설계해야 한다. 파일명 해시, 짧은 HTML 재검증, 원자적 배포 순서가 없으면 부분적으로 오래된 화면이 생긴다.
  • 개인정보가 섞인 응답은 “우리 CDN 설정상 안 저장될 것”이라고 기대하지 말고 헤더로도 의도를 명확히 남겨야 한다.
  • 캐시는 장애 대응 도구가 될 수 있지만, 잘못된 응답을 빠르게 전 세계에 퍼뜨리는 도구도 될 수 있다. TTL을 길게 잡을수록 되돌리는 비용도 커진다.

오늘 바로 적용해보기

첫째, 서비스의 주요 응답을 다섯 종류로 분류해보자. HTML 문서, 해시가 붙은 정적 자산, 공개 API, 사용자별 API, 민감한 응답으로 나누면 대부분의 캐시 정책이 정리된다. 각 종류에 대해 원하는 저장 위치와 신선도 시간을 적어보면 헤더가 자연스럽게 나온다.

둘째, 실제 응답 헤더를 확인하자. 브라우저 개발자 도구, curl -I, CDN 대시보드에서 Cache-Control, ETag, Age, Vary, CDN-Cache-Status 같은 값을 본다. 코드에서 의도한 정책과 실제 엣지에서 내려가는 정책이 다를 수 있다. 프레임워크, 웹 서버, CDN이 헤더를 덮어쓰는 경우도 흔하다.

셋째, 배포 흐름을 캐시 관점에서 점검하자. HTML이 새 번들보다 먼저 배포되는지, 옛 HTML이 새 서버에서 여전히 유효한 자산을 참조할 수 있는지, CDN purge가 실패해도 사용자가 깨진 화면을 보지 않는지 확인한다. 해시 파일을 일정 기간 보존하는 정책은 단순하지만 강력한 안전장치다.

넷째, API 리뷰에서 캐시 정책을 질문으로 넣자. “이 응답은 공유 캐시에 저장되어도 되는가?”, “몇 초 늦어도 되는가?”, “재검증할 validator가 있는가?”, “사용자별 정보가 섞이는가?”, “오류 응답도 캐시될 수 있는가?” 같은 질문은 설계 초기에 비용이 작고 운영 중에는 효과가 크다.

다섯째, 캐시를 성능 수치로 관찰하자. CDN hit ratio, origin request 감소량, 304 비율, 캐시된 응답의 p95 지연 시간, purge 후 전파 시간을 모니터링하면 캐시가 실제로 도움이 되는지 알 수 있다. 캐시는 설정 파일 한 줄이 아니라 운영되는 시스템의 일부다.

더 알아보기

오늘의 takeaway

Cache-Control은 “빠르게 보여줄 것인가”와 “얼마나 최신이어야 하는가”를 HTTP 응답마다 명시하는 실무 계약이다.