[HTTP 웹 기본 지식] 6. HTTP 헤더2 - 캐시와 조건부 요청

건우·2025년 12월 8일

CS / 네트워크

목록 보기
6/6
post-thumbnail

브라우저가 느려지는 이유는 대개 네트워크다. 같은 파일을 매번 내려받는 건 돈(트래픽)과 시간(레이턴시)을 같이 태운다. HTTP 캐시는 이걸 “이미 받은 건 재사용하자”로 바꿔서 UX를 확 끌어올린다. 다만 “캐시한다”는 말 안에는 만료(freshness), 검증(validation), 공유(proxy), 무효화(invalidation)까지 꽤 많은 규칙이 숨어 있다.


1. 캐시가 없을 때 vs 있을 때

캐시가 없을 때

리소스가 변하지 않았더라도 매 요청마다 서버로부터 다시 내려받는다.

  • 네트워크 비용 증가 (대역폭/트래픽)
  • 초기 로딩, 재방문 로딩 모두 느림
  • 결과적으로 사용자 경험이 나빠짐

캐시가 있을 때

응답을 로컬(브라우저 캐시)에 저장하고, 유효 기간 동안은 네트워크를 타지 않는다.

  • 트래픽 절감
  • 로딩 속도 개선
  • 서버 부하 감소

대표적인 지시어는 이거 하나로 시작한다.

  • Cache-Control: max-age=60
    → “이 응답은 60초 동안 fresh(신선)하다. 그동안은 캐시에서 바로 써라.”

중요 포인트

  • max-age가 살아있는 동안은 “재요청” 자체가 안 나간다.
  • max-age가 끝나면 그때부터 “검증(조건부 요청)”이 개입한다.

2. 캐시 만료 이후의 현실: “진짜 바뀌었나?”를 확인해야 한다

max-age가 끝났다고 해서 매번 1MB짜리 파일을 다시 내려받는 건 낭비다.
캐시 만료 이후에도 서버 데이터가 그대로일 수 있기 때문이다.

이때 필요한 게 검증 헤더(validator) 와 조건부 요청(conditional request) 이다.

핵심 목표는 하나다:

“서버 데이터가 그대로라면, 바디(body) 말고 ‘바뀌지 않았다’는 사실만 알려줘.”

그래서 등장하는 응답이:

  • 304 Not Modified
    → 바디 없음. 대신 헤더(메타 정보)만 갱신하고, 클라이언트는 자기 캐시를 재사용한다.

3. 검증 헤더와 조건부 요청: Last-Modified vs ETag

캐시 검증은 크게 두 가지 축으로 돌아간다.

Last-Modified / If-Modified-Since (시간 기반)

서버가 “이 리소스 마지막 수정 시각”을 알려준다.

  • 응답: Last-Modified: Tue, 10 Nov 2020 10:00:00 GMT
  • 재요청(조건부): If-Modified-Since: Tue, 10 Nov 2020 10:00:00 GMT

동작은 직관적이다.

  • 서버가 그 이후로 수정 안 했으면 → 304 Not Modified
  • 수정했으면 → 200 OK + 새 바디

장점

  • 단순하고 구현이 쉽다.

단점(실무에서 걸리는 포인트)

  • 1초 미만 단위 제어가 어렵다.
  • “내용은 동일한데 저장만 다시 해서 수정 시간이 바뀐 경우” 같은 케이스에 취약하다.
  • 서버가 “의도적으로 캐시 정책을 통제”하기 어렵다.
    (Ex. 의미 없는 변경은 캐시 유지하고 싶은데 시간은 바뀜)

ETag / If-None-Match (버전 기반)

서버가 “이 표현의 버전(태그)”를 준다.

  • 응답: ETag: "a2jiodwjekjl3"
  • 재요청: If-None-Match: "a2jiodwjekjl3"

판단은 더 명확해진다.

  • ETag 동일 → 304 Not Modified
  • ETag 다름 → 200 OK + 새 바디

ETag의 본질

  • 클라이언트가 캐시 메커니즘을 “이해”할 필요가 없다.
  • 서버가 “이 버전은 유지/갱신”을 완전히 통제한다.

예를 들어 서버가 이렇게도 할 수 있다:

  • “베타 오픈 3일간은 파일이 조금 바뀌어도 ETag 유지”
  • “배포 주기에 맞춰 ETag 전부 갱신”

4. 캐시 제어 헤더 한 장 요약

Cache-Control (현대 표준)

  • max-age=초 : fresh 기간
  • no-cache : 저장해도 되지만, 사용 전 항상 원 서버에 검증해야 한다.
    (이름이 헷갈리는 대표 케이스)
  • no-store : 저장 금지 (민감 데이터)

하위 호환

  • Pragma: no-cache : HTTP/1.0 대응
  • Expires: 날짜 : 만료 시각(HTTP/1.0). 요즘은 max-age 선호
  • 둘 다 있으면 Cache-Control이 우선 (Expires는 무시)

Validator / Conditional 헤더 정리

  • Validator: Last-Modified, ETag
  • Conditional:
  • 시간 기반: If-Modified-Since, If-Unmodified-Since
  • 태그 기반: If-None-Match, If-Match

5. 프록시 캐시: “나만 빠른 게 아니라, 모두가 빠르게”

브라우저 캐시는 개인(private) 캐시다.
하지만 동일 리소스를 많은 사용자가 요청한다면, 중간에 공용(public) 캐시를 두는 게 더 효율적이다.

Ex.
브라우저(private) → 한국 프록시 캐시(public) → 해외 오리진(origin)

프록시 캐시에서 자주 쓰는 헤더:

  • Cache-Control: public : 공용 캐시에 저장 가능
  • Cache-Control: private : 개인 캐시에만 저장(기본 성격)
  • Cache-Control: s-maxage=초 : 프록시 캐시 전용 max-age
  • Age: 60 : 프록시에 저장된 지 얼마나 됐는지(초)

실무에서는 “브라우저는 빨리 갱신해야 하는데 CDN은 좀 더 오래 들고 있어도 됨” 같은 요구가 많아서 s-maxage가 꽤 유용하다.


6. 캐시 무효화: “확실하게” 막고 싶을 때

민감한 데이터(개인정보, 결제, 관리자 화면)나
“절대 오래된 값을 보여주면 안 되는” 응답은 캐시 전략이 달라진다.

자주 쓰는 조합:

  • Cache-Control: no-store
    → 아예 저장 금지 (가장 강함)
  • Cache-Control: no-cache
    → 저장은 가능, 매번 원 서버 검증 후 사용
  • Cache-Control: must-revalidate
    → 만료 후 첫 조회는 반드시 원 서버 검증
    → 원 서버 접근 실패 시 반드시 오류 (보통 504 Gateway Timeout)

no-cache vs must-revalidate 차이 (실무 포인트)

둘 다 “검증하고 써라” 느낌인데, 장애 상황에서 갈린다.

no-cache

  • 원 서버가 잠깐 죽었을 때
  • 프록시 캐시 설정에 따라 그냥 오래된 캐시를 200으로 내줄 수도 있음
  • (운영 철학: “에러보단 구버전이라도 보여주자”)

must-revalidate

  • 원 서버가 죽었으면
  • 오래된 캐시를 주면 안 됨 → 504로 실패해야 함
  • (운영 철학: “틀린 정보는 못 보여준다”)

7. 정리

캐시 설계는 “성능”이 아니라 “정합성 정책”이다

캐시는 단순히 빠르게 만드는 기능이 아니라,

  • 얼마나 오래 믿을 것인지(max-age)
  • 만료 후엔 어떻게 확인할 것인지(ETag/Last-Modified)
  • 누구와 공유할 것인지(public/private, s-maxage)
  • 장애 시 구버전을 허용할 것인지(no-cache vs must-revalidate)

이런 정책 결정에 더 가깝다.


출처
모든 개발자를 위한 HTTP 웹 기본 지식 (김영한, 인프런, 2020)

0개의 댓글