본문 바로가기
C.W.K.
Stream
Lesson 04 of 05 · published

압축과 연결 재사용 — 바이트와 왕복 줄이기

~10 min · caching-perf, compression, gzip, brotli, keep-alive

Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"HTTP 성능의 오래된 기본기는 여전히 강해. 본문은 필요할 때 압축하고, 이미 맺은 연결은 버리지 말고 재사용해. 둘 다 전송 계층에서 협상되므로 애플리케이션 코드에는 잘 드러나지 않지만, 설정이 빠지면 비용은 곧바로 드러나."

콘텐츠 압축 — 헤더로 협상하고 투명하게 해제하기

HTTP 본문 압축은 두 헤더로 협상해.

  • Accept-Encoding (요청) — 클라이언트가 해제할 수 있는 콘텐츠 인코딩 목록이야. 예를 들면 gzip, br, zstd, identity가 있어.
  • Content-Encoding (응답) — 서버가 실제 응답 본문에 적용한 인코딩을 알려 줘.

브라우저는 지원하는 Accept-Encoding을 보내고, 서버나 CDN은 그중 제공 가능한 표현을 골라 본문을 압축해. 클라이언트는 대개 응답을 투명하게 해제하므로 애플리케이션은 디코딩된 내용을 보게 돼. 인코딩에 따라 본문이 달라진다면 캐시에는 Vary: Accept-Encoding도 필요해.

현재 널리 쓰이는 알고리즘은 이 셋이야.

  • gzip — 지원 범위가 넓고 인코딩과 디코딩이 빠른 편이야. 텍스트의 반복 정도와 설정에 따라 크기를 크게 줄일 수 있어 안전한 기본 선택으로 자주 쓰여.
  • Brotli (br) — 텍스트에서 gzip보다 더 작은 결과를 만들 수 있고 디코딩 속도도 실용적이야. 다만 높은 품질의 인코딩은 CPU 비용이 커서 정적 자산 사전 압축에 특히 잘 맞아.
  • Zstandard (zstd) — 중간 압축 수준에서 속도와 압축률의 균형이 좋아. HTTP 콘텐츠 인코딩 지원은 늘고 있지만 모든 클라이언트와 중간 장치가 지원한다고 가정하면 안 돼.

정적 자산은 사전 압축, 동적 응답은 요청별 압축

인코딩 비용은 한 번 치르는지 요청마다 치르는지에 따라 의미가 달라져.

  • 정적 자산 — 빌드 단계에서 높은 품질의 Brotli로 미리 압축하고, Accept-Encoding 협상 결과에 따라 .br 파일을 제공할 수 있어. 한 번 쓴 CPU 비용을 많은 요청에 나누는 방식이야.
  • 동적 응답 — 중간 품질의 gzip이나 Brotli(level 4-6)를 요청마다 적용해. 어느 크기부터 이득인지는 응답 내용, CPU 여유, 네트워크 비용에 따라 달라지므로 ~1KB 같은 임계값은 출발점일 뿐 측정으로 조정해야 해.

JPEG, WebP, 동영상, .tar.gz처럼 이미 압축된 콘텐츠는 다시 압축해도 거의 줄지 않아. CPU만 쓰고 결과가 더 커질 수도 있으니 보통 제외해.

연결 유지 — 이미 맺은 연결 재사용하기

HTTP/1.0의 기본 동작은 응답마다 TCP 연결을 닫는 방식이었어. 새 연결에는 TCP 핸드셰이크가 필요하고, HTTPS라면 TLS 협상 비용도 더해져. 자산마다 새 연결을 순차적으로 만든다면 왕복 시간이 계속 누적돼. 실제 브라우저는 여러 연결을 병렬로 열어 완화하지만, 핸드셰이크 비용과 연결 수 제한 자체는 남아.

HTTP/1.1은 지속 연결이 기본이고, 오래된 구현에서는 Connection: keep-alive로 이를 명시하기도 해. 응답 뒤 연결을 열어 두면 후속 요청이 같은 TCP와 TLS 세션을 재사용할 수 있어. 절약량은 RTT, TLS 버전, 서버 처리 시간, 연결 풀 동작에 따라 달라지므로 고정된 밀리초로 단정하지 말고 새 연결과 재사용 연결을 같은 조건에서 측정해야 해.

HTTP/2는 한 연결에 여러 동시 스트림을 다중화해 연결 재사용의 범위를 넓혀. HTTP/3는 QUIC 위에서 스트림별 전송과 연결 이동을 지원해, 조건이 맞으면 휴대폰이 Wi-Fi와 셀룰러망 사이를 오갈 때도 연결을 이어 갈 수 있어.

압축과 연결 재사용은 화려하지 않지만 먼저 확인할 기본기야. HTTP/3나 엣지 실행 같은 고급 최적화를 논하기 전에 실제 응답의 Content-Encoding, Vary, 연결 재사용 여부를 측정해. 둘 다 CPU, 메모리, 지연 시간과 맞바꾸는 선택이므로 “공짜”라고 가정하지 말고 운영 부하에 맞게 조정해야 해.

연결 관리의 함정

1. 모든 응답에 Connection: close를 보내기. 오래된 서버나 프록시가 요청마다 Connection: close를 보내면 클라이언트는 후속 요청에 연결을 재사용할 수 없어. 중간 장치까지 포함해 누가 헤더를 붙이는지 찾아야 해.

2. 유휴 연결을 너무 오래 유지하기. 지속 연결은 서버의 소켓과 메모리를 사용해. 서버와 프록시의 keep-alive timeout, 클라이언트 연결 풀 크기를 실제 요청 간격과 동시 접속 수에 맞춰 조정해.

3. TLS 세션 재개를 놓치기. 연결을 재사용하지 못해 새 연결이 필요하더라도 TLS 세션 재개가 가능하면 협상 비용을 줄일 수 있어. TLS 1.3 설정과 세션 티켓 정책이 실제로 동작하는지 확인해.

cwkPippa의 압축 현실

cwkPippa의 FastAPI/Uvicorn은 Starlette GZipMiddleware를 통해 일정 크기 이상의 응답을 gzip으로 압축할 수 있어. 1KB 임계값은 작은 응답의 프레이밍과 CPU 비용을 피하려는 출발점이야. Vite는 해시가 붙은 정적 자산을 만들지만 Brotli 사전 압축 파일을 기본으로 생성하지는 않아. .br 파일 제공은 빌드 플러그인이나 CDN·역방향 프록시 설정이 별도로 맡아야 해. Uvicorn의 HTTP/1.1 지속 연결과 Vercel 엣지의 압축·연결 처리는 애플리케이션 밖에서 일어나므로, 추측 대신 실제 응답 헤더와 연결 동작으로 확인하는 게 맞아.

Code

curl --compressed로 압축 협상 확인하기·bash
# curl 로 압축 negotiate
curl -v --compressed https://creativeworksofknowledge.com/ \
  2>&1 | grep -E '(Accept-Encoding|Content-Encoding)'
# > Accept-Encoding: deflate, gzip, br, zstd
# < content-encoding: br
# (curl 가 자동 decompress; 보는 body 가 plaintext)

# --compressed 없으면 curl 가 압축 안 요청
curl -v https://creativeworksofknowledge.com/ 2>&1 | grep -i encoding
# > (Accept-Encoding 안 보냄 → server 가 plain text 돌려줌)

# 승리 측정: 압축 vs 없이 byte 크기
echo 'brotli 와:'  ; curl -s --compressed https://creativeworksofknowledge.com/ | wc -c
echo '없이:'     ; curl -s              https://creativeworksofknowledge.com/ | wc -c
FastAPI GZipMiddleware — 최소 크기를 둔 자동 압축·python
# FastAPI — response 에 gzip 압축 활성
from fastapi import FastAPI
from starlette.middleware.gzip import GZipMiddleware

app = FastAPI()

# minimum_size 가 작은 response 압축 CPU 비용 피함
app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=6)

@app.get('/api/posts')
async def list_posts():
    return {'items': [...]}  # response body > 1KB → 자동 gzipped

# Brotli 위해선 brotli-asgi 쓰거나 reverse proxy 통해 사전 압축 파일 서빙
httpx.Client로 연결 풀을 재사용해 핸드셰이크 줄이기·python
# Client 쪽 — httpx.Client 로 연결 재사용 (권장 패턴)
import httpx
import time

# 잘못: 호출 당 새 연결
start = time.perf_counter()
for i in range(5):
    httpx.get('https://api.example.com/users/' + str(i))  # 매번 새 TCP+TLS
print(f'재사용 없음: {(time.perf_counter() - start) * 1000:.0f}ms')

# 맞음: Client 통해 한 연결 재사용
start = time.perf_counter()
with httpx.Client() as c:
    for i in range(5):
        c.get('https://api.example.com/users/' + str(i))  # 총 TCP+TLS handshake 하나
print(f'재사용:    {(time.perf_counter() - start) * 1000:.0f}ms')

# HTTP/2 multiplexing 이득 위해 http2=True 추가
with httpx.Client(http2=True) as c:
    c.get('https://api.example.com/users/42')

External links

Exercise

5KB가 넘는 JSON을 제공하는 테스트 엔드포인트나 공개 API를 골라. (1) Accept-Encoding 없이 평문으로 요청하고, (2) --compressed로 gzip이나 Brotli를 협상해 요청하고, (3) curl -w '%{size_download}/%{size_header}\n'으로 전송된 본문과 헤더 크기를 기록해 압축 비율을 계산해. 그다음 httpx.Client() 하나로 연결을 재사용하며 100번 호출한 경우와 요청마다 새 클라이언트를 만든 경우의 시간을 비교해. 공개 API에는 과도한 부하를 주지 말고 요청 수와 간격을 조절해.
Hint
압축률은 JSON의 중복 정도에 따라 크게 달라지므로 특정 백분율을 정답으로 두지 마. curl의 출력 본문은 --compressed를 쓰면 해제된 상태이므로 파이프로 wc -c를 재면 전송 크기가 아니라 해제된 크기를 보게 돼. 전송량 비교에는 size_download나 패킷 측정을 사용해. 연결 재사용 효과도 RTT, TLS 재개, 서버 처리 시간에 따라 달라지니 같은 서버와 같은 요청을 번갈아 여러 차례 측정하고 중앙값을 비교해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.