Log
http2에 대해서 직접 테스트
2026.05.18
HTTP/2 적용 및 성능 측정
운영 중인 서비스의 nginx에 HTTP/2를 적용하고, 성능 개선 효과를 정량적으로 측정한 작업.
요약
| 환경 | HTTP/1.1 | HTTP/2 | 개선율 |
|---|---|---|---|
| 정적 페이지, 리소스 100개 | 250ms | 159ms | -37% |
| 실제 운영 서비스 페이지 | 2.08s | 2.09s | 0% |
리소스 수가 많을수록 HTTP/2의 효과가 커졌으나, 백엔드 응답이 병목인 실 운영 페이지에서는 차이가 미미했다.
작업 동기
운영 서비스는 HTTPS만 적용된 상태였고, HTTP 버전은 확인된 적이 없었다. 다음 명령으로 검증한 결과 HTTP/1.1로 동작 중이었다.
$ curl -I --http2 https://monimentoom.duckdns.org
HTTP/1.1 200 OK
--http2 옵션을 줘도 1.1로 응답하는 것은 서버가 ALPN 협상에서 h2를 제시하지 않는다는 의미다. 설정 한 줄로 적용 가능한 표준 최적화고, 실제 효과를 측정해 다음 작업 우선순위 판단의 근거로 사용하기로 했다.
시스템 구성
[브라우저] ─HTTPS─> [EC2 nginx] ─HTTP─> [Spring Boot 블루/그린]
└ Docker Compose ┘
- nginx 1.29.6 (
nginx:alpine) - Spring Boot 백엔드 (블루/그린 무중단 배포)
- EC2 t2.micro (서울 리전)
- 프론트엔드: Vercel (별도, 이미 HTTP/2)
HTTP/2 적용
사전 확인
컨테이너 내 nginx의 빌드 옵션 확인.
$ docker exec nginx nginx -V
nginx version: nginx/1.29.6
configure arguments: ... --with-http_v2_module --with-http_v3_module ...
http_v2_module이 포함돼 별도 빌드 없이 활성화만 하면 됐다.
설정 변경
호스트에 마운트된 nginx.conf의 443 server 블록에 http2 on;을 추가했다 (nginx 1.25.1+ 신문법).
server {
listen 443 ssl;
http2 on; # 추가
server_name example.com;
ssl_certificate /etc/nginx/fullchain.pem;
ssl_certificate_key /etc/nginx/privkey.pem;
# ...
}
무중단 적용
docker exec -it nginx nginx -t # 문법 체크
docker exec -it nginx nginx -s reload # 워커 교체
nginx -s reload는 마스터 프로세스가 새 설정으로 워커만 교체하므로 다운타임이 없다. 기존 워커는 처리 중인 요청을 마무리한 뒤 종료된다.
검증
$ curl -I --http2 https://monimentoom.duckdns.org
HTTP/2 200
server: nginx/1.29.6
응답 시작 라인이 HTTP/2 200으로 바뀜. 헤더가 모두 소문자로 표시되는 것도 HTTP/2 사양의 특징이다.
측정 설계
두 가지 환경
| 환경 | 목적 | 측정 대상 |
|---|---|---|
| 정적 페이지 (test-static) | 프로토콜 효과 검증 | nginx가 직접 서빙하는 PNG 100개 동시 로드 |
| 실제 운영 서비스 | 사용자 체감 효과 검증 | Vercel 프론트엔드 → EC2 백엔드 API 호출 |
두 환경을 분리한 이유는 변수의 차이 때문이다. 정적 페이지는 백엔드 응답 시간이라는 노이즈가 없어 프로토콜 자체의 효과를 측정할 수 있다. 실 운영 페이지는 백엔드 응답이 포함된 실제 사용자 경험을 반영한다.
측정 페이지 (test-static)
쿼리스트링으로 리소스 개수를 조절할 수 있게 구현했다.
const count = parseInt(new URLSearchParams(location.search).get('n') || '100', 10);
const start = performance.now();
let loaded = 0;
for (let i = 0; i < count; i++) {
const img = new Image();
img.onload = img.onerror = () => {
loaded++;
if (loaded === count) {
document.title = `${(performance.now() - start).toFixed(0)}ms`;
}
};
img.src = `/test-static/img/tile.png?v=${i}&t=${start}`;
document.body.appendChild(img);
}
쿼리스트링이 매번 달라 브라우저 캐시 영향을 받지 않는다.
측정 변수
- HTTP 버전: 1.1, 2
- 리소스 개수: 10, 50, 100
- 시나리오당 측정 횟수: 5회 (평균 사용)
HTTP/1.1로 전환은 http2 on;을 주석 처리하고 reload하는 방식.
측정 노이즈 통제
초기 측정에서 같은 페이지의 새로고침 결과가 16ms, 355ms, 5.43s까지 변동했다. 원인 분석 과정:
| 가설 | 검증 방법 | 결과 |
|---|---|---|
| keep-alive 만료 | 연속 새로고침 vs 간격 새로고침 | 일부 영향 (수백 ms) |
| EC2 CPU 크레딧 고갈 | top, load average | 정상 (idle 99%+) |
| DNS 변동 | time dig 측정 | 주범 (1~3초 변동) |
무료 동적 DNS(DuckDNS)의 응답 시간이 1~3초까지 변동하는 것이 5초 단위 측정 편차의 주된 원인이었다.
DNS 변수 제거 후 검증
/etc/hosts에 IP를 박아 DNS 조회를 우회한 뒤 curl로 단계별 시간을 측정했다.
$ curl -w "DNS: %{time_namelookup}s | TCP: %{time_connect}s | TLS: %{time_appconnect}s | Total: %{time_total}s\n" \
-o /dev/null -s https://monimentoom.duckdns.org
DNS: 0.000s | TCP: 0.001s | TLS: 0.038s | Total: 0.040s (5회 모두 유사)
서버 응답은 일관되게 40ms 내외. 측정 편차의 원인은 클라이언트 측 DNS였음이 확인됐다.
측정 절차 표준화
1. /etc/hosts에 IP 박기 (DNS 변수 제거)
2. macOS DNS 캐시 비우기: sudo dscacheutil -flushcache
3. 크롬 완전 종료 후 재시작
4. chrome://net-internals에서 DNS와 소켓 풀 비우기
5. Secure DNS(DoH) 비활성화
6. 시크릿창 + Disable cache 활성화
7. 시나리오당 5회 측정, 평균 사용
측정 결과
환경 A: 정적 페이지
| 리소스 수 | HTTP/1.1 (ms) | HTTP/2 (ms) | 개선율 |
|---|---|---|---|
| 10개 | 88 | 70 | -21% |
| 50개 | 170 | 110 | -35% |
| 100개 | 250 | 159 | -37% |
리소스 수에 비례해 개선 폭이 커지는 경향이 명확히 나타났다.
환경 B: 실제 운영 서비스
| 환경 | HTTP/1.1 (s) | HTTP/2 (s) | 개선율 |
|---|---|---|---|
| Vercel 프론트 + EC2 API | 2.076 | 2.092 | +0.8% (측정 노이즈 수준) |
실 운영 페이지에서는 의미 있는 차이가 측정되지 않았다.
결과 해석
환경 A: 멀티플렉싱 효과 확인
HTTP/1.1은 도메인당 6개 동시 연결 제한이 있어 100개 리소스를 17라운드에 나눠 받는다(100÷6 올림). HTTP/2는 단일 연결에서 100개 모두 멀티플렉싱으로 처리하므로 1라운드에 끝난다.
HTTP/1.1: 6개 × 17라운드 = 100개
HTTP/2: 100개 × 1라운드
라운드 수 차이가 시간 차이로 직접 반영됐다. 리소스 10개에서는 차이가 작고, 50~100개로 갈수록 격차가 벌어지는 곡선이 이 원리와 일치한다.
환경 B: 차이가 없는 이유
세 가지 요인이 복합적으로 작용했다.
-
백엔드 응답 시간이 페이지 로드의 대부분을 차지. Spring Boot의 DB 쿼리와 비즈니스 로직 처리 시간이 HTTP 프로토콜 오버헤드보다 훨씬 크다. 프로토콜 차이가 백엔드 처리 시간에 묻혔다.
-
API 호출 개수가 적음. 멀티플렉싱 효과는 동시 요청이 많을 때 큰데, 페이지당 API 호출이 소수다.
-
Vercel은 이미 HTTP/2 적용. 프론트엔드 정적 자원(React 번들, CSS, 이미지)은 처음부터 HTTP/2로 전송되고 있었다. 이번 작업으로 영향받는 부분은 EC2 백엔드 API 호출뿐이다.
부가 발견
측정 과정에서 발견한 다른 최적화 후보:
- S3 이미지가 HTTP/1.1로 강제 전송됨. S3는 HTTP/2를 지원하지 않는다. CloudFront 도입 시 HTTP/2/3 자동 적용 + 엣지 캐싱 효과를 동시에 얻을 수 있다.
- DuckDNS의 느린 응답. 1~3초 변동은 첫 접속 사용자에게 그대로 영향을 준다. Route 53 또는 Cloudflare DNS로 이전을 고려할 수 있다.
- 백엔드 응답 시간이 최대 병목. HTTP 프로토콜보다 큰 개선 여지가 백엔드 로직과 DB 쿼리에 있다.
HTTP/3 적용 계획
직접 실험은 향후 작업으로 두고, 적용 방법과 예상 효과를 정리해둔다.
동작 원리
HTTP/3는 TCP 대신 QUIC(UDP 기반) 위에서 동작한다. HTTP/2의 한계인 TCP 레이어의 Head-of-Line Blocking을 해결한다.
| 항목 | HTTP/2 | HTTP/3 |
|---|---|---|
| 전송 계층 | TCP | QUIC (UDP) |
| 멀티플렉싱 | 애플리케이션 계층 | 전송 계층 (스트림 독립) |
| 패킷 손실 시 | 전체 스트림 블록 | 해당 스트림만 영향 |
| 핸드셰이크 | TCP+TLS = 2~3 RTT | 1 RTT (재방문 시 0-RTT) |
| 네트워크 전환 | 연결 끊김 | 연결 유지 |
적용 방법
server {
listen 443 ssl;
listen 443 quic reuseport; # UDP 리스너 추가
http2 on;
server_name example.com;
ssl_protocols TLSv1.3; # HTTP/3는 TLS 1.3 필수
add_header Alt-Svc 'h3=":443"; ma=86400';
# ...
}
추가 작업:
- AWS 보안그룹에 UDP 443 포트 인바운드 허용
- docker-compose.yaml의 ports에
"443:443/udp"추가 후 컨테이너 재생성 - nginx 빌드에
--with-http_v3_module포함 확인 (현재 이미지에 포함됨)
예상 효과
| 시나리오 | 예상 효과 |
|---|---|
| 정적 페이지 (이상적 환경) | HTTP/2 대비 동등 또는 소폭 개선 |
| 모바일/패킷 손실 환경 | 10~20% 개선 (HTTP/2 대비) |
| 재방문 사용자 | 100~200ms 단축 (0-RTT) |
| 네트워크 전환 (WiFi ↔ LTE) | 연결 끊김 방지 (UX 개선) |
HTTP/3의 효과는 단순 속도보다 악조건에서의 안정성에서 더 크게 나타날 것으로 예상한다. 측정 시 크롬 개발자도구의 Network Throttling으로 모바일 환경을 시뮬레이션해 비교한다.
한계
- 측정 횟수. 시나리오당 5회는 경향 확인에는 충분하나, 통계적 신뢰구간을 보고하기에는 부족하다.
- 단일 클라이언트. 단일 위치, 단일 네트워크에서의 측정이라 일반화에 한계가 있다.
- 모바일 환경 미측정. Throttling 시뮬레이션은 가능하나 실제 디바이스 측정은 진행하지 않았다.
사용한 명령어
# HTTP 버전 확인
curl -I --http2 https://monimentoom.duckdns.org
# nginx 빌드 옵션 확인
docker exec nginx nginx -V
# 설정 적용
docker exec nginx nginx -t
docker exec nginx nginx -s reload
# DNS 응답 시간
time dig example.com +short
# 단계별 응답 시간
curl -w "DNS: %{time_namelookup}s | TCP: %{time_connect}s | TLS: %{time_appconnect}s | TTFB: %{time_starttransfer}s | Total: %{time_total}s\n" \
-o /dev/null -s https://monimentoom.duckdns.org
# DNS 변수 제거 (측정 환경 통제)
echo "X.X.X.X example.com" | sudo tee -a /etc/hosts
sudo dscacheutil -flushcache
참고 자료
- RFC 7540 (HTTP/2)
- RFC 9114 (HTTP/3)
- nginx HTTP/2 documentation
- nginx HTTP/3 (QUIC) documentation