ETag(Entity Tag)는 서버의 리소스 버전에 고유 식별자를 할당하는 HTTP 헤더로, 클라이언트가 캐시된 데이터의 유효성을 효율적으로 확인할 수 있게 합니다. 반복 요청 시 브라우저나 애플리케이션이 저장된 ETag를 보내면 서버가 현재 값과 비교합니다. 일치하면 응답 본문 없이 304 Not Modified 상태를 반환합니다. RFC 7232(IETF, 2014)에 따르면, ETag를 사용한 조건부 요청은 자주 요청되는 리소스의 데이터 전송량을 최대 95%까지 줄입니다. 이는 모바일 애플리케이션 성능에 헤더가 매우 중요함을 의미합니다.
핵심 요점
ETag(Entity Tag)는 리소스의 특정 버전에 대한 고유 식별자를 포함하는 HTTP 응답 헤더입니다. 서버는 파일 내용, 메타데이터 또는 수정 번호를 기반으로 ETag를 계산하고 GET 요청에 대한 응답으로 클라이언트에 전송합니다. 클라이언트는 이 식별자를 저장하고 동일한 리소스에 대한 후속 요청에서 If-None-Match 헤더로 전송합니다. 리소스가 변경되지 않은 경우 서버는 304 Not Modified로 응답하고 클라이언트는 캐시된 복사본을 사용합니다.
ETag 형식은 RFC 7232에서 따옴표로 묶인 문자열로 정의됩니다: "33a64df551425fcc55e4d42a148795d9f25f89d4". 값은 파일 내용의 SHA-1 해시, 증분 버전 번호, 정적 파일의 inode-번호-시간 조합 또는 서버가 생성하는 임의 토큰이 될 수 있습니다. 유일한 요구 사항은 리소스가 변경될 때 값이 변경되어야 하고 리소스가 동일하게 유지되면 변경되지 않아야 한다는 것입니다.
ETag는 조건부 요청 메커니즘에 속합니다 — HTTP 프로토콜의 기본 최적화 중 하나입니다. 서버가 항상 전체 응답을 반환하는 무조건 요청과 달리, 조건부 요청은 클라이언트가 데이터를 다시 로드하지 않고 캐시 유효성을 확인할 수 있게 합니다. HTTP Archive(2025)에 따르면, 모든 HTTP 응답의 약 40%가 304 Not Modified이며, 이는 올바른 ETag 및 Last-Modified 구성 덕분입니다.
ETag는 REST API에서 데이터 컬렉션 로딩을 최적화하는 데 사용됩니다 — 객체 목록이 변경되지 않은 경우 클라이언트는 전체 JSON을 전송하지 않고 304를 받습니다. 정적 파일(CSS, JS, 이미지)의 경우 ETag를 통해 CDN과 브라우저가 캐시 신선도를 효율적으로 확인할 수 있습니다. 모바일 애플리케이션에서 ETag는 백그라운드 동기화에 중요합니다: 앱이 서버의 데이터 변경 여부를 확인하고 필요한 경우에만 업데이트를 다운로드합니다. 이는 트래픽과 기기 배터리를 절약합니다.
전체 ETag 수명 주기는 네 단계로 구성됩니다. 서버는 첫 번째 요청에서 ETag를 생성하고 응답 헤더로 반환합니다. 클라이언트는 ETag를 캐시된 리소스와 함께 저장합니다. 반복 요청 시 클라이언트는 저장된 ETag 값과 함께 If-None-Match 헤더를 보냅니다. 서버는 수신된 값을 현재 리소스 ETag와 비교합니다: 일치하면 빈 본문과 함께 304 Not Modified를 반환하고, 일치하지 않으면 새 리소스와 새 ETag와 함께 200 OK를 반환합니다.
// If-None-Match를 사용한 클라이언트 요청
GET /api/users HTTP/1.1
Host: example.com
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
// 서버 응답 — 리소스 변경되지 않음
HTTP/1.1 304 Not Modified
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
모바일 애플리케이션에서 이 주기는 캐싱을 지원하는 HTTP 클라이언트를 통해 구현할 수 있습니다. 예를 들어 OkHttp는 CacheInterceptor를 통해 ETag를 자동으로 관리합니다: 응답 ETag를 저장하고 반복 요청 시 If-None-Match를 추가합니다. 304를 수신하면 OkHttp는 캐시된 데이터를 반환합니다. OkHttp는 추가 구성 없이 ETag를 지원합니다 — OkHttpClient.Builder.cache()를 통해 캐시만 활성화하면 됩니다.
서버는 다양한 방식으로 ETag를 계산할 수 있습니다: 콘텐츠의 MD5 또는 SHA 해시, 데이터베이스의 수정 번호(MySQL의 updated_at 등), 정적 파일의 inode + mtime + 크기 조합(Nginx는 정확히 이 방식으로 ETag를 생성합니다). 동적 API의 경우 콘텐츠 해시가 가장 신뢰할 수 있습니다: JSON 응답에서 하나의 필드만 변경되어도 ETag가 변경됩니다. 그러나 매 요청마다 해시를 계산하면 CPU에 부하가 가해지므로, 고부하 시스템에서는 증분 버전 번호를 사용하는 것이 좋습니다.
RFC 7232는 두 가지 유형의 ETag를 정의합니다: 강함(strong)과 약함(weak). 강한 ETag는 리소스의 두 표현이 바이트 단위로 동일함을 의미합니다 — 단 하나의 비트도 다르지 않습니다. 약한 ETag(접두사 W/)는 의미적 동등성만 보장합니다: 콘텐츠는 직렬화 수준(공백, JSON 필드 순서)에서 다를 수 있지만 클라이언트에게 데이터는 동일한 것으로 간주됩니다. 약한 ETag는 W/ 접두사로 표시됩니다. 예: W/"1a2b3c".
ETag 유형의 선택은 비교 정확성 요구 사항에 따라 다릅니다. 정적 파일(CSS, JS, 이미지)의 경우 강한 ETag가 선호됩니다 — 파일이 변경된 경우 클라이언트가 새 버전을 받아야 합니다. 동일한 JSON이 다른 필드 순서나 형식으로 직렬화될 수 있는 동적 API의 경우 약한 ETag가 더 유연성을 제공합니다: 서버는 문자열 표현이 아닌 비즈니스 데이터를 기반으로 ETag를 생성합니다.
| ETag 유형 | 형식 | 보장 | 용도 |
|---|---|---|---|
| Strong(강함) | "해시" | 바이트 단위 동일성 | 정적 파일, 바이너리 리소스 |
| Weak(약함) | W/"해시" | 의미적 동등성 | JSON API, 동적 페이지 |
약한 ETag의 제한 사항: 범위 요청(Range requests)과 함께 사용할 수 없습니다. 클라이언트가 파일의 일부를 요청하는 경우, 서버는 해당 조각이 전체 리소스에 해당함을 보장하기 위해 강한 ETag를 반환해야 합니다. 약한 ETag는 이러한 보장을 제공하지 않습니다. 다른 시나리오에서는 약한 ETag가 안전하며 API에 권장됩니다.
ETag와 Last-Modified는 조건부 요청을 위한 두 가지 HTTP 헤더로, 함께 자주 사용됩니다. Last-Modified는 리소스의 마지막 수정 날짜를 나타내며 If-Modified-Since 헤더와 함께 작동합니다. ETag는 고유 버전 식별자를 제공하며 If-None-Match와 함께 작동합니다. 각각 장단점이 있으며, 결합하면 최대 캐싱 효율을 제공합니다.
Last-Modified는 구현이 더 간단합니다 — 서버가 파일 시스템에서 자동으로 날짜를 가져오거나 데이터베이스에서 updated_at 필드를 업데이트합니다. 그러나 날짜의 정밀도는 초 단위로, 초당 여러 번 변경되는 리소스에는 충분하지 않습니다. 또한 Last-Modified는 서로 다른 상태를 구분하지 못합니다: 파일이 동일한 버전으로 덮어쓰여지면 날짜는 변경되지만 콘텐츠는 변경되지 않으므로 클라이언트가 동일한 데이터를 다시 로드합니다.
ETag는 더 정확합니다: 콘텐츠가 실제로 변경된 경우에만 변경됩니다. 서버가 백업에서 이전 버전을 복원하면 ETag가 변경됩니다. 파일이 동일한 데이터로 덮어쓰여지면 ETag는 동일하게 유지되며 클라이언트가 다시 로드하지 않습니다. 결합 사용은 HTTP 명세에서 권장됩니다: 서버는 두 헤더를 모두 반환하고, 클라이언트는 If-None-Match와 If-Modified-Since를 동시에 보냅니다. 하나의 헤더라도 변경을 나타내면 서버가 새 리소스를 반환합니다.
명세에 따르면 ETag가 Last-Modified보다 우선순위가 높습니다. 서버가 If-None-Match를 수신하면 If-Modified-Since를 무시하고 ETag만 확인해야 합니다. 이는 경합 조건을 방지합니다: 클라이언트가 Last-Modified를 보낸 후 서버에서 확인하기 전에 리소스가 변경된 경우 ETag가 더 최신 지표가 됩니다. 실제로 서버는 일반적으로 두 헤더를 모두 확인하지만 결과가 일치하지 않을 때 ETag가 우선합니다.
ETag 구성은 서버 유형에 따라 다릅니다. Nginx는 inode, mtime 및 크기를 기반으로 정적 파일의 ETag를 자동으로 생성합니다. Apache는 FileETag 메커니즘을 사용합니다. Node.js, PHP, Python, Ruby의 동적 애플리케이션의 경우 ETag를 프로그래밍 방식으로 생성해야 합니다 — 응답 해시, 데이터 버전 번호 또는 요청 매개변수 조합을 통해.
func etagMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter,
r *http.Request) {
// 데이터 기반 ETag 생성
etag := generateETag(r.URL.Path)
w.Header().Set("ETag", etag)
// If-None-Match 확인
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
next.ServeHTTP(w, r)
})
}
Go의 미들웨어는 요청을 가로채고 요청된 URL에 대한 ETag를 생성하며(예: 캐시 또는 DB에서 데이터 해시 계산) 응답 헤더를 설정합니다. 클라이언트가 If-None-Match를 보냈고 현재 ETag와 일치하면 서버는 메인 핸들러를 호출하지 않고 즉시 304 Not Modified를 반환합니다. 프로덕션에서는 서버 부하를 줄이기 위해 URL 및 매개변수별로 계산된 ETag의 캐싱을 추가해야 합니다.
멀티 서버 구성(라운드 로빈 또는 애니캐스트)에서는 동일한 리소스에 대해 모든 노드에서 ETag가 동일해야 합니다. ETag가 파일 inode를 기반으로 생성되고 사이트가 여러 서버에 배포된 경우 값이 달라집니다. 해결책은 콘텐츠 해시 또는 중앙 집중식 버전 스토리지(Redis, etcd)를 사용하는 것입니다. 두 번째 문제는 gzip 압축입니다: 압축이 활성화되면 Nginx가 ETag를 변경하여 중복 304 응답이 발생할 수 있습니다. 압축된 콘텐츠와 ETag를 동기화하려면 gzip_vary on을 구성해야 합니다.
자주 묻는 질문
네, 서버가 명시적으로 방지하지 않은 경우 가능합니다. ETag는 전역적으로 고유할 필요가 없습니다 — 특정 URL 내에서 고유합니다. 정적 파일의 경우 SHA 해시를 사용할 때 충돌 가능성은 낮지만 사용자 정의 생성기에서 중복이 발생할 수 있습니다.
ETag는 반복적으로 요청되고 거의 변경되지 않는 리소스(정적 자산, API 목록, 구성)에 가장 효과적입니다. 한 번만 로드되는 고유 페이지(예: 주문 확인 페이지)의 경우 ETag가 이점을 제공하지 않습니다.
CDN은 캐시 신선도를 확인하기 위해 원본 요청에서 ETag를 고려합니다. 원본의 리소스 ETag가 변경되면 CDN이 새 버전을 로드합니다. Cloudflare와 Fastly는 원본 수준에서 표준 캐시 무효화 메커니즘으로 ETag를 지원합니다.
RFC 7232는 ETag 길이를 제한하지 않지만 서버와 프록시가 지나치게 긴 값을 자르거나 무시할 수 있습니다. 20~40자 해시 또는 버전 식별자와 체크섬의 조합을 사용하는 것이 좋습니다.
이들은 상호 배타적인 메커니즘이 아닙니다. Cache-Control은 캐싱 정책(저장 기간, 허용 대상)을 정의하고, ETag는 캐시된 리소스의 검증 메커니즘입니다. 최적의 구성에는 두 헤더가 모두 포함됩니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.