Skip to content
HTTP 기초제 8장

캐시는 같은 응답을 다시 쓰면서도 최신성을 어떻게 지킬까?

08. 캐시와 조건부 요청


학습 목표

  1. 캐시가 왜 필요한지, 네트워크 비용과 사용자 경험 관점에서 설명할 수 있다.
  2. Cache-Control: max-age가 브라우저 캐시에 어떤 영향을 주는지 이해할 수 있다.
  3. Last-Modified/If-Modified-SinceETag/If-None-Match의 차이를 설명할 수 있다.
  4. 304 Not Modified가 왜 바디 없이도 유용한지 설명할 수 있다.
  5. private, public, s-maxage, Age가 브라우저 캐시와 프록시 캐시에서 어떤 의미인지 구분할 수 있다.
  6. no-cache, no-store, must-revalidate를 언제 써야 하는지 판단할 수 있다.

전체 구조


1. 왜 캐시가 필요한가?

같은 이미지나 문서를 반복해서 요청하는데, 매번 원서버에서 다시 내려받는다면 낭비가 크다.

  • 네트워크는 메모리나 디스크보다 느리다
  • 응답 바디가 크면 다운로드 비용이 커진다
  • 사용자는 페이지가 다시 열릴 때마다 느리다고 느낀다

예를 들어 star.jpg가 1MB라고 해보자.

  • 첫 요청: 1MB 다운로드
  • 두 번째 요청: 데이터가 안 바뀌었는데도 다시 1MB 다운로드

이건 비효율적이다.
그래서 브라우저나 중간 캐시 서버가 응답을 저장해두고 재사용한다.
이것이 캐시다.

핵심 직관: 캐시는 “같은 데이터를 다시 받을 필요가 없을 때, 네트워크를 아끼고 더 빠르게 보여주는 장치”다.


2. 신선한 캐시: max-age 동안은 바로 재사용한다

가장 먼저 이해해야 할 것은 캐시의 유효 시간이다.

대표 예:

http
Cache-Control: max-age=60

이 뜻은 간단하다.

  • 이 응답은 60초 동안 신선하다
  • 그 시간 안에는 원서버에 다시 묻지 않고 재사용할 수 있다

브라우저 캐시 기본 흐름

왜 빨라질까?

  • 원서버까지 왕복할 필요가 없다
  • 바디를 다시 다운로드하지 않는다
  • 브라우저는 메모리/디스크에 저장된 데이터를 바로 쓸 수 있다

이 때문에 “한 번 본 정적 리소스가 다시 열릴 때 훨씬 빠른 경험”이 만들어진다.

만료되면 어떻게 될까?

max-age가 지나면 캐시는 더 이상 신선하지 않다.
이 상태를 보통 stale하다고 생각하면 된다.

이제 브라우저는 두 가지 중 하나를 해야 한다.

  • 전체를 다시 받기
  • 바뀌었는지만 먼저 확인하고, 안 바뀌었으면 기존 캐시를 재사용하기

두 번째가 바로 조건부 요청이다.


3. 만료된 캐시를 똑똑하게 확인하는 방법

문제는 이것이다.

  • 캐시가 60초 지났다
  • 그런데 서버 데이터는 실제로 안 바뀌었을 수도 있다

이 경우 매번 큰 파일을 다시 받으면 낭비다.
그래서 서버와 클라이언트는 “내 캐시가 아직 유효한지”를 확인하는 메커니즘을 쓴다.

이때 쓰는 것이:

  • 검증 헤더
  • 조건부 요청 헤더

핵심 조합 두 가지

검증 헤더조건부 요청 헤더의미
Last-ModifiedIf-Modified-Since마지막 수정 시각 기준 확인
ETagIf-None-Match서버가 정한 식별자 기준 확인

4. Last-ModifiedIf-Modified-Since

가장 직관적인 방식은 “마지막 수정 시각”을 비교하는 것이다.

서버는 응답에 이런 정보를 넣을 수 있다.

http
Cache-Control: max-age=60
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT

브라우저는 이 값을 캐시와 함께 저장한다.

그리고 캐시가 stale해지면 이렇게 묻는다.

http
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT

즉, 의미는 이것이다.

“내가 가진 버전은 이 시각 기준인데, 그 이후로 바뀌었어?”

데이터가 안 바뀌었으면

서버는 이렇게 응답할 수 있다.

http
HTTP/1.1 304 Not Modified
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=60

여기서 중요한 점:

  • 응답 바디가 없다
  • 헤더만 내려준다
  • 브라우저는 자기 캐시에 있던 바디를 다시 쓴다

즉, 전체 파일 대신 가벼운 헤더만 오가므로 네트워크 비용이 크게 줄어든다.

데이터가 바뀌었으면

서버는 평소처럼 200 OK와 새 바디를 보낸다.

단계로 보기

조건부 요청은 바뀐 경우와 안 바뀐 경우를 갈라낸다

1 / 3검증 요청

브라우저는 만료된 캐시를 들고 서버에게 마지막 수정 시각을 기준으로 다시 확인해 달라고 요청한다.

스스로 확인

304 응답은 바디가 없는데도 왜 성능에 도움이 될까?

Last-Modified 방식의 한계

이 방식은 단순하고 직관적이지만 한계도 있다.

  • 시간 기반이라 아주 미세한 변경을 정교하게 다루기 어렵다
  • 실제 내용은 같아도 수정 시각만 바뀌면 다시 받을 수 있다
  • 서버가 “이 내용은 날짜보다 다른 기준으로 판단하고 싶다”는 경우가 있다

이럴 때 더 유연한 방식이 ETag다.


5. ETagIf-None-Match

ETag는 서버가 리소스 버전을 식별하기 위해 붙이는 임의의 값이다.

예:

http
ETag: "v1-resource-hash"

이 값은:

  • 해시값일 수도 있고
  • 버전 번호일 수도 있고
  • 서버가 임의로 만든 식별자일 수도 있다

핵심은 클라이언트가 그 의미를 몰라도 된다는 점이다.

흐름

  1. 서버가 응답에 ETag를 내려준다
  2. 브라우저가 캐시에 저장한다
  3. stale해지면 If-None-Match로 다시 보낸다
  4. 서버는 값만 비교해서 같으면 304, 다르면 200을 보낸다

ETag가 유리할까?

  • 서버가 캐시 판단 기준을 완전히 통제할 수 있다
  • 날짜 비교보다 더 정교하게 “같은 내용인지” 판단할 수 있다
  • 클라이언트는 값의 의미를 몰라도 된다

예를 들어:

  • 파일 내용이 같으면 같은 ETag 유지
  • 배포 버전에 맞춰 모든 정적 파일 ETag 갱신

같은 식으로 운영할 수 있다.

실무 팁

Last-ModifiedETag는 함께 보일 때도 많다.
그리고 조건부 요청 헤더가 둘 다 있으면 If-None-MatchIf-Modified-Since보다 우선된다.
즉, 실무에서는 ETag 검증이 더 우선되는 기준이고 Last-Modified는 보조 기준으로 함께 쓰이는 경우가 많다.

핵심 직관: Last-Modified는 “언제 바뀌었는가”, ETag는 “같은 버전인가”를 보는 방식이다.


6. 304 Not Modified가 왜 중요한가?

304는 성공처럼 보이지도 않고, 에러처럼 보이지도 않는다.
하지만 캐시 관점에서는 매우 중요하다.

이 응답이 뜻하는 것은 단순하다.

  • 서버 데이터는 바뀌지 않았다
  • 새 바디는 보내지 않겠다
  • 너는 네가 가진 캐시를 계속 써라

즉, 304데이터 이동량을 크게 줄이는 신호다.

이 덕분에:

  • 응답 바디가 큰 이미지, JS, CSS, HTML 문서도
  • “안 바뀌었으면 헤더만 확인하고 재사용”할 수 있다

7. 브라우저 캐시와 프록시 캐시는 무엇이 다를까?

지금까지는 주로 브라우저 내부 캐시를 이야기했다.
하지만 캐시는 브라우저 안에만 있는 것이 아니다.

중간 서버도 캐시를 가질 수 있다.

  • 브라우저 캐시: 사용자 개인 캐시
  • 프록시 캐시 / CDN: 여러 사용자가 공유하는 공용 캐시

왜 공용 캐시가 필요한가?

예를 들어 원서버가 미국에 있고 사용자는 한국에 있다고 해보자.

  • 원서버까지 매번 가면 느리다
  • 그런데 한국 어딘가에 캐시 서버를 두면 훨씬 빨리 응답할 수 있다

첫 사용자는 원서버까지 가야 할 수도 있다.
하지만 그 뒤에는 캐시 서버가 저장한 응답을 재사용해 더 빠르게 내려줄 수 있다.

관련 지시어

헤더/지시어의미
Cache-Control: public공용 캐시에도 저장 가능
Cache-Control: private브라우저 같은 개인 캐시에만 저장, 공용 캐시 금지
Cache-Control: s-maxage=300shared cache에서만 300초 fresh
Age: 120shared cache가 이 응답을 받은 뒤 120초 지났음

publicprivate

  • public: 이미지, 공용 정적 파일처럼 여러 사용자에게 동일한 응답에 적합
  • private: 로그인 사용자 개인 정보처럼 사용자별 응답에 적합

예:

http
Cache-Control: private, max-age=3600

이 뜻은:

  • 브라우저 같은 개인 캐시에는 저장 가능
  • 공용 프록시 캐시에는 저장하지 말라

로그인한 사용자의 주문 내역, 프로필, 장바구니 같은 응답은 보통 이런 쪽이 맞다.

s-maxageAge

s-maxage는 shared cache 전용 max-age다.
즉, CDN/프록시 캐시에게만 따로 더 긴 혹은 더 짧은 신선도를 줄 수 있다.

Age는 shared cache에 이미 얼마나 머물렀는지 초 단위로 알려준다.
즉, max-ages-maxage로 정한 신선도에서 이미 사용한 시간을 보여주는 값으로 이해하면 쉽다.

예:

http
Cache-Control: public, max-age=60, s-maxage=300
Age: 120

이런 응답을 받으면 shared cache 기준으로는 300초 중 120초를 이미 사용한 상태다.
즉, shared cache에는 아직 약 180초 정도 fresh 시간이 남아 있다고 이해하면 된다.


8. 캐시를 막거나 엄격하게 검증해야 할 때

캐시가 항상 좋은 것은 아니다.

예를 들어:

  • 통장 잔고
  • 일회성 결제 결과
  • 민감한 개인정보

이런 응답은 잘못 캐시되면 큰 문제가 된다.

그래서 캐시 제어 지시어가 필요하다.

8-1. no-cache: 저장은 가능하지만, 재사용 전엔 반드시 검증

이 이름은 가장 많이 헷갈린다.

http
Cache-Control: no-cache

이 뜻은:

  • 저장하지 말라가 아니다
  • 저장은 해도 된다
  • 하지만 다시 쓰기 전에 원서버에 반드시 검증해야 한다

즉, “캐시 금지”보다 “검증 강제”에 가깝다.

8-2. no-store: 아예 저장 금지

http
Cache-Control: no-store

이건 이름 그대로다.

  • 브라우저 캐시에도
  • 공용 캐시에도

어떤 종류의 캐시도 응답을 저장하면 안 된다.

민감 정보 응답에 가장 먼저 떠올릴 지시어다.

다른 지시어와 섞여 있어도 no-store가 있으면 가장 보수적으로 no-store처럼 이해하면 된다.

8-3. must-revalidate: stale이면 반드시 원서버 검증

http
Cache-Control: max-age=60, must-revalidate

이 뜻은:

  • fresh할 때는 캐시 재사용 가능
  • stale해진 뒤에는 원서버 검증이 필수
  • 원서버 검증이 안 되면 낡은 응답을 그냥 보여주면 안 된다

즉, 네트워크가 끊겼다고 해서 오래된 데이터를 임의로 재사용하는 것을 더 엄격하게 막는다.

no-cachemust-revalidate를 같이 보는 이유

둘 다 “그냥 막 써도 되는 캐시는 아니다”라는 공통점이 있다.
다만 must-revalidate는 stale 이후 재사용을 더 엄격히 통제하는 쪽으로 이해하면 쉽다.

하위 호환: Expires, Pragma

이 둘은 예전 방식이다.

  • Expires: 절대 만료 시각 지정
  • Pragma: no-cache: 오래된 하위 호환 목적

지금은 보통 Cache-Control이 중심이고, 필요할 때만 호환성 차원에서 함께 고려한다.

실무에서 자주 떠올릴 조합

상황추천 출발점
공용 정적 파일public, max-age=...
개인화 응답private, max-age=...
검증 후 사용해야 함no-cache
민감 정보, 저장 자체 금지no-store
stale 응답 재사용까지 엄격 통제must-revalidate

핵심 암기 포인트

  • 캐시는 같은 응답을 반복 다운로드하지 않게 해 네트워크 비용과 로딩 시간을 줄인다.
  • Cache-Control: max-age=N은 N초 동안 캐시를 fresh 상태로 재사용하게 한다.
  • 캐시가 stale해지면 전체를 다시 받기 전에 조건부 요청으로 “정말 바뀌었는지” 확인할 수 있다.
  • Last-Modified는 시간 기반, ETag는 서버가 정한 버전 식별자 기반 검증이다.
  • 304 Not Modified는 바디 없이 헤더만 보내고, 브라우저가 기존 캐시 바디를 재사용하게 만든다.
  • 브라우저 캐시는 private cache이고, CDN/프록시 캐시는 shared cache다.
  • public은 공용 캐시 허용, private은 공용 캐시 금지다.
  • no-cache는 저장 금지가 아니라 “재사용 전 검증”이다.
  • no-store는 어떤 캐시에도 저장하지 말라는 뜻이다.
  • must-revalidate는 stale 상태에서 원서버 검증 없이 재사용하지 못하게 더 엄격하게 막는다.

확인 질문

  1. 캐시가 없을 때와 있을 때, 같은 이미지 요청의 비용 차이는 왜 크게 날까?
  2. max-age=60은 브라우저에게 정확히 어떤 행동을 기대하게 할까?
  3. Last-Modified/If-Modified-SinceETag/If-None-Match는 각각 무엇을 비교할까?
  4. 304 Not Modified는 바디가 없어도 충분히 유용할까?
  5. privatepublic은 브라우저 캐시와 프록시 캐시 관점에서 무엇이 다를까?
  6. no-cacheno-store는 왜 이름만 보면 헷갈리기 쉬울까?
  7. 사용자 통장 잔고 화면에는 왜 공격적으로 캐시를 허용하면 안 될까?