프로그래밍/Network

HTTP ETag에 대해 알아보자

Lou Park 2022. 10. 13. 22:57

ETag란?

https://www.holisticseo.digital/pagespeed/etag/

ETag란 EntityTag의 줄임말로, 웹 캐시 유효성 검증에 사용된다. 리소스의 특정 버전에대한 고유값이 ETag의 값이되고, 리소스의 내용이 업데이트되면 ETag도 바뀐다. 클라이언트에서 캐싱하고 있는 버전과 서버에서 가지고 있는 버전이 동일하다면 서버는 내용없이 304 Not Modified 라는 상태코드로만 응답을 내려주어 response body에 대한 트래픽을 아낄 수 있다.

 

ETag 작동방식

먼저 HTTP Request를 날려보자. 서버는 ETag와 함께 응답 (상태코드 200)을 내려줄 것이다. 이 요청에 대한 응답의 사이즈는 43.9KB였다.

 

1번째 Response Header

 

ETag의 검사기 종류에는 약한(Weak)과 강한(Strong)검사가 있는데, 약한 검사를 하는 ETag는 W/로 시작하고, 강한 검사는 그것이 붙지 않는다. 약한 검사는 리소스 내용이 유사한 경우 동일하다고 간주, 캐싱 성능을 최적화하는데 도움을 준다. 반면에 강한 검사는 바이트 대 바이트로 동일한지 엄격히 검사한다.

 

ETag와 함께 해당 버전이 언제 수정되었는지에 대한 Last-Modified도 같이 내려준다.

 

그러면 클라이언트는 이 ETag와 응답을 저장하고 있다가, 다음 요청시에는 RequestHeader에 If-None-Match: W/"6345748b-1f101"를 실어준다. 이렇게 조건들을 걸어주는 것을 조건부 요청이라고 한다. W/가 접두사로 붙어있으므로, 서버는 약한 검사를 하여 응답을 내려줄것이다. 만약에 실어준 ETag와 일치한다면 304 Not Modified 응답이 올것이고, 아니라면 다시 200 OK 응답과 새로운 ETag가 내려올 것이다.

 

2번째 Request Header

 

위 사진은 Chrome 브라우저로 요청을 보냈을때 Request Header 내용인데, If-Modified-Since값도 같이 보내고 있다. If-Modified-Since는 지정된 날짜 이후 혹시 리소스가 수정되었다면 200 OK와 함께 리소스를 내려달라는 내용이다. 크롬에서는 아까 응답으로 받은 Last-Modified를 사용했다.

 

2번째 Response Header

Response는 이렇게 왔다. 응답의 Size는 159B로 대폭 줄었다.

 

웹 서버에서 ETag를 활성화 하는 방법

직접 구현해도되지만, Nginx에서 활성화하면 더 간단하다. 하지만 기본값이 etag on;이므로 명시적으로 off를 하지 않은 이상 따로 작성해주지 않아도 된다. nginx에서 생성한 ETag는 위 예시로 든 것처럼 14자리의 문자열이다. nginx가 정확하게는 어떤 로직으로 ETag를 생성하는지는 모르지만, https://github.com/soldair/nginx-etag Repository에서 살펴보면 마지막 수정 시간과 content 길이를 md5 해싱하여 얻어낸 값으로 추정된다.

server {
    etag on;
}

 

OkHttp3의 Etag 지원 (Android)

결론부터말하면 OkHttp에서 지원하지 않는다. (*23.01.08 수정됨)

다음 사진은 Android Network Inspector로 찍었을때 결과인데, 처음에만 Response가 오고, 이후에는 200 Response는 오지만 여기에는 304 조차 찍히지 않아서 꽤 해맸다.

 

Network Inspector

알고보니 GET 요청이기때문에 cache를 설정했을 때 내부에서 캐시를 가져오는 것이었고,

이렇게 캐시된 것이 있을경우 캐시된 리스폰스로 바꿔치기 후 상태코드를 200으로 내려주는 것이었다. 그래서 Network Inspector엔 찍히지 않았다. 그래서 직접 캐시 파일을 찾기로 했다.

val cache = Cache(
    directory = File(application.cacheDir, "http_cache"),
    maxSize = 10L * 1024 * 1024 // 10MB
)
val client = OkHttpClient.Builder()
.cache(cache)
.build()

OkHttp 캐시를 켜기 위해서는 이렇게 설정하면되는데, data/data/본인 패키지명 폴더/http_cache에 들어가면 캐시된 것들을 볼 수 있다. 파일명은 URL을 md5 해시한 값이므로, 너무 파일이 많다면 md5돌려서 찾자.

 

캐시 파일 모습

파일은 ***.0***.1 두개로 나뉘어져있는데, ***.1쪽이 리스폰스 바디에 해당하는 데이터가 담겨있다. 요청 정보는 ***.0 쪽이다. 이 파일들을 삭제한 뒤에 다시 요청하면 현재 캐시된 내용이없어서 처음부터 다시 가져오는 모습을 볼 수 있다.

 

Custom ETag Interceptor 추가하기

ETag를 사용한거라기 보다는 그냥 캐시 컨트롤을 한거다. 안드로이드에서 ETag를 사용하기 위해 Custom Interceptor를 만들었는데, 아래 코드처럼 Interceptor 하나와 NetworkInterceptor하나를 추가해주면된다.

val diskCache = DiskCache(application.cacheDir)
// ...
.client(OkHttpClient
    .Builder()
    .addInterceptor(ETagInterceptor(diskCache))
    .addNetworkInterceptor(ETagNetworkInterceptor(diskCache))
    .connectTimeout(5, TimeUnit.SECONDS)
    .readTimeout(5, TimeUnit.SECONDS)
    .build()
)
// ...

* NetworkInterceptor란?

앱 통신에서 App - OkHttp - Network의 레이어가 있다라고하면 OkHttp - Network 간의 Interceptor가 바로 NetworkInterceptor이다. 앱의 로직과는 관계없이 통신시 필요한 로직이있을때 이를 통해 구현한다.