[HTTP] Content-Encoding을 활용한 압축 전송
대부분의 사이트를 돌아보면 Content-Encoding이라는 응답 헤더를 설정해서 사용하는 것을 볼 수 있다.
이 응답 헤더는 브라우저에게 Response Body를 어떻게 인코딩할지를 알려주는 역할을 한다.
여기에는 단순한 문자열 인코딩 정보가 들어가는게 아니라, 압축 정보가 들어간다.
gzip 같은 대중적인 몇가지 압축 포맷은 브라우저가 직접 지원을 하기 때문에 직접 압축해제를 하게끔 할 수 있다.
서버 입장에서는 매우 큰 데이터가 보내야 할때 압축해서 보내는 것으로 응답 시간과 네트워크 트래픽을 줄일 수 있는 것이다.
low level 제어가 가능한 Rust hyper 환경을 사용해서 간단한 예제를 보여보겠다.
일단 먼저 gzip으로 response body를 압축해서 보내보자. 아직 헤더를 설정하지는 않았다.

그러면, 브라우저는 알 수 없는 압축된 바이너리를 받았으니, 그냥 다운로드를 시켜버린다. 당연한 동작이다.
이제 Content-Encoding 헤더 값을 gzip으로 설정해주면,

브라우저는 해당 바이트열이 gzip으로 압축된 것임을 알기 때문에 압축을 해제해서 표현해주게 된다.
대부분 Front side에서 처리를 해줘야할 부분은 딱히 없다. 브라우저에서 지원하는 기능이기 때문이다.
사이즈와 성능도 한번 비교해볼까?
실제 네트워크 트래픽을 타야지 공정한 비교가 되는지라, 로컬호스트로는 객관적인 테스트가 불가능하다.
AWS EC2 서울 리전에 인스턴스 하나 띄워서 데이터 와리가리를 해봤다.
50메가짜리 무식한 텍스트를 생성해서 전송했는데
gzip 압축을 적용하지 않았을 때는
50메가바이트어치가 그대로 날라왔고
서버 응답에 1초, 실제 다운받는데는 23초가 걸렸다.
하지만 gzip을 적용하고 나서는
50킬로바이트를 전송받았고
서버 실행시간은 3초로 길어진 대신, 다운로드 시간이 2초 미만으로 급격하게 줄어들었다.
서버가 압축 로직을 돌려야 하기 때문에 서버 실행시간이 약간 늘긴 했지만, 결과적으로 전송 시간이 줄어들었기 때문에 7-8배의 속도 최적화 결과를 봤다.
물론 지금 내 경우에는 응답 텍스트를 대충 만들어서 압축이 굉장히 잘 되는 형태이긴 하다.
실제 usecase에서는 이보다는 좀 압축률이 떨어지겠지만, 최소한 몇배 정도는 최적화 효과가 나올 것이다.
response body가 꽤 커질 수 있다면 웬만해서는 적용하지 않을 이유가 딱히 없다.
지원되는 포맷
가장 대중적으로 사용되는 압축 포맷은 gzip이다.
브라우저 지원 수준도 최상이고, 압축률도 좋고, 성능도 무난한 편이기 때문이다.
그래서 다른 서비스들 둘러보면 대부분 gzip만을 쓸 것이다.
하지만 gzip으로 부족하다면 다른 포맷들을 사용해도 된다.
기타 포맷에는 deflate, br, zstd, compress, brotli 등이 있다.
gzip와 deflate를 제외하면 브라우저 지원 수준이 애매하기 때문에, 잘 확인하고 쓰는게 좋다.
저 중에서도 compress는 사실상 사양된 알고리즘이다.
테스트에 사용한 코드
[package]
name = "just_test"
version = "0.1.0"
edition = "2021"
[dependencies]
hyper = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["full"] }
http-body-util = "0.1"
hyper-util = { version = "0.1", features = ["full"] }
flate2 = "1.0"use std::convert::Infallible;
use std::io::Write;
use std::net::SocketAddr;
use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
fn generate_big_text(size: usize) -> String {
let mut s = String::new();
for i in 0..size {
if i % 2 == 0 {
s.push('b');
} else {
s.push('a');
}
}
s
}
async fn hello(_: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
// Generate a big text
let big_text = generate_big_text(1024 * 1024 * 50);
let gzip_encoded = {
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
encoder.write_all(big_text.as_bytes()).unwrap();
encoder.finish().unwrap()
};
let response = Response::builder()
.status(StatusCode::OK)
.body(Full::new(Bytes::from(gzip_encoded)))
.unwrap();
Ok(response)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
// We create a TcpListener and bind it to 127.0.0.1:3000
let listener = TcpListener::bind(addr).await?;
// We start a loop to continuously accept incoming connections
loop {
let (stream, _) = listener.accept().await?;
// Use an adapter to access something implementing `tokio::io` traits as if they implement
// `hyper::rt` IO traits.
let io = TokioIo::new(stream);
// Spawn a tokio task to serve multiple connections concurrently
tokio::task::spawn(async move {
// Finally, we bind the incoming connection to our `hello` service
if let Err(err) = http1::Builder::new()
// `service_fn` converts our function in a `Service`
.serve_connection(io, service_fn(hello))
.await
{
println!("Error serving connection: {:?}", err);
}
});
}
}참조
https://blog.naver.com/dlaxodud2388/221928144324
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding