HTTP/2 & HTTP/3
HTTP/2 & HTTP/3: 웹 성능의 진화
섹션 제목: “HTTP/2 & HTTP/3: 웹 성능의 진화”1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”HTTP/2는 HTTP/1.1의 텍스트 기반 프로토콜을 바이너리 프레이밍으로 재설계해 멀티플렉싱을 가능하게 한 버전이고, HTTP/3는 TCP 대신 UDP 기반의 QUIC 프로토콜 위에서 동작해 연결 지연과 Head-of-Line Blocking을 근본적으로 해결한 버전이다.
전제 지식: L1의 http-basics.md에서 HTTP 요청/응답 구조, 메서드, 상태 코드, TCP 3-way handshake 과정을 다뤘다. 이 문서는 “HTTP가 왜 느린가”부터 시작해 그 해결 과정을 추적한다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”- 웹 성능의 핵심 변수: HTTP 버전은 요청 병렬성, 핸드셰이크 RTT, 헤더 중복, 패킷 손실 회복 방식을 바꾼다. 따라서 “Nginx에서 http2 켜기”가 효과적인지 보려면 단순 평균 응답시간보다
Protocol,Waiting for server response, 재전송률, 연결 수를 함께 봐야 한다. - gRPC의 기반: 마이크로서비스 간 통신에서 gRPC가 HTTP/2 위에서 동작하는 이유, 그리고 그것이 가져다주는 성능 이점을 이해해야 한다.
- 인프라 설계 직결: ALB의
HTTP/2설정, CloudFront의 프로토콜 정책, Nginx 설정이 실제로 어떤 의미인지 판단할 수 있어야 한다. - 디버깅 능력: Chrome DevTools의 Network 탭에서 Protocol 컬럼이
h2인지http/1.1인지, Waterfall 차트 모양이 어떻게 달라지는지 읽을 수 있어야 한다.
2.5 선행 기술의 한계에서 HTTP/2·3가 나온 이유
섹션 제목: “2.5 선행 기술의 한계에서 HTTP/2·3가 나온 이유”HTTP/1.x의 병목은 “HTTP 문법이 텍스트라서 느리다”보다 동시성을 커넥션 수로 해결해야 한다는 구조에 있었다. RFC 9113은 HTTP/1.0이 TCP 커넥션 하나에 outstanding request를 1개만 둘 수 있었고, HTTP/1.1 pipelining도 application-layer HOL Blocking을 완전히 풀지 못해 클라이언트가 여러 TCP 커넥션을 열었다고 설명한다. HTTP/2는 이 한계를 하나의 TCP 커넥션 안에 여러 stream을 섞어 보내는 binary framing으로 풀었다. 같은 HTML이 style.css, app.js, hero.jpg를 동시에 요구할 때 HTTP/1.1은 보통 브라우저의 도메인별 연결 한도에 걸리지만, HTTP/2는 각 요청을 별도 stream으로 나누고 HEADERS/DATA frame에 stream ID를 붙여 순서 독립적으로 재조립한다.
하지만 HTTP/2도 TCP 위에서 동작하므로 패킷 하나가 사라지면 TCP byte stream 전체가 재전송을 기다리는 문제는 남는다. RFC 9113도 TCP HOL Blocking은 HTTP/2가 해결하지 않는다고 명시한다. HTTP/3가 QUIC으로 이동한 이유가 여기에 있다. RFC 9114의 HTTP/3는 request-response pair를 QUIC stream 하나에 매핑하고, 한 stream의 손실이 다른 stream 진행을 막지 않도록 설계한다. 즉 계보는 HTTP/1.1: 커넥션 병렬화로 우회 → HTTP/2: 애플리케이션 stream 다중화 → HTTP/3: 전송 계층 stream 독립성이다.
3. HTTP/1.1의 한계
섹션 제목: “3. HTTP/1.1의 한계”3-1. 커넥션 하나에 요청 하나: HOL Blocking
섹션 제목: “3-1. 커넥션 하나에 요청 하나: HOL Blocking”HTTP/1.1은 기본적으로 하나의 TCP 커넥션에서 요청-응답이 순차적으로 처리된다. 브라우저가 HTML을 받은 후 CSS, JS, 이미지 등 수십 개의 리소스를 요청해야 할 때:
TCP 커넥션 1: → GET /style.css ← 기다림... ← 응답 ← 이제 다음 요청 가능 → GET /app.js ← 또 기다림... ← 응답앞의 요청이 느리면 뒤의 요청 전부가 막힌다. 이것이 HTTP/1.1의 Head-of-Line (HOL) Blocking이다.
3-2. HTTP Pipelining: 실패한 해결책
섹션 제목: “3-2. HTTP Pipelining: 실패한 해결책”HTTP/1.1 스펙에는 Pipelining이라는 기능이 있다. 응답을 기다리지 않고 여러 요청을 미리 보내는 방식이다.
클라이언트 → GET /a, GET /b, GET /c (연속 전송)서버 ← /a 응답, /b 응답, /c 응답 (순서대로 응답)문제는 서버가 반드시 요청 순서대로 응답을 보내야 한다는 점이다. /b가 빠르게 처리되더라도 /a 응답이 완료될 때까지 기다려야 한다. HOL Blocking이 TCP 레벨로 내려갔을 뿐이다. 결국 대부분의 브라우저가 Pipelining을 기본 비활성화했다.
3-3. 커넥션 수 제한과 도메인 샤딩
섹션 제목: “3-3. 커넥션 수 제한과 도메인 샤딩”브라우저는 HOL Blocking을 우회하기 위해 도메인당 6개의 병렬 TCP 커넥션을 열었다.
브라우저 ──커넥션1──→ GET /style.css ──커넥션2──→ GET /app.js ──커넥션3──→ GET /logo.png ──커넥션4──→ GET /font.woff2 ──커넥션5──→ GET /analytics.js ──커넥션6──→ GET /hero.jpg ← 7번째 요청은 커넥션이 생길 때까지 대기이 문제를 우회하기 위해 static1.example.com, static2.example.com처럼 여러 도메인으로 리소스를 분산하는 도메인 샤딩(Domain Sharding) 기법이 유행했다. 하지만:
- DNS 조회 비용이 늘어난다
- TLS 핸드셰이크 비용이 도메인마다 발생한다
- HTTP/2에서는 오히려 역효과가 난다 (뒤에서 설명)
3-4. 헤더 중복 전송
섹션 제목: “3-4. 헤더 중복 전송”HTTP/1.1 헤더는 텍스트이고 매 요청마다 전체를 다시 전송한다.
GET /api/users HTTP/1.1Host: api.example.comAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...Accept: application/jsonAccept-Encoding: gzip, deflate, brAccept-Language: ko-KR,ko;q=0.9Cookie: session=abc123; _ga=GA1.2.xxx이 헤더들이 동일한 도메인에 수십 번 요청할 때마다 반복된다. Authorization, User-Agent, Cookie는 거의 변하지 않는데도 매번 전송한다. 요청당 수백 바이트에서 수 킬로바이트의 낭비가 발생한다.
4. HTTP/2 핵심 개념
섹션 제목: “4. HTTP/2 핵심 개념”HTTP/2는 2015년 RFC 7540으로 표준화됐다. 기반은 Google의 SPDY 프로토콜이다.
4-1. 바이너리 프레이밍 레이어
섹션 제목: “4-1. 바이너리 프레이밍 레이어”HTTP/1.1이 텍스트 기반이라면 HTTP/2는 바이너리 프레임(Binary Frame) 기반이다.
HTTP/1.1 요청 (텍스트):GET /api/data HTTP/1.1\r\nHost: example.com\r\n\r\n
HTTP/2 요청 (바이너리 프레임):┌──────────────────────────────────────────┐│ Length (3 bytes) │ Type (1 byte) ││ Flags (1 byte) │ Stream ID (4 bytes) ││ Payload... │└──────────────────────────────────────────┘프레임 타입:
HEADERS: HTTP 헤더를 전달 (요청/응답 시작)DATA: 바디 데이터를 전달SETTINGS: 커넥션 설정 교환WINDOW_UPDATE: 흐름 제어PING: 커넥션 유지 확인RST_STREAM: 스트림 강제 종료GOAWAY: 커넥션 종료 알림
바이너리로 파싱하면 텍스트 파싱에 비해 오류 가능성이 줄고 처리 속도가 빠르다. 사람이 읽기는 어렵지만 기계가 처리하기는 훨씬 효율적이다.
4-2. 스트림과 멀티플렉싱
섹션 제목: “4-2. 스트림과 멀티플렉싱”HTTP/2의 핵심은 하나의 TCP 커넥션 안에서 여러 요청-응답을 동시에 처리할 수 있다는 점이다. 이것이 **멀티플렉싱(Multiplexing)**이다.
스트림(Stream): 하나의 TCP 커넥션 내에서 독립적인 양방향 바이트 흐름. 각 스트림은 고유한 ID를 가진다.
TCP 커넥션 1개│├── 스트림 1: GET /style.css (HEADERS 프레임 → DATA 프레임)├── 스트림 3: GET /app.js (HEADERS 프레임 → DATA 프레임)├── 스트림 5: GET /logo.png (HEADERS 프레임 → DATA 프레임)└── 스트림 7: GET /font.woff2 (HEADERS 프레임 → DATA 프레임) ↑ 모두 동시에 진행 중, 순서 보장 없음클라이언트는 홀수 스트림 ID(1, 3, 5…), 서버 푸시는 짝수 스트림 ID(2, 4, 6…)를 사용한다.
시간 →TCP 커넥션:[S1:HEADERS][S3:HEADERS][S5:HEADERS][S1:DATA...][S3:DATA...][S5:DATA...] ↑ ↑ ↑ CSS 요청 JS 요청 이미지 요청 → 섞여서 전송되지만 스트림 ID로 구분결과: HTTP/1.1에서 6개의 TCP 커넥션이 필요했던 작업을 HTTP/2는 1개의 TCP 커넥션으로 처리한다. TCP 핸드셰이크 비용이 한 번으로 줄어든다.
4-3. 스트림 우선순위 (Stream Prioritization)
섹션 제목: “4-3. 스트림 우선순위 (Stream Prioritization)”모든 리소스가 동등하지 않다. HTML > CSS > JS > 이미지 순으로 중요하다. HTTP/2는 스트림 간 **의존성과 가중치(weight)**를 설정할 수 있다.
HEADERS 프레임: Stream-Dependency: 0 ← 부모 스트림 없음 (root) Weight: 256 ← 높은 우선순위
HEADERS 프레임: Stream-Dependency: 1 ← 스트림 1에 의존 Weight: 16 ← 낮은 우선순위실무에서는 브라우저가 자동으로 우선순위를 설정한다. 개발자가 직접 제어하기보다 “우선순위 메커니즘이 있다”는 사실을 알고 서버(Nginx, h2o)가 이를 올바르게 구현했는지 확인하는 것이 중요하다. 단, RFC 9113은 RFC 7540의 tree 기반 priority signaling을 deprecated 처리했다. 따라서 LCP 개선을 priority weight 하나에 기대하면 안 된다. nghttp -nv https://example.com/로 PRIORITY/HEADERS 순서를 확인했는데도 DevTools Waterfall에서 CSS보다 대형 이미지가 먼저 대역폭을 점유한다면, preload, critical CSS, 이미지 lazy loading 같은 리소스 힌트와 번들 전략을 먼저 조정한다.
주의: HTTP/3(QUIC)에서는 스트림 우선순위 메커니즘이 PRIORITY_UPDATE 프레임으로 변경됐고, 구현체마다 지원 수준이 다르다.
5. HPACK 헤더 압축
섹션 제목: “5. HPACK 헤더 압축”HTTP/2는 HPACK이라는 헤더 압축 알고리즘을 도입해 반복되는 헤더를 효율적으로 전송한다.
5-1. 정적 테이블 (Static Table)
섹션 제목: “5-1. 정적 테이블 (Static Table)”자주 쓰이는 헤더 이름과 값 조합 61개가 미리 정의된 테이블이다.
Index Header Name Header Value 1 :authority 2 :method GET 3 :method POST 4 :path / 5 :path /index.html 6 :scheme http 7 :scheme https 8 :status 200 13 :status 404 ... 32 content-type application/x-www-form-urlencodedGET / 요청은 인덱스 2(:method: GET)와 인덱스 4(:path: /)로 각각 1바이트씩 표현 가능하다.
5-2. 동적 테이블 (Dynamic Table)
섹션 제목: “5-2. 동적 테이블 (Dynamic Table)”커넥션 수명 동안 실제 전송된 헤더를 누적한다. 한 번 전송한 헤더는 이후 요청에서 인덱스 하나로 참조할 수 있다.
첫 번째 요청: authorization: Bearer eyJhbGci... → 전체 전송 + 동적 테이블에 추가 (인덱스 62)
두 번째 요청: authorization: Bearer eyJhbGci... → 인덱스 62만 전송 (1바이트)5-3. 허프만 코딩 (Huffman Coding)
섹션 제목: “5-3. 허프만 코딩 (Huffman Coding)”자주 등장하는 문자를 짧은 비트열로 인코딩하는 무손실 압축이다. HPACK 인코딩 시 선택적으로 적용된다. HTTP 헤더에 자주 쓰이는 ASCII 문자를 기준으로 최적화된 허프만 트리를 사용한다.
실제 절감 효과: 반복 요청이 많은 API 서버에서 헤더 크기가 85-90% 감소하는 사례가 보고된다.
6. 서버 푸시 (Server Push): 아이디어는 좋았지만
섹션 제목: “6. 서버 푸시 (Server Push): 아이디어는 좋았지만”6-1. 개념
섹션 제목: “6-1. 개념”HTTP/2 서버 푸시는 클라이언트가 요청하기 전에 서버가 먼저 리소스를 보내는 기능이다.
클라이언트 → GET /index.html
서버 ← PUSH_PROMISE (스트림 2: /style.css)서버 ← PUSH_PROMISE (스트림 4: /app.js)서버 ← 스트림 1: index.html 본문서버 ← 스트림 2: style.css 본문 ← 클라이언트가 요청하기 전에 이미 전송 중서버 ← 스트림 4: app.js 본문이론적으로는 HTML 파싱 전에 CSS, JS가 이미 도착해 있으므로 왕복 지연(RTT)을 줄일 수 있다.
6-2. 왜 실패했는가
섹션 제목: “6-2. 왜 실패했는가”캐시 문제: 브라우저가 이미 캐시에 해당 리소스를 가지고 있어도 서버는 그 사실을 모른다. 불필요한 데이터를 전송하게 된다.
서버: "style.css 미리 줄게" ←── 브라우저는 이미 캐시에 있음브라우저: RST_STREAM ←── 필요 없다고 거절, 하지만 이미 대역폭은 소모됨복잡한 구현: 어떤 리소스를 푸시할지 서버가 판단해야 하는데, 잘못 판단하면 오히려 성능이 나빠진다.
브라우저 지원 철회: 2022년 Chrome이 HTTP/2 서버 푸시 지원을 제거했다. 실제 성능 개선 효과가 미미하고 대역폭 낭비가 컸기 때문이다.
현재 대안: <link rel="preload"> 헤더 또는 103 Early Hints 상태 코드가 서버 푸시를 대체하고 있다.
# 103 Early Hints (서버 푸시의 현실적인 대안)location / { add_header Link "</style.css>; rel=preload; as=style"; add_header Link "</app.js>; rel=preload; as=script";}6.5 새 프로토콜 만났을 때 — 전이 분석 체크리스트
섹션 제목: “6.5 새 프로토콜 만났을 때 — 전이 분석 체크리스트”HOL Blocking·Multiplexing·Flow Control 같은 HTTP/2·3 핵심 원리는 다른 시스템에서도 반복된다. “stream 1개가 다른 stream을 막는가?” 라는 질문이 핵심.
| 원리 | HTTP/1.1 | HTTP/2 | HTTP/3 | DB Connection Pool | Kafka |
|---|---|---|---|---|---|
| HOL Blocking 위치 | 응답 단위 (1 req/conn) | TCP 패킷 단위 (1개 stream loss → 전체 차단) | 없음 — QUIC stream별 독립 | pool 1개 점유 → 나머지 대기 | partition 1개의 lag → consumer 그룹 전체 영향 |
| 다중화(Multiplexing) | X — 6 connection 병렬 | 1 connection · N stream | 1 connection · N stream (UDP) | N 연결 = N pool slot | N partition = N consumer parallelism |
| 흐름 제어 | TCP에 의존 | stream window + connection window | stream window + flow control | pool size + queue | producer rate vs consumer prefetch |
| head-of-line 회복 | 새 connection | 패킷 재전송 대기 | stream별 독립 회복 | timeout + 다른 pool slot | rebalance |
새 프로토콜/시스템을 만났을 때 5가지 진단 질문
- HOL Blocking이 어디서 발생하는가? — 응답 단위? 패킷 단위? stream 단위?
- 다중화 단위는? — connection? stream? partition? request?
- 흐름 제어 메커니즘은? — window(TCP/HTTP/2), credit(gRPC), prefetch(MQ), pool size?
- 헤더/메타데이터 압축은? — HPACK(HTTP/2), QPACK(HTTP/3), Snappy(Kafka)?
- 0-RTT/early data 가능한가? — TLS 0-RTT, QUIC 0-RTT, HTTP cache hit, Kafka idempotent producer?
HTTP/2·3 Silent Failure
섹션 제목: “HTTP/2·3 Silent Failure”| 유형 | 시나리오 | 감지 |
|---|---|---|
| HPACK dynamic table 오염 | 헤더 이름 충돌로 캐시 오염 → 잘못된 헤더 값 전달. 에러 없음 | nghttp2 trace 또는 Wireshark HTTP/2 inspector |
| QUIC ECN 무시 | 미들박스가 ECN bit를 클리어 → 혼잡 제어 약화. 측정 안 하면 모름 | curl --curl-trace + 서버 측 tc 통계 비교 |
| Server Push 무시 | 클라이언트가 push 거부(Chrome 106+ 기본 거부) → 의도한 prefetch 미작동 | nginx http2_push_status 로그, RUM(Real User Monitoring) |
| HTTP/3 fallback 침묵 | UDP 차단 환경에서 자동으로 HTTP/2 fallback → 의도한 0-RTT 효과 사라짐 | Alt-Svc 헤더 모니터링, curl --http3-only 테스트 |
(참고: RFC 7540 — HTTP/2, RFC 9114 — HTTP/3, RFC 9000 — QUIC)
7. HTTP/3 & QUIC
섹션 제목: “7. HTTP/3 & QUIC”HTTP/2가 해결하지 못한 근본적인 문제가 있었다: TCP 자체의 HOL Blocking.
7-1. HTTP/2에서도 남아 있는 TCP HOL Blocking
섹션 제목: “7-1. HTTP/2에서도 남아 있는 TCP HOL Blocking”HTTP/2는 애플리케이션 레벨의 HOL Blocking을 멀티플렉싱으로 해결했다. 그러나 TCP 레벨에서는 여전히 문제가 있다.
HTTP/2 스트림 멀티플렉싱:스트림 1, 3, 5가 동시에 TCP 패킷으로 전송됨
TCP 패킷 손실 발생: 패킷 3 (스트림 3의 일부) 손실됨 ↓ TCP는 패킷 3이 재전송될 때까지 패킷 4, 5, 6 전달을 중단 ↓ 스트림 1, 5는 자신의 데이터가 정상이지만 대기HTTP/2의 여러 스트림이 하나의 TCP 커넥션을 공유하기 때문에, 하나의 패킷 손실이 모든 스트림을 멈춘다. 파편화된 HTTP/1.1의 개별 TCP 커넥션보다 오히려 나쁠 수 있다. 무선망·국제망처럼 손실이 있는 경로에서 HTTP/3를 검토할 때는 먼저 같은 URL을 curl -w '%{http_version} %{time_connect} %{time_starttransfer}\n' --http2 https://example.com과 curl -w '%{http_version} %{time_connect} %{time_starttransfer}\n' --http3 https://example.com로 20회 이상 비교한다. HTTP/3의 p95 time_starttransfer가 줄지 않거나 실패율이 높으면 UDP 경로, CDN QUIC 설정, Alt-Svc 캐시를 먼저 의심한다.
7-2. QUIC: UDP 위의 신뢰성 있는 전송
섹션 제목: “7-2. QUIC: UDP 위의 신뢰성 있는 전송”HTTP/3는 TCP를 버리고 QUIC(Quick UDP Internet Connections) 위에서 동작한다. QUIC은 UDP를 기반으로 하되, TCP의 신뢰성 기능을 애플리케이션 레벨에서 구현한다.
HTTP/1.1: [ HTTP/1.1 ] → [ TLS ] → [ TCP ] → [ IP ]HTTP/2: [ HTTP/2 ] → [ TLS ] → [ TCP ] → [ IP ]HTTP/3: [ HTTP/3 ] → [ QUIC (TLS 1.3 내장) ] → [ UDP ] → [ IP ]QUIC의 특징:
독립 스트림: QUIC는 스트림 단위로 독립적인 전달을 보장한다. 스트림 3의 패킷이 손실되더라도 스트림 1, 5는 계속 진행된다.
QUIC 스트림 멀티플렉싱:스트림 1: [패킷A] [패킷B] ──→ 정상 도착, 즉시 전달스트림 3: [패킷X] [???] ──→ 패킷 손실, 스트림 3만 재전송 대기스트림 5: [패킷P] [패킷Q] ──→ 정상 도착, 즉시 전달Connection ID: QUIC는 IP 주소 + 포트 번호 대신 Connection ID로 커넥션을 식별한다. Wi-Fi에서 LTE로 전환되어 IP 주소가 바뀌어도 같은 커넥션을 유지할 수 있다 → 커넥션 마이그레이션(Connection Migration).
TLS 1.3 내장: QUIC는 TLS 1.3을 프로토콜에 통합했다. 별도의 TLS 핸드셰이크 과정이 없다.
7-3. 0-RTT 연결 수립
섹션 제목: “7-3. 0-RTT 연결 수립”HTTP/1.1 + TLS의 연결 수립 지연을 비교해보자.
HTTP/1.1 + TLS 1.2 (첫 연결):1. TCP SYN → (1 RTT)2. TCP SYN-ACK ←3. TCP ACK + TLS CH → (1 RTT)4. TLS SH + Cert ←5. TLS Finished → (1 RTT)6. HTTP Request →7. HTTP Response ←= 총 3 RTT 후 첫 번째 바이트 수신QUIC (첫 연결, 1-RTT):1. Initial + CRYPTO → (1 RTT, TCP + TLS 핸드셰이크 동시 처리)2. Handshake + CRYPTO ←3. HTTP Request →4. HTTP Response ←= 총 1 RTT 후 첫 번째 바이트 수신QUIC (재연결, 0-RTT):이전 세션 키(Session Ticket) 사용1. Initial + 0-RTT HTTP Request → (즉시 데이터 전송)2. HTTP Response ←= RTT 없이 데이터 전송 가능0-RTT는 이전 방문에서 캐시된 세션 정보를 재사용하는 것이다. 주의할 점은 0-RTT 데이터가 재전송 공격(Replay Attack)에 취약하다는 것이다. 따라서 0-RTT 경로에서는 멱등(idempotent) 요청(GET)만 허용하고 POST는 1-RTT 핸드셰이크 완료 후 처리해야 한다.
7-4. QUIC 내부 구조
섹션 제목: “7-4. QUIC 내부 구조”QUIC 패킷 구조:┌─────────────────────────────────────┐│ QUIC Header (Connection ID, Packet#)│├─────────────────────────────────────┤│ QUIC Frame 1 (STREAM frame) ││ Stream ID: 4 ││ Offset: 0 ││ Data: [payload] │├─────────────────────────────────────┤│ QUIC Frame 2 (ACK frame) ││ Largest Acked: 12 ││ ACK Ranges: [10-12] │└─────────────────────────────────────┘ ↓ 전체가 TLS 1.3으로 암호화됨┌─────────────────────────────────────┐│ UDP Header │└─────────────────────────────────────┘QUIC의 패킷 번호는 모노토닉하게 증가한다. 재전송 시에도 동일한 시퀀스 번호를 재사용하지 않으므로, TCP의 “재전송 모호성(Retransmission Ambiguity)” 문제가 없다.
8. HTTP/2 vs HTTP/3: HOL Blocking 비교 정리
섹션 제목: “8. HTTP/2 vs HTTP/3: HOL Blocking 비교 정리” HTTP/1.1 HTTP/2 HTTP/3 (QUIC)──────────────────────────────────────────────────────────────App-level HOL 있음 없음 (멀티플렉싱) 없음TCP-level HOL 있음 있음 없음 (UDP 기반)연결 수립 3 RTT 3 RTT 1 RTT / 0-RTT헤더 압축 없음 HPACK QPACKIP 변경 시 재연결 재연결 유지 (Connection ID)전송 레이어 TCP TCP UDP (QUIC)암호화 별도 TLS 별도 TLS TLS 1.3 내장언제 HTTP/3가 특히 효과적인가:
- 모바일 환경 (Wi-Fi ↔ LTE 전환이 잦음)
- 고지연 / 패킷 손실이 있는 네트워크
- 많은 소규모 병렬 요청
HTTP/2가 여전히 좋은 경우:
- 안정적인 유선 네트워크 내부 통신 (데이터센터 내부)
- UDP를 차단하는 방화벽 환경 (일부 기업 네트워크는 UDP 443을 막음)
- gRPC 내부 통신 (HTTP/2 기반이 안정적)
결정 기준은 “HTTP/3가 최신이니 켠다”가 아니라 실패해도 HTTP/2로 자연스럽게 돌아오는가다. RFC 9114는 UDP 차단 같은 연결 문제가 있으면 TCP 기반 HTTP 버전을 시도해야 한다고 권고한다. 운영에서는 curl --http3-only https://api.example.com/health가 실패해도 curl --http2 https://api.example.com/health가 즉시 성공해야 하고, 응답에는 Alt-Svc: h3=":443"; ma=...가 있어야 한다. 이 조건이 깨지면 사용자는 “일부 네트워크에서만 느림/접속 안 됨”을 보고하고 서버 로그에는 정상 HTTP/2 요청만 남는 silent failure가 된다. 이때는 HTTP/3를 단독 경로로 만들지 말고 TCP 443 listener와 HTTP/2 fallback을 유지한 채 QUIC 실패율을 별도 지표로 본다.
9. gRPC가 HTTP/2를 쓰는 이유
섹션 제목: “9. gRPC가 HTTP/2를 쓰는 이유”9-1. gRPC 전송 구조
섹션 제목: “9-1. gRPC 전송 구조”gRPC는 Google이 설계한 고성능 RPC 프레임워크다. 내부적으로 HTTP/2를 전송 프로토콜로 사용한다.
gRPC 메시지 전송 구조:┌──────────────────────────────────────┐│ gRPC 레이어 ││ - Protobuf 직렬화/역직렬화 ││ - 서비스 정의 (proto 파일) │├──────────────────────────────────────┤│ HTTP/2 레이어 ││ - HEADERS 프레임: gRPC 메타데이터 ││ - DATA 프레임: Protobuf 페이로드 ││ - Trailers: 상태 코드 │├──────────────────────────────────────┤│ TLS → TCP → IP │└──────────────────────────────────────┘9-2. HTTP/2를 선택한 이유
섹션 제목: “9-2. HTTP/2를 선택한 이유”양방향 스트리밍: HTTP/2의 스트림은 전이중(full-duplex) 통신을 지원한다. gRPC의 4가지 통신 패턴이 모두 가능하다.
// Unary: 단방향 요청-응답rpc GetUser (UserRequest) returns (UserResponse);
// Server Streaming: 서버가 여러 번 응답rpc ListUsers (ListRequest) returns (stream UserResponse);
// Client Streaming: 클라이언트가 여러 번 요청rpc UploadFile (stream Chunk) returns (UploadResponse);
// Bidirectional Streaming: 양방향 스트리밍rpc Chat (stream Message) returns (stream Message);멀티플렉싱: 하나의 TCP 커넥션에서 수백 개의 gRPC 호출을 동시에 처리할 수 있다. REST/HTTP/1.1 대비 커넥션 관리 오버헤드가 현저히 낮다.
헤더 압축: Protobuf로 직렬화된 페이로드가 이미 작은데, HPACK으로 헤더까지 압축하면 전체 메시지 크기가 REST/JSON 대비 훨씬 작다.
Flow Control: HTTP/2의 흐름 제어 메커니즘으로 수신 측이 처리할 수 있는 만큼만 데이터를 받을 수 있다. 백프레셔(Backpressure) 구현에 필수다.
9-3. gRPC HTTP/2 헤더 구조
섹션 제목: “9-3. gRPC HTTP/2 헤더 구조”gRPC 요청 헤더 (HEADERS 프레임)::method: POST:path: /helloworld.Greeter/SayHello:scheme: https:authority: api.example.comcontent-type: application/grpc+protogrpc-timeout: 30Sauthorization: Bearer <token>
DATA 프레임 (Protobuf 바이너리):[압축 플래그 1바이트][메시지 길이 4바이트][Protobuf 직렬화 데이터]
HEADERS 프레임 (Trailers-only):grpc-status: 0 ← 0 = OKgrpc-message: ""10. ALB/Nginx에서의 HTTP/2 설정
섹션 제목: “10. ALB/Nginx에서의 HTTP/2 설정”10-1. AWS ALB HTTP/2 설정
섹션 제목: “10-1. AWS ALB HTTP/2 설정”ALB는 기본적으로 HTTP/2를 지원하지만, 동작 방식을 이해해야 한다.
클라이언트 ─── HTTP/2 ───→ ALB ─── HTTP/1.1 ───→ 타겟 그룹(EC2/ECS)중요: ALB는 클라이언트와의 통신에서는 HTTP/2를 지원하지만, 백엔드(타겟 그룹)와의 통신은 기본적으로 HTTP/1.1을 사용한다. AWS 문서 기준으로 target group의 protocol version을 HTTP2 또는 GRPC로 지정해야 백엔드까지 HTTP/2/gRPC를 보낼 수 있고, gRPC 요청이 HTTP/1.1 target group으로 들어가면 오류 조합이다. 또한 ALB의 클라이언트 HTTP/2 연결당 최대 stream 수는 128이므로, gRPC fan-out이 큰 서비스는 채널 수와 target group connection 지표를 함께 본다.
# Terraform: ALB 리스너 HTTP/2 설정resource "aws_lb_listener" "https" { load_balancer_arn = aws_lb.main.arn port = 443 protocol = "HTTPS" ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" # TLS 1.3 지원 certificate_arn = aws_acm_certificate.main.arn
default_action { type = "forward" target_group_arn = aws_lb_target_group.app.arn }}
# gRPC 타겟 그룹 (ALB ↔ 백엔드도 HTTP/2)resource "aws_lb_target_group" "grpc" { name = "grpc-target" port = 50051 protocol = "HTTP" protocol_version = "GRPC" # ← 이 설정이 HTTP/2 end-to-end를 활성화 vpc_id = aws_vpc.main.id
health_check { path = "/grpc.health.v1.Health/Check" matcher = "0" # gRPC 상태 코드 OK }}10-2. Nginx HTTP/2 설정
섹션 제목: “10-2. Nginx HTTP/2 설정”# /etc/nginx/nginx.conf 또는 사이트 설정
http { # HTTP/2 활성화 (listen에 http2 추가) server { listen 443 ssl; http2 on; # Nginx 1.25.1+에서는 별도 지시어 # 구버전: listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/example.com.crt; ssl_certificate_key /etc/ssl/private/example.com.key;
# TLS 1.3 활성화 (HTTP/2 성능 극대화) ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256;
# HTTP/2 튜닝 http2_max_concurrent_streams 128; # 동시 스트림 수 (기본값) http2_recv_buffer_size 256k;
# 헤더 크기 제한 (HPACK 테이블 크기와 연관) large_client_header_buffers 4 16k;
location / { proxy_pass http://backend; proxy_http_version 1.1; # 백엔드와는 HTTP/1.1 (기본) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; }
# gRPC 백엔드 (HTTP/2 end-to-end) location /grpc/ { grpc_pass grpc://grpc_backend; } }
upstream grpc_backend { server 10.0.1.10:50051; server 10.0.1.11:50051; keepalive 32; # HTTP/2 커넥션 재사용 }}10-3. HTTP/3 (QUIC) Nginx 설정
섹션 제목: “10-3. HTTP/3 (QUIC) Nginx 설정”server { # HTTP/3 (QUIC) 활성화 listen 443 quic reuseport; # UDP 443 listen 443 ssl; # TCP 443 (HTTP/2 fallback) http2 on;
ssl_certificate /etc/ssl/certs/example.com.crt; ssl_certificate_key /etc/ssl/private/example.com.key;
# HTTP/3 알림 헤더 (브라우저에게 HTTP/3 가용성 알림) add_header Alt-Svc 'h3=":443"; ma=86400';
# QUIC 설정 quic_retry on; # 0-RTT Replay Attack 방지 ssl_early_data on; # TLS 1.3 0-RTT 활성화
location / { proxy_pass http://backend; }}Alt-Svc 헤더 역할: 브라우저는 첫 번째 HTTP/2 응답에서 Alt-Svc 헤더를 받아 다음 방문부터 HTTP/3를 시도한다.
Alt-Svc: h3=":443"; ma=86400 ↑ ↑ HTTP/3 (h3) Max-Age: 86400초 동안 캐시 포트 443에서 사용 가능11. 프론트엔드 → 플랫폼 브릿지
섹션 제목: “11. 프론트엔드 → 플랫폼 브릿지”11-1. Chrome DevTools Protocol 컬럼 읽기
섹션 제목: “11-1. Chrome DevTools Protocol 컬럼 읽기”Network 탭의 Protocol 컬럼은 각 요청이 어떤 HTTP 버전으로 처리됐는지 보여준다.
Protocol 컬럼 값: http/1.1 → HTTP/1.1 (TCP, 텍스트 기반) h2 → HTTP/2 (TCP, 바이너리 프레임) h3 → HTTP/3 (QUIC, UDP) (없음) → HTTP (암호화 없는 HTTP/1.1)Waterfall 차트 해석:
HTTP/1.1의 Waterfall: style.css ━━━━━━━━━━━━━━━━ app.js ━━━━━━━━━━━━━━━━ ← 직렬 처리, 계단식 image.jpg ━━━━━━━━━━━━━━━━
HTTP/2의 Waterfall: style.css ━━━━━━━━ app.js ━━━━━━━━━━━━━━ ← 병렬 처리, 겹침 image.jpg ━━━━━━━━━━━━━━━━━━━━━━━━━━Protocol 컬럼이 h2인데 Waterfall이 계단식으로 보인다면:
- 서버 측 스트림 처리가 병목이거나
- JavaScript로 순차적 fetch를 하고 있거나
- HTTP/2 설정에 문제가 있는 것이다.
DevTools에서 Protocol 컬럼 활성화 방법:
1. Chrome DevTools → Network 탭 열기2. 컬럼 헤더 우클릭 → Protocol 체크3. 페이지 새로고침4. 리소스 클릭 → Headers 탭 → General 섹션에서 버전 확인11-2. curl로 HTTP 버전 확인
섹션 제목: “11-2. curl로 HTTP 버전 확인”# HTTP/2 지원 확인curl -I --http2 https://api.example.com/health# 응답 헤더: HTTP/2 200
# HTTP/3 지원 확인 (curl 7.66.0+)curl -I --http3 https://api.example.com/health# 응답 헤더: HTTP/3 200
# 상세 연결 정보 확인curl -v --http2 https://api.example.com/health 2>&1 | grep -E "HTTP/|ALPN|TLS"# 예상 출력:# * ALPN: offering h2# * ALPN: server accepted h2# < HTTP/2 200ALPN (Application-Layer Protocol Negotiation): TLS 핸드셰이크 중에 HTTP 버전을 협상하는 TLS 확장이다. 클라이언트가 h2, http/1.1을 제안하고 서버가 지원 가능한 버전을 선택한다. HTTP/2가 항상 HTTPS 위에서 동작하는 이유이기도 하다 (스펙상 HTTP/2는 평문 가능하지만, 모든 브라우저가 TLS를 요구한다).
11-3. 번들 최적화와 HTTP 버전의 관계
섹션 제목: “11-3. 번들 최적화와 HTTP 버전의 관계”프론트엔드 번들링 전략과 HTTP 버전은 상호 영향을 준다.
HTTP/1.1 시대의 최적화 패턴 (지금은 안티패턴):
// webpack.config.js (HTTP/1.1 시대)// 파일 수를 줄이기 위해 모든 것을 하나로 번들링module.exports = { output: { filename: "bundle.js", // 모든 코드를 하나의 거대한 파일로 },};HTTP/1.1에서는 파일 수가 곧 커넥션 수였기 때문에 파일을 합치는 것이 유리했다.
HTTP/2 시대의 최적화 패턴:
// webpack.config.js (HTTP/2 시대)module.exports = { output: { filename: "[name].[contenthash].js", }, optimization: { splitChunks: { chunks: "all", // 작은 청크로 나누는 것이 이제 유리 // HTTP/2 멀티플렉싱으로 병렬 전송되므로 파일 수가 문제 없음 minSize: 20000, // 20KB 이상이면 분리 maxSize: 244000, // 244KB 이하로 유지 (캐시 효율) }, },};HTTP/2에서는 멀티플렉싱 덕분에 파일을 여러 개로 분리해도 병렬로 받아온다. 오히려 캐시 효율이 높아진다. vendor.js와 app.js를 분리하면 앱 코드가 바뀌어도 vendor는 캐시에서 재사용 가능하다.
도메인 샤딩은 HTTP/2에서 역효과:
// HTTP/1.1 시대 (이제는 안티패턴)// static1.example.com, static2.example.com 등으로 분산// → HTTP/2에서는 도메인마다 별도 커넥션, HPACK 동적 테이블 공유 불가// → 오히려 성능 저하
// HTTP/2 시대 (올바른 접근)// 단일 도메인 사용, HTTP/2 멀티플렉싱에 맡기기const CDN_URL = "https://static.example.com";Core Web Vitals와의 연결:
LCP (Largest Contentful Paint) 개선: HTTP/2 → 히어로 이미지와 CSS를 병렬 로드 HTTP/3 → 초기 연결 지연(RTT) 감소로 첫 바이트 수신 빨라짐
INP (Interaction to Next Paint) 개선: HTTP/2 멀티플렉싱 → JS 청크를 병렬 로드해 파싱 시작 빨라짐
CLS (Cumulative Layout Shift): HTTP 버전과 직접 관계는 적지만, 폰트/이미지 로드 속도 개선으로 간접 효과12. 실무 적용 시나리오
섹션 제목: “12. 실무 적용 시나리오”12-1. HTTP/2 적용 체크리스트
섹션 제목: “12-1. HTTP/2 적용 체크리스트”# 1. 현재 서비스 HTTP 버전 확인curl -sI https://api.example.com | head -1# HTTP/2 200 ← 정상# HTTP/1.1 200 ← HTTP/2 미적용
# 2. ALPN 협상 확인openssl s_client -connect api.example.com:443 -alpn h2 2>&1 | grep ALPN# ALPN protocol: h2 ← HTTP/2 지원됨
# 3. Alt-Svc 헤더 확인 (HTTP/3 지원 여부)curl -sI https://api.example.com | grep alt-svc# alt-svc: h3=":443"; ma=8640012-2. gRPC 서버 헬스체크 구현
섹션 제목: “12-2. gRPC 서버 헬스체크 구현”// grpc-server.ts (Node.js)import * as grpc from "@grpc/grpc-js";
// gRPC 표준 헬스체크 프로토콜 구현// ALB GRPC 타겟 그룹이 /grpc.health.v1.Health/Check 경로를 호출함const healthCheck = { check: (call: any, callback: any) => { callback(null, { status: "SERVING" }); }, watch: (call: any) => { call.write({ status: "SERVING" }); },};
const server = new grpc.Server();// 헬스체크 서비스 등록server.addService(healthProto.grpc.health.v1.Health.service, healthCheck);server.bindAsync( "0.0.0.0:50051", grpc.ServerCredentials.createInsecure(), () => { server.start(); console.log("gRPC server running on :50051"); },);12-3. HTTP/2 성능 문제 디버깅
섹션 제목: “12-3. HTTP/2 성능 문제 디버깅”# Nginx HTTP/2 연결 상태 확인location /nginx_status { stub_status on; allow 127.0.0.1; deny all;}
curl http://localhost/nginx_status# Active connections: 42# server accepts handled requests# 1234 1234 5678# Reading: 0 Writing: 1 Waiting: 41
# HTTP/2 스트림 레벨 디버깅 (nghttp2 도구)# brew install nghttp2nghttp -v https://api.example.com/# [ 0.123] Connected# [ 0.234] send SETTINGS frame# [ 0.345] recv SETTINGS frame# [ 0.456] send HEADERS frame; END_STREAM# :method: GET# :path: /# :scheme: https# [ 0.567] recv HEADERS frame; END_STREAM# :status: 20012.5 트러블슈팅
섹션 제목: “12.5 트러블슈팅”🔧 gRPC 요청이 ALB에서 “502 Bad Gateway” 반환
섹션 제목: “🔧 gRPC 요청이 ALB에서 “502 Bad Gateway” 반환”증상:
grpc 요청 시: FAILED_PRECONDITION: HTTP status code 502; body: ...# 또는 curl로 확인:curl -v --http2 https://api.example.com/grpc.health.v1.Health/Check# < HTTP/2 502원인: ALB 타겟 그룹의 Protocol Version이 HTTP1로 설정된 경우, gRPC 요청(HTTP/2 기반)이 백엔드에 HTTP/1.1로 다운그레이드되어 전달된다. gRPC 서버는 HTTP/1.1을 이해하지 못해 502를 반환한다.
해결:
# 1. 현재 타겟 그룹 Protocol Version 확인aws elbv2 describe-target-groups \ --target-group-arns arn:aws:elasticloadbalancing:... \ --query 'TargetGroups[*].{Name:TargetGroupName,Proto:Protocol,ProtoVer:ProtocolVersion}'# 출력:# [{"Name": "grpc-service", "Proto": "HTTP", "ProtoVer": "HTTP1"}] ← 문제
# 2. Terraform으로 수정# resource "aws_lb_target_group" "grpc" {# protocol_version = "GRPC" # ← HTTP1 → GRPC로 변경# }
# 3. AWS CLI로 수정 (타겟 그룹 재생성 필요, 인플레이스 변경 불가)aws elbv2 create-target-group \ --name grpc-service-v2 \ --protocol HTTP \ --protocol-version GRPC \ --port 50051 \ --vpc-id vpc-xxxxx
# 4. 헬스체크 확인 (gRPC 상태 코드 0 = OK)aws elbv2 describe-target-health \ --target-group-arn arn:aws:elasticloadbalancing:...🔧 HTTP/2 활성화 후 Nginx에서 “ERR_HTTP2_PROTOCOL_ERROR”
섹션 제목: “🔧 HTTP/2 활성화 후 Nginx에서 “ERR_HTTP2_PROTOCOL_ERROR””증상:
Chrome 콘솔: net::ERR_HTTP2_PROTOCOL_ERROR# curl로 확인:curl -v --http2 https://api.example.com/large-response# stream 1 was reset: INTERNAL_ERROR주로 대용량 파일 다운로드나 SSE(Server-Sent Events) 스트리밍 엔드포인트에서 발생.
원인: Nginx의 proxy_buffer_size 또는 http2_chunk_size가 너무 작거나, 백엔드와의 Keep-Alive 커넥션이 끊어진 상태에서 HTTP/2 스트림이 중단된다. 또는 응답 헤더에 HTTP/1.1 전용 헤더(Transfer-Encoding: chunked)가 포함된 경우에도 발생.
해결:
# /etc/nginx/nginx.conf 수정http { # HTTP/2 청크 크기 조정 (기본 8k → 16k) http2_chunk_size 16k;
# 프록시 버퍼 설정 조정 proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k;
# SSE/스트리밍 엔드포인트: 버퍼링 비활성화 location /events { proxy_pass http://backend; proxy_buffering off; # ← 핵심: 청크 즉시 전달 proxy_cache off; proxy_set_header Connection ''; proxy_http_version 1.1; chunked_transfer_encoding off; }}🔧 HTTP/3 전환 후 일부 사용자만 연결 실패
섹션 제목: “🔧 HTTP/3 전환 후 일부 사용자만 연결 실패”증상:
# 일부 사용자 리포트:"사이트에 접속이 안 됩니다" (특히 기업 네트워크 사용자)# 서버 로그에는 정상 응답 기록되어 있음# curl로 재현:curl --http3 https://api.example.com/# QUIC: Connection refused (UDP 443 차단)원인: HTTP/3는 UDP 443 포트를 사용한다. 일부 기업 방화벽, VPN, 중간 네트워크 장비가 UDP 443을 차단하거나 속도를 제한(throttle)한다. QUIC 연결이 실패하면 브라우저는 HTTP/2(TCP 443)로 폴백해야 하는데, 폴백 지연이 사용자에게 “접속 안 됨”으로 느껴질 수 있다.
해결:
# HTTP/3 설정 시 항상 HTTP/2 fallback 유지server { listen 443 quic reuseport; # UDP: HTTP/3 listen 443 ssl; # TCP: HTTP/2 fallback http2 on;
# Alt-Svc 헤더로 HTTP/3 알림 (브라우저가 RACE 방식으로 시도) add_header Alt-Svc 'h3=":443"; ma=86400';
# QUIC 연결 실패 시 TCP 폴백 허용 (기본 동작) # quic_retry on; → 활성화 시 0-RTT 재전송 공격 방지}# UDP 443 통신 가능 여부 확인# (도구: nc 또는 quic-go 테스트 도구)nc -u -z api.example.com 443# 응답 없음 → UDP 443 차단됨
# 대안: HTTP/3 지원 확인 (응답 헤더로)curl -sI https://api.example.com | grep -i alt-svc# alt-svc: h3=":443"; ma=86400 ← HTTP/3 가용운영 조언: CDN(CloudFront, Cloudflare)을 앞단에 두면 HTTP/3 지원 및 UDP 차단 환경 대응이 자동화된다. 직접 QUIC을 운영하는 경우 모니터링 지표에 QUIC fallback rate를 추가한다.
🔧 HPACK 동적 테이블 크기 초과로 연결 끊김
섹션 제목: “🔧 HPACK 동적 테이블 크기 초과로 연결 끊김”증상:
Nginx 에러 로그: [error] ... client sent too large header while reading client request headers# 또는 클라이언트 측: h2 stream closed due to HEADER_TABLE_SIZE error원인: JWT 토큰, 쿠키, 커스텀 헤더가 누적되어 HPACK 동적 테이블 크기를 초과한다. 기본 HPACK 테이블 크기는 4096 바이트이며, 대용량 Authorization 헤더가 있으면 이를 초과할 수 있다.
해결:
# Nginx HPACK 헤더 버퍼 크기 조정large_client_header_buffers 8 32k; # 기본 4 8k → 증가
# 또는 HTTP/2 SETTINGS HEADER_TABLE_SIZE 조정 (Nginx 기본값)http2_max_header_size 16k;13. 핵심 요약
섹션 제목: “13. 핵심 요약”| 항목 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 전송 형식 | 텍스트 | 바이너리 프레임 | 바이너리 (QUIC) |
| 멀티플렉싱 | 없음 (6개 커넥션) | O (단일 커넥션) | O (단일 커넥션) |
| 앱 레벨 HOL | 있음 | 없음 | 없음 |
| TCP 레벨 HOL | 있음 | 있음 | 없음 (UDP) |
| 헤더 압축 | 없음 | HPACK | QPACK |
| 전송 계층 | TCP | TCP | UDP (QUIC) |
| 연결 수립 | 2-3 RTT | 2-3 RTT | 1 RTT / 0-RTT |
| IP 변경 내성 | 없음 | 없음 | O (Connection ID) |
| 암호화 | TLS 별도 | TLS 별도 | TLS 1.3 내장 |
핵심 교훈:
- HTTP/2는 “파일 수를 줄여야 한다”는 HTTP/1.1 시대의 패턴을 무너뜨렸다
- HTTP/3는 “TCP가 만능이 아니다”는 것을 보여줬다
- gRPC를 쓴다면 HTTP/2의 스트림과 멀티플렉싱을 이해하는 것이 필수다
- ALB → 백엔드 구간은 기본 HTTP/1.1이다. gRPC는 GRPC 타겟 그룹 타입을 써야 end-to-end HTTP/2가 된다
- DevTools의 Protocol 컬럼과 Waterfall은 성능 분석의 출발점이다
📚 추천 리소스
섹션 제목: “📚 추천 리소스”- 📖 Introduction to HTTP/2 — web.dev — 멀티플렉싱, HPACK, 서버 푸시를 시각 자료와 함께 설명. 이 문서의 4~6절 심화용 (입문)
- 📖 HTTP/2 and How it Works — Medium (Carson) — 바이너리 프레임 구조를 hex dump와 함께 설명. 스트림 ID와 프레임 타입 이해에 최적 (중급)
- 📖 What is HTTP/3? — Cloudflare — QUIC의 Connection Migration, 0-RTT, TLS 1.3 내장을 명확하게 정리 (입문)
- 📖 RFC 9113 — HTTP/2 공식 스펙 — 프레임 타입, 흐름 제어, HPACK 인덱스 테이블의 공식 정의. 구현 수준 이해용 (고급)
- 📖 HTTP/2 — The Secret Weapon of gRPC — DEV Community — gRPC가 HTTP/2의 스트림과 멀티플렉싱을 어떻게 활용하는지 상세 설명 (중급)
- 📖 RFC 9114 — HTTP/3 공식 스펙 — QUIC stream 독립성, Alt-Svc 광고, UDP 차단 시 TCP 기반 HTTP fallback 권고 (고급)
- 📖 RFC 9000 — QUIC 공식 스펙 — Connection ID, migration, QUIC transport 동작 원리 (고급)
- 📖 RFC 7541 — HPACK 공식 스펙 — HPACK 정적 테이블 61개, 동적 테이블, 보안 고려사항 (고급)
- 📖 RFC 9204 — QPACK 공식 스펙 — HTTP/3에서 HPACK 대신 QPACK이 필요한 이유와 blocking trade-off (고급)
- 📖 Remove HTTP/2 Server Push from Chrome — Chrome Developers — Chrome 106의 Server Push 비활성화, HTTP/2 사이트 중 사용률 1.25% 근거 (운영)
- 📖 Target groups for Application Load Balancers — AWS — ALB target group protocol version, gRPC 조합, HTTP/2 stream 한도 (운영)
14. 더 깊이 파고들기
섹션 제목: “14. 더 깊이 파고들기”- L2 연결: [TCP/UDP Internals] — QUIC이 왜 UDP를 선택했는지, TCP의 재전송 타이머와 QUIC의 손실 감지 비교
- L7 연결: [TLS/HTTPS] — ALPN 협상 상세, TLS 1.3 0-RTT와 QUIC의 통합 암호화
- L8 연결: [gRPC & Protobuf] — HTTP/2 스트림을 gRPC가 어떻게 활용하는지, 채널 관리, 백프레셔
실습 과제 (예상 출력 포함):
# 1. HTTP 버전 확인curl -sI https://cloudflare.com | head -1예상 출력:
HTTP/2 200# 2. ALPN 협상 확인 (HTTP/2 지원 여부)openssl s_client -connect cloudflare.com:443 -alpn h2 2>/dev/null | grep ALPN예상 출력:
ALPN protocol: h2# 3. HTTP/3 지원 확인 (Alt-Svc 헤더)curl -sI https://cloudflare.com | grep -i alt-svc예상 출력:
alt-svc: h3=":443"; ma=86400, h3-29=":443"; ma=86400# 4. nghttp2로 HTTP/2 프레임 레벨 확인 (brew install nghttp2)nghttp -v https://nghttp2.org/ 2>&1 | grep -E "send|recv|HEADERS|DATA"예상 출력:
[ 0.085] send SETTINGS frame <length=12, flags=0x00, stream_id=0>[ 0.132] recv SETTINGS frame <length=18, flags=0x00, stream_id=0>[ 0.132] send HEADERS frame <length=38, flags=0x25, stream_id=1> :method: GET :path: / :scheme: https[ 0.198] recv HEADERS frame <length=97, flags=0x04, stream_id=1> :status: 200 content-type: text/html# 5. ALB 타겟 그룹 Protocol Version 확인aws elbv2 describe-target-groups \ --query 'TargetGroups[*].{Name:TargetGroupName,Version:ProtocolVersion}' \ --output table --region ap-northeast-2예상 출력:
-------------------------------------------| DescribeTargetGroups |+-----------------------+-----------------+| Name | Version |+-----------------------+-----------------+| api-service | HTTP1 || grpc-service | GRPC |+-----------------------+-----------------+