이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- ETag와 Last-Modified가 body 전송량과 origin 부하를 어떻게 줄이는가?
- 304, 412,
If-None-Match,If-Match를 백엔드 API에서 어떻게 해석하는가?- weak/strong validator와 사용자별 응답 ETag를 설계할 때 무엇을 조심해야 하는가?
개요
조건부 요청은 client가 “내가 가진 버전이 아직 최신인가?”를 물어보고, 서버가 바뀌지 않았다면 body 없이 304 Not Modified로 답하는 방식이다. 큰 응답, 정적 파일, 자주 조회되는 공개 데이터에서는 네트워크 비용과 serialization 비용을 줄일 수 있다.
ETag는 리소스 표현의 버전 식별자다.Last-Modified는 리소스의 마지막 변경 시각이다.If-None-Match는 GET 재검증에 주로 쓰이고, 일치하면304가 된다.If-Match는 업데이트 충돌 방지에 쓰이며, 불일치하면412 Precondition Failed가 될 수 있다.
원리
조건부 GET 흐름은 다음과 같다.
GET /products/1
<- 200 OK
<- ETag: "product-1-v42"
GET /products/1
If-None-Match: "product-1-v42"
<- 304 Not Modified304 응답에는 body가 없다. client는 자신이 가진 cached body를 재사용한다. 이때 서버는 cache 관련 header를 여전히 올바르게 내려야 한다.
Strong과 Weak ETag
| 종류 | 예시 | 의미 |
|---|---|---|
| strong ETag | "abc123" | byte 단위 표현이 같아야 동일 |
| weak ETag | W/"abc123" | 의미상 같으면 동일하다고 볼 수 있음 |
정적 파일이나 JSON byte가 정확히 중요하면 strong ETag가 이해하기 쉽다. gzip 여부, pretty print, field order처럼 표현이 달라질 수 있는 API는 weak ETag가 더 현실적일 수 있다. 단, validator 선택은 CDN, compression, serialization 방식과 연결된다.
Last-Modified의 한계
Last-Modified는 간단하지만 초 단위 해상도와 clock skew 문제가 있다. 짧은 시간에 여러 번 바뀌는 리소스에는 ETag가 더 정확하다. DB row의 updated_at만으로 충분한지, version column이나 content hash가 필요한지 판단해야 한다.
If-Match와 동시 수정 방지
ETag는 조회 최적화뿐 아니라 업데이트 충돌 방지에도 쓸 수 있다.
PUT /documents/1
If-Match: "doc-1-v7"
<- 412 Precondition Failedclient가 오래된 버전을 바탕으로 수정하려 하면 서버가 거절한다. 협업 문서, 재고, 설정 변경처럼 lost update가 중요한 도메인에서는 유용하다.
코드와 명령으로 확인하기
ETag를 받고 재검증한다.
etag=$(curl -sI https://api.example.com/products/1 | awk -F': ' 'tolower($1)=="etag"{print $2}' | tr -d '\r')
curl -i https://api.example.com/products/1 \
-H "If-None-Match: ${etag}"Spring에서 조건부 응답을 구성한다.
String etag = "\"product-" + product.id() + "-v" + product.version() + "\"";
return ResponseEntity.ok()
.eTag(etag)
.lastModified(product.updatedAt().toEpochMilli())
.cacheControl(CacheControl.maxAge(Duration.ofSeconds(30)).cachePublic())
.body(product);대형 응답에서는 body를 만들기 전에 조건부 요청을 판단할 수 있는 구조가 더 좋다. 무거운 JSON을 다 만든 뒤 ETag를 계산하면 네트워크 비용은 줄어도 CPU 비용은 남는다.
사용자별 ETag 주의
같은 URL이라도 사용자 권한에 따라 응답이 다르면 ETag도 사용자별 표현을 반영해야 한다. 아니면 cache key를 사용자별로 나누거나 공유 cache 저장을 막아야 한다.
- 공개 상품 상세: version 기반 ETag 가능
- 사용자별 추천 목록:
private또는 사용자별 cache key 필요 - 권한별 필드가 다른 문서: 권한 버전까지 validator에 반영
ETag가 너무 상세하면 HIT율이 낮아지고, 너무 거칠면 stale data나 권한 문제가 생긴다.
인프라 협업 포인트
- CDN이 origin의 ETag를 유지하는지, compression 과정에서 바꾸거나 제거하는지 확인한다.
- edge에서 gzip/br 변환을 하면 strong ETag가 표현별로 달라질 수 있다.
- 304 응답이 CDN에서 생성되는지 origin에서 생성되는지 로그로 구분한다.
- purge와 ETag 변경 중 어떤 방식으로 최신화를 보장할지 배포 절차에 넣는다.
실전 팁
- 304 비율이 높아도 origin CPU가 높다면 body 생성 후 ETag 계산을 하고 있을 수 있다.
- ETag를 DB version으로 만들면 싸고 안정적이지만, 응답에 포함되는 derived field나 권한 필드까지 버전이 반영되는지 봐야 한다.
Last-Modified는 clock skew와 배포 서버별 시간 차이를 조심한다.- optimistic locking을 HTTP API로 노출할 때
If-Match와412를 사용하면 REST 의미와 잘 맞는다. - 개인 프로젝트에서는 정적 파일은 빌드 fingerprint로, API는
updated_at/version 기반 ETag로 작게 시작할 수 있다.
위험 신호!
- 사용자별 응답에 같은 ETag와
publiccache를 쓴다. - 304 응답에서
Cache-Control이나ETag가 빠진다. - gzip 적용 전후 strong ETag 의미를 검증하지 않는다.
- ETag 계산이 응답 생성보다 더 비싸다.
확인 질문
확인 질문
- 304는 왜 오류가 아닌가?
- client가 가진 cached representation이 아직 유효하므로 body 없이 재사용하라는 정상 응답이기 때문이다.
If-Match는 어떤 문제를 줄이는가?
- 오래된 버전을 기반으로 업데이트해 최신 변경을 덮어쓰는 lost update를 줄인다.
- 사용자별 응답 ETag에서 조심할 점은 무엇인가?
- 권한과 개인화 차이가 validator 또는 cache key에 반영되지 않으면 다른 사용자 응답이 재사용될 수 있다.