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 버전만 바꿔 20-50% 성능 개선이 가능하다. 이를 이해하지 못하면 “Nginx에서 http2 켜기”가 왜 효과가 있는지 설명할 수 없다.
- gRPC의 기반: 마이크로서비스 간 통신에서 gRPC가 HTTP/2 위에서 동작하는 이유, 그리고 그것이 가져다주는 성능 이점을 이해해야 한다.
- 인프라 설계 직결: ALB의
HTTP/2설정, CloudFront의 프로토콜 정책, Nginx 설정이 실제로 어떤 의미인지 판단할 수 있어야 한다. - 디버깅 능력: Chrome DevTools의 Network 탭에서 Protocol 컬럼이
h2인지http/1.1인지, Waterfall 차트 모양이 어떻게 달라지는지 읽을 수 있어야 한다.
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)가 이를 올바르게 구현했는지 확인하는 것이 중요하다.
주의: 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";}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 커넥션보다 오히려 나쁠 수 있다. 패킷 손실률 2%에서 HTTP/2가 HTTP/1.1보다 느린 테스트 결과도 있다.
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 기반이 안정적)
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을 사용한다. 백엔드까지 HTTP/2를 쓰려면 gRPC 타겟 그룹 타입을 사용해야 한다.
# 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의 스트림과 멀티플렉싱을 어떻게 활용하는지 상세 설명 (중급)
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 |+-----------------------+-----------------+