Accept-Encoding의 이해

구경회·2021년 7월 9일
4
post-thumbnail
post-custom-banner

개요

웹의 여러 요소들은 그냥 주고 받기에는 너무 크다. 따라서 여러 압축 알고리즘을 통해 압축을 한 후 주고 받는다. 그런데, 어떤 것들은 이미 압축이 되어 다시 압축할 필요가 없는 경우도 있고 클라이언트나 서버에서 어떤 알고리즘은 지원하고 어떤 것은 그렇지 않을 수도 있다. 서버와 클라이언트는 어떻게 협상하는가? 클라이언트가 서버에게 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();
    }
}

참고문헌

profile
즐기는 거야
post-custom-banner

0개의 댓글