ETag·Last-Modified·304

장석원·2026년 9월 28일

정적 데이터를 캐시로 옮기는 작업을 진행중이다.
조건부 요청은 "캐시가 만료됐을 때 전체를 다시 받을지, 변경 여부만 확인할지". 결정하는 다음 단계

조건부 요청이 실제로 아끼는 건 무엇이고, 못 아끼는 건 무엇인가.

조건부로 요청하면 매번 요청하는 건 동일하지만 본문자체를 들고오지 않음

아끼는 것은. 응답 바디 전송량
그 결과로 대역폭, 브라우저의 다운로드 시간, 파싱 전 대기 시간이 줄어든다.

아끼지 못하는 것은. 요청 한번의 왕복 과 서버가 변했는지 판단하는 비용.
그러므로 조건부 요청은 "캐시가 만료된 뒤 재검증을 싸게 만드는 장치"이지, "요청을 없애는 장치"가 아니다.
요청 자체를 없애는 것은 max-age처럼 캐시가 신선한 동안의 역할.

1. ETag는 무엇으로 만들어지나

RFC 9110은 ETag를 "표현을 구분하는 불투명한 값" 이라고 정의한다.
어떻게 만들지는 서버 구현이 정하므로, 바디 해시일수도 아닐수도 있다.

일단 이것을 기록하고 작성하는데 ETag란 서버가 응답에 붙이는이 데이터의 버전 표시 이다. Entity Tag 준말

비유로 하면 문서 파일 끝의 v3, v4 등등 내용 바뀌면 꼬리표도 바뀌고, 같으면 꼬리표도 같다.

HTTP/1.1 200 OK
ETag: "a1b2c3"
Content-Type: application/json

{ "product": [...] }
  1. 브라우저가 /api/catalog를 처음 요청하면, 서버는 데이터와 함께 ETag: "a1b2c3"을 준다. 브라우저는 데이터와 꼬리표를 같이 저장
  2. 나중에 같은 주소를 다시 요청할 때, 브라우저는 "나 a1b2c3 버전을 갖고 있어" 라고 알려줌 이게 If-None-Match: "a1b2c3" 헤더이다.
  3. 서버는 지금 데이터의 꼬리표와 비교한다.
    • 같으면 304 Not Modified를 보내고, "바뀐 거 없으니 갖고 있는 거 써" 라는 뜻이고, 데이터 본문은 보내지 않는다
    • 다르면 200과 새 데이터, 새 꼬리표를 보낸다.

RFC 9110

RFC는 인터넷 기술의 공식 규격 문서. IETF라는 표준화 단체가 번호를 붙여 발행.
브라우저와 서버, CDN을 만드는 사람들이 이 문서를 기준으로 구현한다.

RFC 9110은 HTTP의 의미를 정의한 문서이다.

이어서 ETag는 무엇으로 만들어지는지

  • Express: 응답 바디의 길이와 SHA-1 해시로 만든 약한 ETag를 사용한다. 형식은 W/"길이-해시"임
  • nginx 정적 파일: 바디를 읽지 않는다. 파일의 수정 시작과 크기를 16진수로 붙여서 "mtime-size" 형태로 만든다.
  • S3: 일반 단일 업로드는 객체의 MD5 값을 사용한다. 멀티파트 업로드는 각 파트 MD5의 MD5 뒤에 -파트수를 붙이므로 파일 내용의 MD5와 다르다.
  • 직접 구현: DB의 updated_at이나 버전 컬럼으로 만들 수도 있다. 이방식이 가장 저렴하다.

강한 ETag와 약한 (W/) ETag의 차이
강한 ETag는 바이트 단위로 같다는 약속. 약한건 의미상 같다는 약속.

2. 서버는 무엇을 계산해서 304를 결정하나

서버는 "지금 이 리소스의 ETag"를 구한 뒤, 요청의 If-None-Match값과 비교한다. 같으면 304, 다르면 200과 새 바디를 보낸다.

비교 자체는 문자열 비교라서 공짜에 가깝다. 비용은 현재 ETag를 구하는 과정에 있고, ETag를 어떻게 만드느냐에 따라 달라진다.

  • Express처럼 바디 해시 방식: 핸들러를 끝까지 실행해 JSON을 완성하고, 그 바디를 해싱한 다음에야 비교할 수 있따. 304를 보내더라도 DB 조회와 직렬화, 해싱은 모두 이미 끝난 상태이다.
  • updated_at 같은 메타데이터 방식: 가벼운 쿼리 하나로 버전만 확인하고, 일치하면 본 조회를 건너뛸 수 있다. 서버 비용까지 아끼려면 아래처럼

Last-Modified / If-Modified-Since , ETag/ If-None-Match
수정 시각 식별자(해시,버전 등)
HTTP-date라서 초 단위 값에 따라 다름
같은 1초 안에 두번 바뀌면 두 번째 만드는 비용이 들 수 있다.
변경 놓침. 내용은 같은데 재배포로 서버가 여러대 일때 서버 마다
mtime만 바뀌면 불필요하게 200보냄 값이 다르면 무력화 됨

초 단위 정밀도 문제는 Last-Modified 쪽에서 문제 생긴다.
두 헤더가 함께 오면 RFC 9110에 따라 서버는 If-None-Match를 우선 평가하고If-Modified-Since는 무시한다.

4. 304는 서버 처리 비용을 아끼나

기본적으로 아끼지는 못한다.
라우팅, 미들웨어, 인증, 헤더 계산은 그대로 일어나고, 바디 해시 방식이면 DB 조회와 직렬화 까지 모두 일어난다.
아끼는 것은 단지 네트워크로 나가는 바이트 뿐

5. CDN 5분 캐시 위에서 ETag/304가 추가로 얻어주는 것

CDN 캐시가 신선한 5분 동안

  • 오리진은 요청을 아예 받지 않는다.
  • 브라우저가 자기 캐시를 들고 If-None-Match로 재검증하면, CDN이 캐시해 둔 ETag와 비교해서 엣지에서 직접 304를 줄 수 있따. 이때 사용자와 엣지 사이의 바디 전송을 아낀다.

CDN 캐시가 만료된 순간

  • CDN이 오리진에 If-None-Match를 붙여 재검증한다.
  • 데이터가 바뀌지 않았다면 오리진은 304를 보내고, CDN은 바디를 다시 받지 않은 채 TTL만 5분 연장한다. 오리진에서 CDN으로 가는 전송량이 줄어든다.
  • 다마ㅓㄴ 4번에서 본 것 처럼 오리진이 요청을 처리하는 비용은 그대로 든다.

ETag는 만료 시점의 재검증을 싸게 만든다. 만료전의 요청수를 줄이는건 이미 CDN TTL이 하고있음.

6. stale-while-revalidate와 함께 쓸 이유가 있는지.

두 기능은 대체제가 아닌 보완재이고, 푸는 문제가 다르다.

  • stale-while-revalidate는 "언제, 누가 재검증을 기다리나"를 해결한다. 만료된 캐시를 일단 내어주고 재검증은 백그라운드에서 해서, 사용자가 기다리지 않게 하는것!
  • 조건부 요청은 "재검증 한 번이 얼마나 싸냐" 를 해결함

같이하면 이득임다 . 사용자 지연은 SWR이 숨기고, 전송량은 ETag가 줄인다.

0개의 댓글