Tomcat 은 W/ 없는 ETag 응답을 압축하지 않는다. 동적 응답의 ETag 는 weak 으로 만들자.
현재 우리 코드에는 큰 응답을 반환하는 API가 있다.
메인 로직용 전시 데이터인데, 전시에 모두 필요한 데이터이므로 당장 쪼개기 애매해 하나의 API 에서 응답한다.
이를 매번 서버가 응답해주는 건 비효율적이라 캐싱 및 압축을 하기로 했다.
하지만, 스프링에선 자칫 잘 모르고 설정하거나 구현하면 의도와 다르게 도는 경우가 존재한다.
하나씩 알아 가보자.
특정 버전의 리소스를 식별하는 식별자. 특정 URL 의 리소스가 변경된다면, 새로운 ETag 가 생성된다.
ETag 를 통해 웹 서버는 리소스가 바뀌지 않았으면 본문을 다시 보내지 않을 수 있게된다.
ETag 는 2가지가 존재한다.
ETag: "abc123" ← strong
ETag: W/"abc123" ← weak앞에 W/ 가 있으면 weak ETag, 없으면 strong ETag 다.
이 두 가지는 아래와 같다.
이 태그가 같으면 바이트 단위로 동일하다. 사소한 바이트 하나까지 전부 같아야만 한다.이 태그가 같으면 의미상 동등(semantically equivalent)하다. 바이트는 달라도 된다.ETag는 대표적으로 조건부 GET과 Range 요청에 사용된다.
If-None-Match : ETag 를 담아서 요청한다. 가진 버전과 다르면, 본문 요구
If-Range : ETag 와 Range 요청을 함께 보낸다. 가진 버전 같으면, 이어받을 본문 요구
206 : Partial Content,
Range헤더에 기술된 데이터 범위에 대해 성공적으로 응답을 의미
조건부 헤더마다 ETag 비교 방식이 다르다.
| 헤더 | 비교 방식 | Weak ETag |
|---|---|---|
If-None-Match | Weak comparison | 304 재검증에 사용 가능 |
If-Range | Strong comparison | Range 요청에는 사용할 수 없음 |
서버는 응답이 큰 경우 네트워크 아웃바운드를 줄이기 위해 가능하다면, 압축을 해서 보내려고 한다.
server:
compression:
enabled: true
mime-types: text/html,application/javascript,application/json
min-response-size: 500Spring Boot 는 활성화, 미디어 타입, 최소 크기 등을 설정하게 지원해준다.
gzip은 HTTP에서 데이터를 압축해 전송하는 대표적인 방식이다. 기본 흐름은 다음과 같다.
Accept-Encoding: gzip 을 보낸다.Content-Encoding: gzip 으로 압축했다는 것을 알려준다.curl -sS -D - -o /dev/null -w '%{size_download} bytes' URL
curl -sS -D - -o /dev/null -w '%{size_download} bytes' -H 'Accept-Encoding: gzip' URL-D - : 응답 헤더를 표준 출력에 표시-w : 전송된 본문 크기를 표시Content-Encoding, Vary, ETag가 기대한 값인지 함께 확인그런데 위 yml 로 Spring 이 직접 압축 해주지 않는다.
Spring Boot 는 부팅될 때 내장 Tomcat 을 구동한다.
톰캣은 gzip 압축만 지원 (Brotli, zstd 등 사용하려면 앞단 서버 필요)
yml 의 값들을 Connector 속성으로 심어준다.
protocol.setCompression("on"); // enabled: true
protocol.setCompressionMinSize(...); // min-response-size
protocol.setCompressibleMimeType(...); // mime-types-> 압축 판정과 실행은 내장 Tomcat의 Connector가 담당한다.
-> 응답 헤더에 ETag가 있고 W/로 시작하지 않으면, Tomcat의 일반 압축 경로는 압축을 건너뛴다!!!
Tomcat의 일반 Connector 압축 경로는 Strong ETag가 있는 응답을 압축하지 않는다. 왜 그런지, 논리적으로 이해해 보자.
리소스 A가 있고, 이 데이터는 당분간 변경되지 않는다고 가정해 보자.
리소스 A의 응답값: A
리소스 A의 gzip 을 해제한 응답값: Agzip 으로 압축했건, 하지 않았건 사용하는 응답값은 A 로 동일하다.
리소스 A의 gzip 압축한 응답값: G-A하지만, 응답받은 바이트 자체를 보면 gzip 압축 응답값은 G-A 이다.
애플리케이션이 압축 전 본문을 기준으로 ETag를 만들면 두 응답의 ETag는 E-A로 같아진다.
리소스 A 응답 헤더의 ETag: E-A
리소스 A 를 gzip 압축한 응답 헤더의 ETag: E-A
압축을 해제한 뒤의 의미는 같지만, HTTP가 전달하는 representation data의 바이트열은 다르다.
Strong ETag는 의미가 같은지만 주장하는 값이 아니라, 바이트 단위로 동일하다는 것을 나타내는 validator다.
=> 서로 다른 바이트에 같은 Strong ETag를 붙이면 Strong ETag의 의미를 지킬 수 없다.
다시 한번, Tomcat 입장에서 정리하면 다음과 같다.
noCompressionStrongETag : strong etag 를 압축 대상으로 삼을지 결정, 기본값은 true(압축하지 않는다.)

Spring 에서는 ShallowEtagHeaderFilter 라는걸 지원해주지만
특정 API 에만 명시적으로 적용하고 싶으면, 직접 구현해야 할 수도 있다.
ShallowEtagHeaderFilter는 기본 설정이 Strong이므로, 압축이 되는지 확인해야 한다. 또한 응답을 버퍼링한 뒤 ETag를 계산하므로 큰 응답에 무조건 적용하면 메모리 사용량과 처리 비용을 고려해야 할 수 있다.
String WEAK_ETAG_PREFIX = "W/\"0";
char WEAK_ETAG_SUFFIX = '"';
byte[] bytes = objectMapper.writerWithView(jsonView).writeValueAsBytes(body);
StringBuilder sb = new StringBuilder(39);
sb.append(WEAK_ETAG_PREFIX);
DigestUtils.appendMd5DigestAsHex(bytes, sb);
sb.append(WEAK_ETAG_SUFFIX);
return sb.toString();응답 DTO를 실제 응답과 같은 @JsonView와 직렬화 설정으로 직렬화해 MD5를 만들고, 처음부터 W/를 붙인다.
응답 DTO 가 인스턴스마다 달라지는 요소는 없는지도 확인이 필요하다.
Set<Permission> allowed = Set.of(Permission.READ, Permission.WRITE, Permission.DELETE);
위 값은, 인스턴스마다 해싱 값이 달라질 수 있다! - 관련 노트
컨트롤러에서는 붙이기만 하면 된다.
Spring MVC는 200 OK인 ResponseEntity를 처리할 때 GET 또는 HEAD 요청의 ETag를 조건부 요청과 비교한다. 일치하면 본문 없이 304 Not Modified를 반환한다.
var headers = new HttpHeaders();
headers.set(HttpHeaders.ETAG, etag);
headers.set(HttpHeaders.VARY, "Accept-Encoding");
return ResponseEntity.ok()
.headers(headers) // W/"0..."
.cacheControl(CacheControl.noCache()) // 캐시하되, 쓰기 전에 매번 재검증
.body(responseDto);압축 설정을 했는데 압축이 안 된다면 응답의 ETag 헤더를 확인해 보자.
W/ 없이 시작하는 Strong ETag가 있으면 Tomcat의 일반 Connector 압축 경로는 그 응답을 압축하지 않는다.
같은 애플리케이션 ETag를 압축 전후에 유지하면서 Tomcat 압축을 사용하려면 Weak ETag가 실무적인 선택이다. 다만 표현별 Strong ETag를 만들 수 있다면 그 방법도 유효하다.
추가로 API의 응답 크기, Content-Encoding, Vary, ETag, ALB의 outbound 비용 변화를 함께 측정해 보자.
요청량이 늘어나면 이런 사소한 차이가 큰 비용 절감을 만들 수 있다.