웹의 여러 요소들은 그냥 주고 받기에는 너무 크다. 따라서 여러 압축 알고리즘을 통해 압축을 한 후 주고 받는다. 그런데, 어떤 것들은 이미 압축이 되어 다시 압축할 필요가 없는 경우도 있고 클라이언트나 서버에서 어떤 알고리즘은 지원하고 어떤 것은 그렇지 않을 수도 있다. 서버와 클라이언트는 어떻게 협상하는가? 클라이언트가 서버에게 Accept-Encoding
헤더를 보냄으로써 알 수 있다.
Accept-Encoding: gzip
Accept-Encoding: gzip, compress, br
Accept-Encoding: br;q=1.0, gzip;q=0.8, *;q=0.1
위와 같이 gzip
, br
, 그리고 가중치를 함께 적음으로서 서버에게 위 알고리즘을 사용한다고 알려줄 수 있다. gzip, compress, br
을 함께 적으면 저 셋 모두 클라이언트에서 처리할 수 있으니 아무렇게나 달라는 뜻이다.
네이버의 PC 홈 화면에서 날라가는 request를 캡쳐한 것이다. Accept-Encoding
헤더가 gzip, deflate, br
로 되어있다.
gzip
의 경우 그 유명한 gzip 알고리즘(https://www.gzip.org/)을 의미한다.
br
의 경우 구글에서 만든 Brotli
압축 알고리즘을 의미하는데 크롬의 경우 버전 50 이후(2016년 배포), 파이어폭스는 44 이후(16년), 사파리는 11 이후 (17년 배포) 버전을 이용한다면 이용할 수 있다. css
, js
, html
파일에 대해 gzip
보다 대략 15~20% 크기 측면에서 효율적인 것으로 알려져 있다.
Accept-Encoding: deflate, gzip;q=1.0, *;q=0.5
위와 같이 가중치를 주어서 계산할 수도 있다.
자바로 짠 통신 프레임워크인 Netty
의 구현을 참고하자.
protected String determineEncoding(String acceptEncoding) {
float starQ = -1.0f;
float brQ = -1.0f;
float gzipQ = -1.0f;
float deflateQ = -1.0f;
for (String encoding : acceptEncoding.split(",")) {
float q = 1.0f;
int equalsPos = encoding.indexOf('=');
if (equalsPos != -1) {
try {
q = Float.parseFloat(encoding.substring(equalsPos + 1));
} catch (NumberFormatException e) {
// Ignore encoding
q = 0.0f;
}
}
if (encoding.contains("*")) {
starQ = q;
} else if (encoding.contains("br") && q > brQ) {
brQ = q;
} else if (encoding.contains("gzip") && q > gzipQ) {
gzipQ = q;
} else if (encoding.contains("deflate") && q > deflateQ) {
deflateQ = q;
}
}
if (brQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
if (brQ != -1.0f && brQ >= gzipQ) {
return Brotli.isAvailable() ? "br" : null;
} else if (gzipQ != -1.0f && gzipQ >= deflateQ) {
return "gzip";
} else if (deflateQ != -1.0f) {
return "deflate";
}
}
if (starQ > 0.0f) {
if (brQ == -1.0f) {
return Brotli.isAvailable() ? "br" : null;
}
if (gzipQ == -1.0f) {
return "gzip";
}
if (deflateQ == -1.0f) {
return "deflate";
}
}
return null;
}
가중치를 기반으로 br
, gzip
, deflate
중 가능한 것을 적용하는 것을 확인할 수 있다. Brotli
의 경우 지원하지 않는 시스템이 많아 우선 Brotli.isAvailable
을 호출해 사용 가능한지를 확인할 수 있다. 실제로 Netty
에서 플랫폼에 대한 테스트 이슈를 해결하기 위한 PR이 여러 개 올라와 있다.
Armeria
의 경우 위 Netty
의 코드를 기반으로 일부 수정해서 사용하고 있는데, 테스트 코드는 Armeria
가 더 직관적이기 때문에 Armeria
의 것을 확인하자.
public class HttpEncodersTest {
@Rule public MockitoRule mocks = MockitoJUnit.rule();
@Mock private HttpRequest request;
@Test
public void noAcceptEncoding() {
when(request.headers()).thenReturn(RequestHeaders.of(HttpMethod.GET, "/"));
assertThat(HttpEncoders.getWrapperForRequest(request)).isNull();
}
@Test
public void acceptEncodingGzip() {
when(request.headers()).thenReturn(RequestHeaders.of(HttpMethod.GET, "/",
HttpHeaderNames.ACCEPT_ENCODING, "gzip"));
assertThat(HttpEncoders.getWrapperForRequest(request)).isEqualTo(HttpEncodingType.GZIP);
}
@Test
public void acceptEncodingDeflate() {
when(request.headers()).thenReturn(RequestHeaders.of(HttpMethod.GET, "/",
HttpHeaderNames.ACCEPT_ENCODING, "deflate"));
assertThat(HttpEncoders.getWrapperForRequest(request)).isEqualTo(HttpEncodingType.DEFLATE);
@Test
public void acceptEncodingBoth() {
when(request.headers()).thenReturn(RequestHeaders.of(HttpMethod.GET, "/",
HttpHeaderNames.ACCEPT_ENCODING, "gzip, deflate"));
assertThat(HttpEncoders.getWrapperForRequest(request)).isEqualTo(HttpEncodingType.GZIP);
}
@Test
public void acceptEncodingUnknown() {
when(request.headers()).thenReturn(RequestHeaders.of(HttpMethod.GET, "/",
HttpHeaderNames.ACCEPT_ENCODING, "piedpiper"));
assertThat(HttpEncoders.getWrapperForRequest(request)).isNull();
}
}