HTTP 프로토콜 완벽 가이드 | HTTP/1.1·HTTP/2·HTTP/3·QUIC 비교
이 글의 핵심
HTTP/1.1, HTTP/2, HTTP/3, QUIC의 동작 원리와 진화 과정. 멀티플렉싱, 헤더 압축, 0-RTT, 패킷 손실 복구까지. 실전 성능 비교와 최적화 전략 완벽 마스터.
들어가며: 웹의 진화
”웹 페이지가 왜 이렇게 느릴까?”
웹 페이지를 열 때 로딩 시간이 길거나, 모바일에서 끊김이 발생하거나, 이미지가 순차적으로 나타나는 경험을 해보셨나요? 이는 HTTP 프로토콜의 한계 때문입니다. HTTP/1.1은 1997년에 설계되어 현대 웹의 복잡성을 따라가지 못했고, HTTP/2는 멀티플렉싱으로 개선했지만 TCP의 HOL Blocking 문제가 남아있었습니다. HTTP/3와 QUIC은 UDP 기반으로 이 모든 문제를 해결했습니다. HTTP 프로토콜의 진화:
- HTTP/0.9 (1991): 단순 GET 요청
- HTTP/1.0 (1996): 헤더, 메서드 추가
- HTTP/1.1 (1997): Keep-Alive, 파이프라이닝
- HTTP/2 (2015): 멀티플렉싱, 헤더 압축
- HTTP/3 (2022): QUIC, UDP 기반
HTTP의 탄생과 웹 아키텍처의 진화
0.1 Tim Berners-Lee와 HTTP/0.9 (1991)
1989년, CERN의 물리학자 Tim Berners-Lee는 단순한 문제를 해결하려 했습니다:
“연구자들이 전 세계에 흩어져 있는 문서를 쉽게 공유할 수 없을까?”
그의 해답은 세 가지 발명이었습니다:
- HTML — 하이퍼텍스트 문서 형식
- URL — 자원의 주소 체계
- HTTP — 문서 전송 프로토콜
HTTP/0.9 (1991)의 놀라운 단순성:
GET /index.html
이게 전부였습니다. 응답도 HTML 본문뿐, 헤더도 없었습니다:
<html>
<body>Hello World</body>
</html>
왜 이렇게 단순했나?
- 목표: 물리학 논문 공유
- 네트워크: 초당 수 KB (느린 모뎀)
- 문서: 텍스트만 (이미지조차 없음)
0.2 HTTP/1.0과 상업 웹의 시작 (1996)
1993년 Mosaic 브라우저가 등장하며 웹이 대중화되었고, 1996년 HTTP/1.0이 공식 표준이 되었습니다.
추가된 기능:
GET /image.gif HTTP/1.0
Host: www.example.com
User-Agent: Mozilla/2.0
Accept: image/gif, image/jpeg
HTTP/1.0 200 OK
Content-Type: image/gif
Content-Length: 5432
<binary data>
핵심 변화:
- 헤더 추가: Content-Type, Content-Length, User-Agent
- 메서드 확장: POST, HEAD 추가
- 상태 코드: 200, 404, 500 등
하지만 치명적 결함:
문제: 연결당 하나의 요청
페이지 로드 시:
1. index.html 요청 → TCP 연결 생성 → 전송 → 연결 종료
2. style.css 요청 → TCP 연결 생성 → 전송 → 연결 종료
3. image.jpg 요청 → TCP 연결 생성 → 전송 → 연결 종료
10개 리소스 = 10번 TCP 핸드셰이크 = 엄청난 지연!
TCP 3-Way Handshake의 숨겨진 비용:
SYN → SYN-ACK → ACK = 1.5 RTT
서울-미국 서버 RTT: 150ms
→ 연결 설정만 225ms
→ 10개 리소스 = 2.25초!
0.3 HTTP/1.1과 Keep-Alive 혁명 (1997)
HTTP/1.1은 Roy Fielding의 박사 논문 REST 아키텍처에서 영감을 받아 설계되었습니다.
핵심 개선:
GET /index.html HTTP/1.1
Host: www.example.com
Connection: keep-alive
HTTP/1.1 200 OK
Content-Length: 1234
Connection: keep-alive
<html>...</html>
GET /style.css HTTP/1.1
Host: www.example.com
HTTP/1.1 200 OK
Content-Length: 5678
body { ... }
Keep-Alive의 위력:
HTTP/1.0 (연결당 1개 요청):
TCP 연결 → 요청 1 → 종료
TCP 연결 → 요청 2 → 종료
...
총 시간: 10 * (RTT + 전송) = 1.5s + 전송
HTTP/1.1 (연결 재사용):
TCP 연결 → 요청 1 → 요청 2 → ... → 요청 10 → 종료
총 시간: RTT + 전송 = 0.15s + 전송
→ 10배 빠름!
하지만 새로운 문제: Head-of-Line (HOL) Blocking
파이프라이닝 문제:
┌────────┬────────┬────────┐
│ 요청 1 │ 요청 2 │ 요청 3 │ → 순차 전송
└────────┴────────┴────────┘
↓ ↓ ↓
┌────────┬────────┬────────┐
│ 응답 1 │ 응답 2 │ 응답 3 │ ← 순서대로만 받을 수 있음
└────────┴────────┴────────┘
만약 응답 1이 느리면?
→ 응답 2, 3도 대기 (HOL Blocking)
0.4 HTTP/2의 혁명: Google SPDY의 표준화 (2015)
2009년, Google은 “웹이 너무 느리다”며 SPDY (SPeeDY) 프로토콜을 개발했습니다.
핵심 아이디어: 멀티플렉싱
HTTP/1.1 (6개 병렬 연결):
┌─────┐ ┌─────┐ ┌─────┐
│TCP 1│ │TCP 2│ │TCP 6│
├─────┤ ├─────┤ ├─────┤
│요청 1│ │요청 2│ │요청 6│
└─────┘ └─────┘ └─────┘
HTTP/2 (1개 연결):
┌─────────────────────────┐
│ TCP 1개 │
├──────┬──────┬──────────┤
│Stream│Stream│ Stream │
│ 1 │ 2 │ N │
├──────┼──────┼──────────┤
│요청 1│요청 2│ 요청 N │
└──────┴──────┴──────────┘
SPDY → HTTP/2 변환 과정:
- 2012: Chrome에서 SPDY 활성화
- 2013: IETF가 SPDY를 HTTP/2 드래프트로 채택
- 2015: HTTP/2 RFC 7540 발표
- 2016: 주요 브라우저 모두 지원
HTTP/2 바이너리 프레이밍:
HTTP/1.1 (텍스트):
GET / HTTP/1.1\r\n
Host: example.com\r\n
\r\n
HTTP/2 (바이너리 프레임):
┌─────────────────────────────┐
│ Frame Header (9 bytes) │
├─────────────────────────────┤
│ Length: 0x000123 │
│ Type: HEADERS (0x01) │
│ Flags: END_HEADERS │
│ Stream ID: 0x00000001 │
├─────────────────────────────┤
│ Frame Payload │
│ (HPACK 압축 헤더) │
└─────────────────────────────┘
HPACK 헤더 압축의 혁신:
HTTP/1.1의 문제:
GET /api/users/1 HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0...
Cookie: session=abc123; token=xyz789; ...
Accept: application/json
GET /api/users/2 HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0...
Cookie: session=abc123; token=xyz789; ...
Accept: application/json
→ 헤더가 매번 반복 (수백 바이트 낭비)
HPACK 해결:
정적 테이블 (61개 자주 쓰는 헤더):
Index 1: :authority
Index 2: :method GET
Index 3: :method POST
...
동적 테이블 (연결마다 유지):
Index 62: Cookie: session=abc123
Index 63: User-Agent: Mozilla/5.0...
전송:
요청 1: [LITERAL_HEADER] Host: api.example.com
[LITERAL_HEADER] Cookie: session=abc123
→ 동적 테이블에 추가
요청 2: [INDEXED 62] [INDEXED 63]
→ 인덱스만 전송 (10배 압축)
0.5 HTTP/3와 QUIC: UDP의 역습 (2022)
2012년, Google 엔지니어들은 근본적 질문을 했습니다:
“HTTP/2도 TCP HOL Blocking은 못 피한다. TCP 자체를 바꿀 수 없을까?”
TCP의 구조적 한계:
TCP는 "바이트 스트림" 추상화:
[데이터 1][데이터 2][데이터 3] → 순서 보장
패킷 손실:
[데이터 1][손실!][데이터 3]
↓
재전송 대기
↓
[데이터 1][데이터 2][데이터 3]
↑ ↑
받았음 받았는데도 대기! (HOL Blocking)
HTTP/2의 스트림 1, 2, 3이 모두 같은 TCP 연결이면:
→ 스트림 1 패킷 손실 시 스트림 2, 3도 블록!
QUIC의 해결책: UDP 위에 스트림 단위 흐름 제어
QUIC:
[Stream 1][Stream 2][Stream 3] → 독립적 재전송
패킷 손실:
[Stream 1 OK][Stream 2 손실][Stream 3 OK]
↓
Stream 2만 재전송
↓
Stream 1, 3은 계속 처리! (No HOL Blocking)
QUIC의 추가 혁신:
1) 0-RTT 연결 재개:
기존 (TCP + TLS):
┌─────────────────────────────────┐
│ TCP Handshake: 1 RTT │
│ TLS Handshake: 1-2 RTT │
│ HTTP 요청: 1 RTT │
├─────────────────────────────────┤
│ 총: 3-4 RTT = 450-600ms (150ms RTT)│
└─────────────────────────────────┘
QUIC (첫 연결):
┌─────────────────────────────────┐
│ QUIC Handshake: 1 RTT (crypto + conn)│
│ HTTP 요청: 0 RTT (병합) │
├─────────────────────────────────┤
│ 총: 1 RTT = 150ms │
└─────────────────────────────────┘
QUIC (재연결, 0-RTT):
┌─────────────────────────────────┐
│ HTTP 요청: 0 RTT (캐시된 키) │
├─────────────────────────────────┤
│ 총: 0 RTT = 0ms! │
└─────────────────────────────────┘
2) 연결 마이그레이션:
시나리오: WiFi → 모바일 데이터 전환
TCP:
WiFi 연결 (IP: 192.168.1.100)
↓ 네트워크 전환
모바일 (IP: 10.20.30.40)
→ IP 변경 = 연결 끊김
→ 재연결 필요 (3-4 RTT)
QUIC:
WiFi 연결 (Connection ID: abc123)
↓ 네트워크 전환
모바일 (IP: 10.20.30.40, Connection ID: abc123)
→ Connection ID로 식별
→ 연결 유지! (0 RTT)
YouTube 모바일 앱의 실측:
- HTTP/2: 네트워크 전환 시 1-3초 버퍼링
- HTTP/3: 네트워크 전환 시 끊김 없음
0.6 프로토콜 설계의 트레이드오프
왜 HTTP/2가 바이너리로 바뀌었나?
초기 HTTP/1.1 설계자들의 철학:
“텍스트 프로토콜은 디버깅이 쉽다. telnet으로 직접 입력할 수 있다.”
2015년 HTTP/2 설계자들의 반박:
“현대 웹은 초당 수백 개 요청을 보낸다. 파싱 오버헤드가 병목이다.”
실제 측정:
HTTP/1.1 파싱 (텍스트):
- 헤더 파싱: 문자열 분리, 대소문자 무시 비교
- 시간: ~10-50 마이크로초 (헤더 개수에 비례)
HTTP/2 파싱 (바이너리):
- 프레임 파싱: 9바이트 헤더 읽기, 타입/플래그 체크
- 시간: ~1-5 마이크로초 (고정)
→ 10배 빠름, CPU 사용량 감소
왜 HTTP/3가 UDP를 선택했나?
TCP의 커널 구현 문제:
TCP 스택 위치:
┌─────────────────┐
│ 애플리케이션 │
├─────────────────┤
│ 유저 공간 │
└─────────────────┘
─────────────────────
┌─────────────────┐
│ 커널 공간 │
│ TCP 스택 │ ← 여기서 처리!
└─────────────────┘
문제:
1. TCP 개선 = 커널 패치 필요
2. OS 업데이트 주기: 년 단위
3. 실험 불가능 (시스템 전체 영향)
UDP + QUIC 해결책:
QUIC 스택 위치:
┌─────────────────┐
│ 애플리케이션 │
│ QUIC 라이브러리 │ ← 유저 공간!
├─────────────────┤
│ 유저 공간 │
└─────────────────┘
─────────────────────
┌─────────────────┐
│ 커널 공간 │
│ UDP (단순 전달) │
└─────────────────┘
장점:
1. 브라우저 업데이트만으로 개선
2. A/B 테스트 가능
3. 빠른 프로토콜 진화
결과: QUIC은 2년마다 새 RFC 발표. TCP는 20년째 큰 변화 없음.
실전 경험에서 배운 교훈
글로벌 CDN을 운영하며 HTTP/2 → HTTP/3 전환을 주도했던 경험에서 가장 인상 깊었던 것은 “이론과 현실의 괴리”였습니다.
HTTP/2 도입 시 예상치 못한 문제:
- 우선순위 무시: 브라우저가 보낸 우선순위를 nginx가 제대로 처리하지 못해, 중요한 CSS보다 이미지가 먼저 전송되는 경우 발생
- 서버 푸시 실패: 캐시 상태를 모르고 푸시해서 오히려 대역폭 낭비. 결국 대부분의 사이트가 서버 푸시를 비활성화
- 중간 프록시 문제: 오래된 로드 밸런서가 HTTP/2를 지원하지 않아 다운그레이드
HTTP/3 도입의 현실적 제약:
- UDP 방화벽: 기업 방화벽이 UDP 443을 막는 경우 다수 (약 30%)
- CPU 오버헤드: 암호화를 유저 공간에서 처리하므로 TLS 1.3보다 CPU 10-15% 더 사용
- 디버깅 어려움: Wireshark도 QUIC 암호화 해독이 어려움
이 글에서는 그런 프로토콜 이론과 실제 배포 현실의 차이를 구체적으로 정리했습니다. 특히 트러블슈팅 섹션은 실제 프로덕션에서 HTTP/2, HTTP/3 운영하며 겪은 장애 대응 경험을 바탕으로 작성했으니, 비슷한 문제를 마주했을 때 참고하시면 도움이 될 것입니다.
1. HTTP/1.1
HTTP/1.1 기본 구조
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: TCP 3-Way Handshake
C->>S: SYN
S-->>C: SYN-ACK
C->>S: ACK
Note over C,S: HTTP 요청/응답
C->>S: GET /index.html HTTP/1.1\nHost: example.com
S-->>C: HTTP/1.1 200 OK\nContent-Type: text/html\n\n...
C->>S: GET /style.css HTTP/1.1
S-->>C: HTTP/1.1 200 OK\nContent-Type: text/css\n\nbody {...}
C->>S: GET /script.js HTTP/1.1
S-->>C: HTTP/1.1 200 OK\nContent-Type: text/javascript\n\nfunction...
Note over C,S: 연결 종료 (또는 Keep-Alive)
HTTP/1.1 요청 메시지 - 구조와 의미
HTTP 요청은 텍스트 기반 프로토콜로, 사람이 읽을 수 있는 형식입니다.
GET /api/users/123 HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: application/json
Accept-Language: ko-KR,ko;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session_id=abc123; user_pref=dark
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
요청 라인 (Request Line) 분석:
GET /api/users/123 HTTP/1.1
│ │ │
│ │ └─ 프로토콜 버전
│ └─ 요청 URI (Uniform Resource Identifier)
└─ HTTP 메서드
필수 헤더와 선택 헤더:
Host: api.example.com
- 필수 헤더 (HTTP/1.1에서 유일한 필수 헤더)
- 가상 호스팅을 위해 필수: 하나의 IP에 여러 도메인이 호스팅될 수 있음
- 예:
example.com,api.example.com,www.example.com모두 같은 서버 IP
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
- 클라이언트 식별: 브라우저 종류, 버전, OS 정보
- 서버가 클라이언트에 따라 다른 응답 제공 가능
- 모바일: 간소화된 HTML
- 구형 브라우저: 폴리필 포함
- 보안 주의: User-Agent 스푸핑 가능 → 신뢰하지 말 것
Accept: application/json
- Content Negotiation: 클라이언트가 받고 싶은 데이터 형식
- 여러 타입 지원 가능:
Accept: application/json, text/html;q=0.9, */*;q=0.8q=0.9: 품질 가중치 (0.0 ~ 1.0, 기본값 1.0)- 서버는 가장 높은
q값의 타입으로 응답
Accept-Language: ko-KR,ko;q=0.9,en;q=0.8
- 다국어 지원: 선호 언어 순서
ko-KR: 한국어(한국) - 1.0 (기본)ko: 한국어 일반 - 0.9en: 영어 - 0.8
- 서버는 이를 참고해 번역 제공
Accept-Encoding: gzip, deflate, br
- 압축 지원: 클라이언트가 처리할 수 있는 압축 알고리즘
gzip: 범용 압축 (70-90% 압축률)deflate: gzip의 기반 알고리즘br(Brotli): Google 개발, gzip보다 15-20% 효율적
- 서버가 압축해서 보내면 대역폭 절약 (특히 텍스트)
Connection: keep-alive
- 연결 유지: TCP 연결을 재사용
- HTTP/1.0 기본값:
close(매번 새 연결) - HTTP/1.1 기본값:
keep-alive(연결 재사용) - Keep-Alive 장점:
- TCP 3-way handshake 생략 (RTT 절약)
- TLS 핸드셰이크 생략 (2-RTT 절약)
- Slow Start 회피 (TCP 혼잡 제어)
Cookie: session_id=abc123; user_pref=dark
- 상태 유지: HTTP는 무상태(Stateless) 프로토콜이지만 쿠키로 상태 저장
- 형식:
name1=value1; name2=value2 - 보안 속성 (서버가 Set-Cookie로 설정):
HttpOnly: JavaScript 접근 불가 (XSS 방어)Secure: HTTPS에서만 전송SameSite: CSRF 방어
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
- 인증 토큰: 사용자 신원 확인
- 인증 방식:
Basic: Base64 인코딩 (안전하지 않음)Bearer: JWT 토큰 (현대적)Digest: 해시 기반 (레거시)
- JWT (JSON Web Token) 구조:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header (알고리즘) .eyJ1c2VySWQiOiIxMjMifQ ← Payload (데이터) .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature (검증)
HTTP 헤더의 크기 문제:
일반적인 요청 헤더: 약 500-2000 bytes
- User-Agent: ~150 bytes
- Cookie: ~1000 bytes (세션, 추적 등)
- Authorization: ~200-500 bytes (JWT)
문제:
- 매 요청마다 중복 전송
- 10개 리소스 로드 → 5-20KB 헤더 오버헤드
→ HTTP/2의 HPACK 압축으로 해결
HTTP/1.1 응답 메시지 - 캐싱과 최적화의 핵심
HTTP/1.1 200 OK
Date: Wed, 01 Apr 2026 10:00:00 GMT
Server: nginx/1.24.0
Content-Type: application/json; charset=utf-8
Content-Length: 1234
Connection: keep-alive
Cache-Control: max-age=3600
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 01 Apr 2026 09:00:00 GMT
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
{
"id": 123,
"name": "John Doe",
"email": "[email protected]"
}
상태 라인 (Status Line) 분석:
HTTP/1.1 200 OK
│ │ │
│ │ └─ 상태 메시지 (Reason Phrase)
│ └─ 상태 코드 (Status Code)
└─ 프로토콜 버전
응답 헤더 상세 분석:
Date: Wed, 01 Apr 2026 10:00:00 GMT
- 서버 시간: 응답 생성 시각 (GMT/UTC 기준)
- 용도:
- 캐시 유효성 계산 (
Cache-Control: max-age와 함께) - 클라이언트 시간 동기화 참고
- 로그 분석 및 디버깅
- 캐시 유효성 계산 (
Server: nginx/1.24.0
- 서버 소프트웨어: 웹 서버 종류와 버전
- 보안 주의: 버전 정보 노출 위험
- 알려진 취약점 공격 가능
- 프로덕션에서는 숨기는 것이 Best Practice
# nginx.conf server_tokens off; # "nginx" only
Content-Type: application/json; charset=utf-8
- MIME 타입: 응답 본문의 데이터 형식
- 주요 MIME 타입:
text/html: HTML 문서application/json: JSON 데이터application/javascript: JavaScript 파일text/css: CSS 스타일시트image/png,image/jpeg: 이미지
charset=utf-8: 문자 인코딩 (필수 명시!)- 생략 시 브라우저가 추측 → 잘못된 렌더링 가능
Content-Length: 1234
- 본문 크기: 바이트 단위
- 용도:
- 클라이언트가 전체 데이터를 받았는지 확인
- 진행률 표시 (파일 다운로드)
- TCP 연결 재사용 가능 여부 판단
Transfer-Encoding: chunked와 함께 사용 불가- Chunked: 크기를 미리 알 수 없을 때 (스트리밍)
Cache-Control: max-age=3600
- 캐시 제어: 브라우저/CDN 캐싱 정책
- 주요 지시자:
Cache-Control: public, max-age=31536000, immutable # public: 중간 캐시(CDN) 허용 # max-age=31536000: 1년간 캐시 (초 단위) # immutable: 절대 변경 안 됨 (재검증 불필요) Cache-Control: private, no-cache # private: 브라우저만 캐시 (CDN 불가) # no-cache: 매번 서버에 재검증 (304 Not Modified 가능) Cache-Control: no-store # no-store: 절대 캐시 안 함 (민감한 데이터) - 성능 영향:
캐시 없음: 매 요청 서버 왕복 (수백ms) 캐시 적중: 로컬에서 즉시 로드 (0ms) → 페이지 로딩 속도 10-100배 차이!
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
- 리소스 버전 식별자: 파일 내용의 해시 또는 버전 번호
- 조건부 요청에 사용:
# 1. 첫 요청 GET /api/users/123 HTTP/1.1 # 2. 서버 응답 HTTP/1.1 200 OK ETag: "33a64df5" {...} # 3. 재요청 (ETag 포함) GET /api/users/123 HTTP/1.1 If-None-Match: "33a64df5" # 4. 변경 없으면 304 (본문 없음) HTTP/1.1 304 Not Modified ETag: "33a64df5" # 변경 있으면 200 (새 데이터) HTTP/1.1 200 OK ETag: "abc12345" {...새 데이터...} - 대역폭 절약:
- 304 응답: 헤더만 (~500 bytes)
- 200 응답: 헤더 + 본문 (~10KB)
- 절약률: 95%+
Last-Modified: Wed, 01 Apr 2026 09:00:00 GMT
- 마지막 수정 시각: ETag의 시간 기반 대안
- 조건부 요청:
GET /style.css HTTP/1.1 If-Modified-Since: Wed, 01 Apr 2026 09:00:00 GMT # 변경 없으면 HTTP/1.1 304 Not Modified # 변경 있으면 HTTP/1.1 200 OK Last-Modified: Wed, 01 Apr 2026 10:00:00 GMT - ETag vs Last-Modified:
- ETag: 정확 (내용 기반), 계산 비용 높음
- Last-Modified: 빠름, 1초 단위 정밀도
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
- 커스텀 헤더 (
X-접두사는 레거시 관례) - Rate Limiting: API 사용량 제한
- 표준화된 헤더:
X-RateLimit-Limit: 1000 # 시간당 최대 요청 수 X-RateLimit-Remaining: 999 # 남은 요청 수 X-RateLimit-Reset: 1680345600 # 리셋 시각 (UNIX timestamp) Retry-After: 3600 # 재시도 대기 시간 (초) - 초과 시 응답:
HTTP/1.1 429 Too Many Requests Retry-After: 3600 X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1680345600
응답 최적화 체크리스트:
✅ Gzip/Brotli 압축 활성화
Content-Encoding: br
✅ 적절한 Cache-Control 설정
정적 파일: max-age=31536000, immutable
API: no-cache 또는 짧은 max-age
✅ ETag 또는 Last-Modified 제공
304 Not Modified 활용
✅ Content-Length 명시
TCP 연결 재사용
✅ CORS 헤더 설정 (필요시)
Access-Control-Allow-Origin
HTTP/1.1 메서드
# GET (조회)
GET /api/users HTTP/1.1
# POST (생성)
POST /api/users HTTP/1.1
Content-Type: application/json
{
"name": "John Doe",
"email": "[email protected]"
}
# PUT (전체 수정)
PUT /api/users/123 HTTP/1.1
Content-Type: application/json
{
"name": "Jane Doe",
"email": "[email protected]"
}
# PATCH (부분 수정)
PATCH /api/users/123 HTTP/1.1
Content-Type: application/json
{
"email": "[email protected]"
}
# DELETE (삭제)
DELETE /api/users/123 HTTP/1.1
# HEAD (헤더만 조회)
HEAD /api/users/123 HTTP/1.1
# OPTIONS (지원 메서드 확인)
OPTIONS /api/users HTTP/1.1
HTTP/1.1 상태 코드 - 실전 사용 가이드
1xx: 정보 (Informational)
서버가 요청을 받았으며 처리 중임을 알립니다.
100 Continue
- 대용량 업로드 최적화: 클라이언트가 본문을 보내기 전에 서버의 허가를 받음
- 사용 예:
# 1. 클라이언트가 헤더만 먼저 전송 PUT /upload HTTP/1.1 Content-Length: 1073741824 # 1GB 파일 Expect: 100-continue # 2. 서버가 허가 HTTP/1.1 100 Continue # 3. 클라이언트가 본문 전송 (1GB) [...binary data...] # 4. 서버 응답 HTTP/1.1 200 OK - 장점: 서버가 거부할 경우(인증 실패 등) 대용량 데이터 전송 방지
101 Switching Protocols
- 프로토콜 업그레이드: HTTP → WebSocket 전환 시 사용
# 클라이언트 요청 GET /chat HTTP/1.1 Upgrade: websocket Connection: Upgrade # 서버 응답 HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade
2xx: 성공 (Success)
요청이 성공적으로 처리되었습니다.
200 OK
- 가장 일반적인 성공 응답
- GET, POST, PUT, PATCH 모두 사용 가능
- 응답 본문에 요청된 리소스 포함
201 Created
- 리소스 생성 성공 (주로 POST)
- Location 헤더로 생성된 리소스 URL 제공:
POST /api/users HTTP/1.1 {"name": "John"} HTTP/1.1 201 Created Location: /api/users/123 {"id": 123, "name": "John"}
204 No Content
- 성공했지만 반환할 내용 없음
- 사용 예:
- DELETE 성공
- PUT 업데이트 후 응답 불필요
- Webhook 수신 확인
- 응답 본문 없음 (Content-Length: 0)
206 Partial Content
- 범위 요청 (Range Request): 파일의 일부만 전송
- 대용량 파일 다운로드 재개:
# 클라이언트 요청 (1MB-2MB 구간) GET /video.mp4 HTTP/1.1 Range: bytes=1048576-2097151 # 서버 응답 HTTP/1.1 206 Partial Content Content-Range: bytes 1048576-2097151/104857600 Content-Length: 1048576 [...1MB 데이터...] - 스트리밍 비디오에서 필수
3xx: 리다이렉션 (Redirection)
클라이언트가 추가 동작을 취해야 합니다.
301 Moved Permanently
- 영구 이동: 리소스가 완전히 다른 URL로 이동
- SEO 중요: 검색 엔진이 새 URL을 인덱싱
- 사용 예:
# http → https 리다이렉트 HTTP/1.1 301 Moved Permanently Location: https://example.com/page # www 제거 HTTP/1.1 301 Moved Permanently Location: https://example.com/ - 브라우저 캐싱: 301은 영구 캐시됨 (주의!)
302 Found (Temporary Redirect)
- 임시 이동: 일시적으로 다른 URL 사용
- 로그인 리다이렉트:
GET /dashboard HTTP/1.1 HTTP/1.1 302 Found Location: /login?redirect=/dashboard - 주의: POST 요청 시 GET으로 변경될 수 있음
304 Not Modified
- 캐시 유효: 리소스가 변경되지 않음
- 조건부 요청과 함께 사용:
GET /api/data HTTP/1.1 If-None-Match: "abc123" HTTP/1.1 304 Not Modified ETag: "abc123" Cache-Control: max-age=3600 - 응답 본문 없음 (대역폭 절약)
307 Temporary Redirect
308 Permanent Redirect
- 메서드 유지: POST 요청 시 POST로 리다이렉트
- 307 vs 302:
- 302: POST → GET 변경 가능
- 307: POST → POST 유지
4xx: 클라이언트 오류 (Client Error)
클라이언트의 요청이 잘못되었습니다.
400 Bad Request
- 잘못된 요청 구문: 파싱 불가
- 사용 예:
- JSON 문법 오류
- 필수 필드 누락
- 잘못된 데이터 타입
{ "error": "Validation Failed", "details": [ {"field": "email", "message": "Invalid email format"} ] }
401 Unauthorized (실제로는 "Unauthenticated")
- 인증 필요: 로그인하지 않음
- WWW-Authenticate 헤더 필수:
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="api" {"error": "Authentication required"}
403 Forbidden
- 권한 없음: 인증은 되었지만 접근 권한 없음
- 401 vs 403:
- 401: “누구세요?” → 로그인 필요
- 403: “당신은 접근 불가” → 권한 부족
HTTP/1.1 403 Forbidden {"error": "Insufficient permissions"}
404 Not Found
- 리소스 없음: URL이 존재하지 않음
- 주의: 존재하지만 권한 없을 때 403 대신 404 반환 가능 (보안)
405 Method Not Allowed
- 메서드 불허: 리소스는 있지만 해당 메서드 지원 안 함
- Allow 헤더로 지원 메서드 알림:
DELETE /api/users/123 HTTP/1.1 HTTP/1.1 405 Method Not Allowed Allow: GET, PUT
408 Request Timeout
- 요청 시간 초과: 클라이언트가 요청을 완료하지 않음
- 서버 타임아웃 설정:
# nginx client_body_timeout 60s;
429 Too Many Requests
- Rate Limit 초과: 너무 많은 요청
- Retry-After 헤더 포함 권장:
HTTP/1.1 429 Too Many Requests Retry-After: 3600 X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 0
5xx: 서버 오류 (Server Error)
서버가 요청을 처리하지 못했습니다.
500 Internal Server Error
- 일반 서버 오류: 예외 발생
- 디버깅 정보 노출 주의:
# ❌ 나쁜 예 (스택 트레이스 노출) HTTP/1.1 500 Internal Server Error {"error": "NullPointerException at line 42..."} # ✅ 좋은 예 (일반 메시지) HTTP/1.1 500 Internal Server Error {"error": "Internal server error", "request_id": "abc123"}
502 Bad Gateway
- 게이트웨이 오류: 업스트림 서버 응답 불량
- 사용 예:
- Nginx → 백엔드 서버 연결 실패
- Load Balancer → 모든 서버 다운
503 Service Unavailable
- 서비스 이용 불가: 일시적 과부하 또는 유지보수
- Retry-After 헤더 사용:
HTTP/1.1 503 Service Unavailable Retry-After: 120 {"error": "Service temporarily unavailable"}
504 Gateway Timeout
- 게이트웨이 시간 초과: 업스트림 서버 응답 지연
- 408 vs 504:
- 408: 클라이언트가 느림
- 504: 백엔드 서버가 느림
상태 코드 선택 가이드:
리소스 생성 성공 → 201 Created (Location 헤더)
업데이트 성공 (반환 불필요) → 204 No Content
업데이트 성공 (반환 필요) → 200 OK
삭제 성공 → 204 No Content
로그인 안 함 → 401 Unauthorized
로그인 했지만 권한 없음 → 403 Forbidden
리소스 없음 → 404 Not Found
메서드 지원 안 함 → 405 Method Not Allowed
유효성 검사 실패 → 400 Bad Request
Rate Limit → 429 Too Many Requests
서버 버그 → 500 Internal Server Error
백엔드 연결 실패 → 502 Bad Gateway
일시적 과부하 → 503 Service Unavailable
백엔드 응답 지연 → 504 Gateway Timeout
HTTP/1.1 문제점
flowchart TB
subgraph Problems[HTTP/1.1 문제점]
P1["🐌 HOL Blocking\nHead-of-Line Blocking"]
P2["📦 헤더 중복\n매 요청마다 반복"]
P3["🔗 연결 제한\n브라우저당 6-8개"]
P4["⏱️ 느린 시작\nTCP + TLS 핸드셰이크"]
end
subgraph Impact[영향]
I1[페이지 로딩 지연]
I2[대역폭 낭비]
I3[연결 관리 복잡]
I4[모바일 성능 저하]
end
Problems --> Impact
HOL Blocking 예시:
연결 1: [HTML ████████████████] 완료
연결 2: [CSS ████░░░░░░░░░░░░] 대기 중....(패킷 손실)
연결 3: [JS ░░░░░░░░░░░░░░░░] 대기 중....(CSS 완료 대기)
연결 4: [IMG ░░░░░░░░░░░░░░░] 대기 중...
→ CSS 하나가 지연되면 나머지 모두 대기
2. HTTPS와 TLS
HTTPS란?
HTTPS는 HTTP over TLS/SSL로, HTTP 통신을 암호화하여 보안을 제공합니다.
TLS 핸드셰이크 (TLS 1.2)
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: TCP 3-Way Handshake (1-RTT)
C->>S: SYN
S-->>C: SYN-ACK
C->>S: ACK
Note over C,S: TLS 핸드셰이크 (2-RTT)
C->>S: ClientHello\n(지원 암호화 알고리즘)
S-->>C: ServerHello\n(선택된 암호화, 인증서)
S-->>C: Certificate\n(서버 인증서)
S-->>C: ServerHelloDone
C->>S: ClientKeyExchange\n(암호화된 세션 키)
C->>S: ChangeCipherSpec
C->>S: Finished
S-->>C: ChangeCipherSpec
S-->>C: Finished
Note over C,S: 암호화된 HTTP 통신
C->>S: GET /index.html (암호화)
S-->>C: HTTP/1.1 200 OK (암호화)
총 지연: 3-RTT (TCP 1-RTT + TLS 2-RTT)
TLS 1.3 (개선)
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: TCP 3-Way Handshake (1-RTT)
C->>S: SYN
S-->>C: SYN-ACK
C->>S: ACK
Note over C,S: TLS 1.3 핸드셰이크 (1-RTT)
C->>S: ClientHello\n(암호화 알고리즘 + 키 공유)
S-->>C: ServerHello + Certificate + Finished\n(한 번에 전송)
Note over C,S: 즉시 암호화 통신 가능
C->>S: GET /index.html (암호화)
S-->>C: HTTP/1.1 200 OK (암호화)
총 지연: 2-RTT (TCP 1-RTT + TLS 1-RTT)
TLS 1.3 내부 메커니즘 - 왜 더 빠른가?
TLS 1.2 vs TLS 1.3 상세 비교:
TLS 1.2 핸드셰이크 (2-RTT):
Client → Server:
ClientHello
- Protocol Version: TLS 1.2
- Random (32 bytes)
- Cipher Suites: [TLS_RSA_WITH_AES_128_GCM_SHA256, ...]
- Extensions
Server → Client:
ServerHello
- Chosen Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256
- Random (32 bytes)
Certificate
- X.509 인증서 체인
- 공개 키 (RSA 2048-bit)
ServerKeyExchange (선택사항)
- DHE/ECDHE 사용 시
ServerHelloDone
Client → Server:
ClientKeyExchange
- PreMasterSecret (RSA로 암호화)
ChangeCipherSpec
- 이제부터 암호화 시작
Finished
- 핸드셰이크 해시 검증
Server → Client:
ChangeCipherSpec
Finished
→ 4번의 왕복 (2-RTT)
───────────────────────────────────────
TLS 1.3 핸드셰이크 (1-RTT):
Client → Server (첫 왕복):
ClientHello
- Protocol Version: TLS 1.3
- Random (32 bytes)
- Cipher Suites: [TLS_AES_128_GCM_SHA256, ...]
- Key Share Extension ← 핵심!
* 클라이언트가 ECDHE 키 공유 미리 전송
* x25519, secp256r1, secp384r1 등
* 예상되는 키 교환 알고리즘으로 즉시 생성
예시:
Key Share Entry:
Group: x25519 (0x001d)
Key Exchange: 0x1234abcd... (32 bytes)
계산:
private_key = random(32 bytes)
public_key = X25519(private_key, G) # G: generator
→ public_key를 ClientHello에 포함
Server → Client (첫 왕복 응답):
ServerHello
- Chosen Cipher Suite: TLS_AES_128_GCM_SHA256
- Key Share Extension ← 서버도 즉시 응답!
* 서버의 ECDHE 키 공유
* 클라이언트 키로 즉시 공유 비밀 계산 가능
{EncryptedExtensions} ← 이미 암호화!
- 서버 확장 정보
{Certificate}
- X.509 인증서
{CertificateVerify}
- 인증서 서명 검증
{Finished}
- 핸드셰이크 검증
Client → Server (두 번째 왕복):
{Finished}
- 클라이언트 핸드셰이크 검증
[Application Data] ← 즉시 HTTP 요청 가능!
- GET /index.html
→ 2번의 왕복 (1-RTT) + 즉시 데이터 전송
───────────────────────────────────────
TLS 1.3 개선 사항:
1. Pre-Shared Key (미리 키 교환):
클라이언트가 예상 키 알고리즘으로 키를 미리 생성
서버가 즉시 응답 가능 → 1-RTT
2. 불필요한 단계 제거:
TLS 1.2: ChangeCipherSpec 명시적 메시지
TLS 1.3: Key Share 후 즉시 암호화 시작
3. RSA 제거:
TLS 1.2: RSA 키 교환 (Forward Secrecy 없음)
TLS 1.3: ECDHE 필수 (Forward Secrecy)
4. 약한 암호화 제거:
TLS 1.2: RC4, 3DES, MD5, SHA1
TLS 1.3: AES-GCM, ChaCha20-Poly1305만
5. 압축 제거:
CRIME 공격 방지 (압축을 통한 정보 유출)
TLS 1.3 0-RTT (세션 재개 시):
0-RTT (Zero Round Trip Time):
이전 연결에서 PSK (Pre-Shared Key) 협상 완료 시:
Client → Server (첫 패킷):
ClientHello
- PSK Extension
* PSK Identity (이전 연결 ID)
* PSK Binder (검증 값)
- Early Data Extension
* 0-RTT 지원 알림
{Application Data} ← HTTP 요청 즉시 전송!
- GET /api/data HTTP/1.1
- (PSK로 암호화)
Server → Client:
ServerHello
- PSK 수락
{Application Data} ← HTTP 응답
- HTTP/1.1 200 OK
- {...}
→ 0-RTT! 연결 설정 없이 즉시 데이터 전송
0-RTT 제약사항:
- 재전송 공격 (Replay Attack) 가능
* 공격자가 0-RTT 데이터를 캡처하여 재전송
* 멱등성(Idempotent) 요청만 안전
- ✅ GET /data (안전)
- ❌ POST /transfer (위험!)
해결책:
- GET 요청만 0-RTT 허용
- POST/PUT/DELETE는 1-RTT 대기
- 서버가 replay 방지 메커니즘 구현 (Nonce)
- PSK 수명:
* 일반적으로 7일
* max_early_data_size 제한 (보통 16KB)
TLS 핸드셰이크 암호화 메커니즘:
키 파생 과정 (HKDF):
1. Key Share 교환:
Client Public Key: pub_c
Server Public Key: pub_s
Client Private Key: priv_c
Server Private Key: priv_s
2. Shared Secret 계산 (ECDH):
shared_secret = ECDH(priv_c, pub_s)
= ECDH(priv_s, pub_c)
(양쪽이 같은 값 도출)
3. Handshake Secret 파생 (HKDF):
handshake_secret = HKDF-Extract(
salt: early_secret,
IKM: shared_secret
)
4. Handshake Keys 파생:
client_handshake_key = HKDF-Expand-Label(
handshake_secret,
"c hs traffic",
ClientHello...ServerHello
)
server_handshake_key = HKDF-Expand-Label(
handshake_secret,
"s hs traffic",
ClientHello...ServerHello
)
5. Application Secret 파생:
master_secret = HKDF-Extract(
salt: handshake_secret,
IKM: 0
)
client_application_key = HKDF-Expand-Label(
master_secret,
"c ap traffic",
full_handshake
)
6. 데이터 암호화:
AES-128-GCM 또는 ChaCha20-Poly1305
암호화:
ciphertext = AES-GCM.Encrypt(
key: client_application_key,
nonce: sequence_number,
data: HTTP_request
)
Forward Secrecy:
- 핸드셰이크마다 새 ECDHE 키 생성
- 이전 세션 키 유출 시에도 과거 데이터 안전
- 서버 개인키 유출 시에도 세션 키 복구 불가
TLS 1.3 vs TLS 1.2 성능 비교:
실측 예시 (RTT 50ms):
TLS 1.2:
- TCP 3-Way: 50ms
- TLS Handshake: 100ms (2-RTT)
- HTTP Request: 50ms
─────────────────────
Total: 200ms
TLS 1.3 (첫 연결):
- TCP 3-Way: 50ms
- TLS Handshake: 50ms (1-RTT)
- HTTP Request: 0ms (동시 전송)
─────────────────────
Total: 100ms (50% 개선!)
TLS 1.3 (0-RTT 재개):
- TCP 3-Way: 50ms
- TLS Handshake: 0ms
- HTTP Request: 0ms (동시 전송)
─────────────────────
Total: 50ms (75% 개선!)
모바일 환경 (RTT 200ms):
TLS 1.2: 600ms
TLS 1.3: 200ms (67% 개선!)
TLS 1.3 (0-RTT): 200ms
HTTPS 구현 (Node.js)
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
// SSL/TLS 인증서 로드
const options = {
key: fs.readFileSync('/etc/ssl/private/server.key'),
cert: fs.readFileSync('/etc/ssl/certs/server.crt'),
// TLS 1.3 강제
minVersion: 'TLSv1.3',
// 암호화 스위트 설정
ciphers: [
'TLS_AES_128_GCM_SHA256',
'TLS_AES_256_GCM_SHA384',
'TLS_CHACHA20_POLY1305_SHA256'
].join(':'),
// HSTS (HTTP Strict Transport Security)
honorCipherOrder: true
};
app.get('/', (req, res) => {
res.send('Hello HTTPS!');
});
// HTTPS 서버 시작
https.createServer(options, app).listen(443, () => {
console.log('✅ HTTPS Server running on port 443');
});
// HTTP → HTTPS 리다이렉트
const http = require('http');
http.createServer((req, res) => {
res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });
res.end();
}).listen(80);
Let’s Encrypt (무료 SSL)
# Certbot 설치
sudo apt install certbot python3-certbot-nginx
# Nginx용 인증서 발급
sudo certbot --nginx -d example.com -d www.example.com
# 수동 발급
sudo certbot certonly --standalone -d example.com
# 자동 갱신 (90일마다)
sudo certbot renew --dry-run
# Cron 등록
0 0 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
3. HTTP/2
HTTP/2란?
HTTP/2는 2015년에 표준화된 프로토콜로, 멀티플렉싱, 헤더 압축, 서버 푸시 등으로 성능을 대폭 개선했습니다.
HTTP/1.1 vs HTTP/2
flowchart TB
subgraph HTTP1[HTTP/1.1]
direction TB
C1[Connection 1] --> R1[HTML]
C2[Connection 2] --> R2[CSS]
C3[Connection 3] --> R3[JS]
C4[Connection 4] --> R4[IMG1]
C5[Connection 5] --> R5[IMG2]
C6[Connection 6] --> R6[IMG3]
end
subgraph HTTP2[HTTP/2]
direction TB
MC["Single Connection\nMultiplexing"]
MC --> S1[Stream 1: HTML]
MC --> S2[Stream 2: CSS]
MC --> S3[Stream 3: JS]
MC --> S4[Stream 4: IMG1]
MC --> S5[Stream 5: IMG2]
MC --> S6[Stream 6: IMG3]
end
style HTTP1 fill:#fcc
style HTTP2 fill:#cfc
HTTP/2 주요 기능
1. 멀티플렉싱 (Multiplexing)
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: 단일 TCP 연결
par Stream 1 (HTML)
C->>S: HEADERS (Stream 1)
S-->>C: HEADERS + DATA (Stream 1)
and Stream 2 (CSS)
C->>S: HEADERS (Stream 2)
S-->>C: HEADERS + DATA (Stream 2)
and Stream 3 (JS)
C->>S: HEADERS (Stream 3)
S-->>C: HEADERS + DATA (Stream 3)
end
Note over C,S: 동시에 여러 리소스 전송
2. 헤더 압축 (HPACK)
HTTP/1.1 (반복되는 헤더):
GET /page1 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0...
Accept: text/html...
Cookie: session=abc123...
[총 500 bytes]
GET /page2 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0...
Accept: text/html...
Cookie: session=abc123...
[총 500 bytes]
→ 1000 bytes 전송
HTTP/2 (HPACK 압축):
Stream 1: [전체 헤더] 500 bytes
Stream 2: [차이만] 50 bytes
→ 550 bytes 전송 (45% 절감)
3. 서버 푸시 (Server Push)
sequenceDiagram
participant C as Client
participant S as Server
C->>S: GET /index.html
Note over S: HTML 파싱 후\n필요한 리소스 예측
S-->>C: PUSH_PROMISE (Stream 2: /style.css)
S-->>C: PUSH_PROMISE (Stream 4: /script.js)
S-->>C: HEADERS + DATA (Stream 1: HTML)
S-->>C: HEADERS + DATA (Stream 2: CSS)
S-->>C: HEADERS + DATA (Stream 4: JS)
Note over C: 클라이언트 요청 전에\n리소스 수신 완료
4. 스트림 우선순위
flowchart TB
Root[Root]
Root -->|Weight: 256| HTML["HTML\nStream 1"]
Root -->|Weight: 128| CSS["CSS\nStream 2"]
Root -->|Weight: 64| JS["JS\nStream 3"]
Root -->|Weight: 32| IMG["Images\nStream 4-10"]
style HTML fill:#f96
style CSS fill:#fc6
style JS fill:#6cf
style IMG fill:#9c6
HTTP/2 멀티플렉싱의 내부 동작
프레임 단위 전송과 인터리빙:
HTTP/1.1의 문제:
하나의 TCP 연결에서 요청-응답이 순차적으로만 처리됨
Client → Server: GET /index.html
Client ← Server: index.html (전체 전송 완료)
Client → Server: GET /style.css
Client ← Server: style.css (전체 전송 완료)
Client → Server: GET /script.js
Client ← Server: script.js (전체 전송 완료)
→ 각 리소스가 완전히 전송될 때까지 다음 리소스 대기
HTTP/2 멀티플렉싱:
하나의 TCP 연결에서 여러 스트림이 동시에 전송
프레임 인터리빙 (Frame Interleaving):
TCP 연결:
┌──────────────────────────────────────────────────────┐
│ Frame 1 (Stream 1: HTML) │ 16KB │
│ Frame 2 (Stream 2: CSS) │ 16KB │
│ Frame 3 (Stream 3: JS) │ 16KB │
│ Frame 1 (Stream 1: HTML) │ 16KB (계속) │
│ Frame 2 (Stream 2: CSS) │ 16KB (계속) │
│ Frame 4 (Stream 4: IMG) │ 16KB │
│ Frame 1 (Stream 1: HTML) │ 8KB (완료) │
│ Frame 3 (Stream 3: JS) │ 16KB (계속) │
└──────────────────────────────────────────────────────┘
→ 모든 스트림이 동시에 조금씩 전송됨
→ 큰 파일이 작은 파일을 차단하지 않음
프레임 크기와 흐름 제어:
기본 설정:
- 최대 프레임 크기: 16KB (SETTINGS_MAX_FRAME_SIZE)
- 초기 윈도우 크기: 65,535 bytes (SETTINGS_INITIAL_WINDOW_SIZE)
큰 파일 전송 시:
1MB 이미지 전송:
1,048,576 bytes / 16,384 bytes per frame = 64 frames
Frame 1: Stream 1, Length 16384, Type DATA
Frame 2: Stream 1, Length 16384, Type DATA
...
Frame 64: Stream 1, Length 16384, Type DATA
동시에 다른 스트림도 인터리빙:
Frame 1: Stream 1 (IMG)
Frame 2: Stream 2 (HTML)
Frame 3: Stream 3 (CSS)
Frame 4: Stream 1 (IMG)
Frame 5: Stream 2 (HTML)
...
흐름 제어 (Flow Control):
Client → Server: WINDOW_UPDATE (Stream 0: +100KB)
→ 전체 연결의 윈도우 증가
Client → Server: WINDOW_UPDATE (Stream 1: +50KB)
→ Stream 1의 윈도우 증가
Server는 윈도우 크기만큼만 전송 가능
→ 과도한 버퍼링 방지
HTTP/2 프레임 구조
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
프레임 필드 상세:
Length (24 bits):
- 프레임 페이로드 크기 (0 ~ 16,777,215 bytes)
- 기본 최대: 16,384 bytes (16KB)
- SETTINGS_MAX_FRAME_SIZE로 조정 가능
Type (8 bits):
프레임 타입:
0x0 DATA - 실제 데이터
0x1 HEADERS - HTTP 헤더
0x2 PRIORITY - 스트림 우선순위
0x3 RST_STREAM - 스트림 종료
0x4 SETTINGS - 연결 설정
0x5 PUSH_PROMISE - 서버 푸시 예고
0x6 PING - 연결 확인
0x7 GOAWAY - 연결 종료 알림
0x8 WINDOW_UPDATE - 흐름 제어
0x9 CONTINUATION - 헤더 연속
Flags (8 bits):
프레임 타입별 플래그:
- DATA:
* END_STREAM (0x1): 스트림 마지막 프레임
* PADDED (0x8): 패딩 포함
- HEADERS:
* END_STREAM (0x1)
* END_HEADERS (0x4): 헤더 블록 완료
* PADDED (0x8)
* PRIORITY (0x20): 우선순위 정보 포함
Stream Identifier (31 bits):
- 스트림 ID (0 ~ 2^31-1)
- 0: 전체 연결 제어
- 홀수: 클라이언트 시작 스트림
- 짝수: 서버 시작 스트림 (PUSH_PROMISE)
프레임 전송 예시:
GET /api/users 요청:
1. HEADERS Frame (Stream 1):
Length: 45 bytes
Type: HEADERS (0x1)
Flags: END_HEADERS (0x4)
Stream ID: 1
Payload:
:method: GET
:path: /api/users
:scheme: https
:authority: api.example.com
user-agent: curl/8.0.0
2. Server 응답 - HEADERS Frame (Stream 1):
Length: 32 bytes
Type: HEADERS (0x1)
Flags: 0x0 (계속 전송)
Stream ID: 1
Payload:
:status: 200
content-type: application/json
content-length: 1024
3. Server 응답 - DATA Frame (Stream 1):
Length: 1024 bytes
Type: DATA (0x0)
Flags: END_STREAM (0x1) (마지막 프레임)
Stream ID: 1
Payload:
{"users": [...]}
동시에 다른 요청:
1. HEADERS Frame (Stream 3):
GET /api/posts
2. HEADERS Frame (Stream 5):
GET /api/comments
→ 3개 스트림이 동시에 처리됨
HTTP/2 서버 설정 (Nginx)
# /etc/nginx/nginx.conf
server {
listen 443 ssl http2;
server_name example.com;
# SSL 인증서
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# TLS 설정
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# HTTP/2 설정
http2_push_preload on;
location / {
root /var/www/html;
index index.html;
# 서버 푸시
add_header Link "</style.css>; rel=preload; as=style";
add_header Link "</script.js>; rel=preload; as=script";
}
}
HTTP/2 클라이언트 (Python)
import httpx
import asyncio
async def http2_example():
"""HTTP/2 클라이언트"""
async with httpx.AsyncClient(http2=True) as client:
# 단일 요청
response = await client.get('https://example.com/')
print(f"Protocol: {response.http_version}")
print(f"Status: {response.status_code}")
# 병렬 요청 (멀티플렉싱)
urls = [
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3',
]
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
for i, resp in enumerate(responses):
print(f"URL {i+1}: {resp.status_code} ({len(resp.content)} bytes)")
asyncio.run(http2_example())
4. HTTP/3와 QUIC
HTTP/3란?
HTTP/3는 TCP 대신 QUIC(UDP 기반)을 사용하는 차세대 프로토콜로, 2022년에 표준화되었습니다.
TCP vs QUIC 연결 설정
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: HTTP/2 over TLS 1.3 (2-RTT)
C->>S: TCP SYN
S-->>C: TCP SYN-ACK
C->>S: TCP ACK + TLS ClientHello
S-->>C: TLS ServerHello + Certificate + Finished
C->>S: HTTP Request
S-->>C: HTTP Response
Note over C,S: 총 2-RTT 후 데이터 전송
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: HTTP/3 over QUIC (1-RTT)
C->>S: QUIC Initial Packet\n(ClientHello + 연결 설정)
S-->>C: QUIC Handshake Packet\n(ServerHello + Certificate + HTTP)
C->>S: HTTP Request (암호화)
S-->>C: HTTP Response (암호화)
Note over C,S: 총 1-RTT 후 데이터 전송
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: HTTP/3 0-RTT (재연결)
C->>S: QUIC 0-RTT Packet\n(이전 세션 키 + HTTP Request)
S-->>C: HTTP Response (암호화)
Note over C,S: 즉시 데이터 전송 (0-RTT)
QUIC 주요 특징
1. HOL Blocking 해결
flowchart TB
subgraph TCP["HTTP/2 (TCP)"]
direction LR
T1[TCP Stream]
T1 --> P1[Packet 1 ✅]
T1 --> P2[Packet 2 ❌ Lost]
T1 --> P3[Packet 3 ⏸️ Blocked]
T1 --> P4[Packet 4 ⏸️ Blocked]
end
subgraph QUIC["HTTP/3 (QUIC)"]
direction LR
Q1[Stream 1] --> QP1[Packet 1 ✅]
Q2[Stream 2] --> QP2[Packet 2 ❌ Lost]
Q3[Stream 3] --> QP3[Packet 3 ✅ Continue]
Q4[Stream 4] --> QP4[Packet 4 ✅ Continue]
end
style TCP fill:#fcc
style QUIC fill:#cfc
TCP: 하나의 패킷 손실이 모든 스트림을 차단
QUIC: 각 스트림이 독립적, 손실된 스트림만 영향
2. 연결 마이그레이션
flowchart LR
subgraph Before[Wi-Fi 연결]
C1["Client\nIP: 192.168.1.100"]
S1[Server]
C1 -->|QUIC Connection ID: abc123| S1
end
subgraph After[4G 전환]
C2["Client\nIP: 203.0.113.50"]
S2[Server]
C2 -->|Same Connection ID: abc123| S2
end
Before -.->|Network Switch| After
style After fill:#cfc
TCP: IP 변경 시 연결 끊김
QUIC: Connection ID로 연결 유지
3. 패킷 손실 복구 메커니즘
TCP의 패킷 손실 복구:
TCP 시퀀스 번호 (Sequence Number) 기반:
전송:
Seq: 1000 → [Data: Hello]
Seq: 1005 → [Data: World] ❌ Lost
Seq: 1010 → [Data: !!!!]
수신자:
Recv Seq: 1000 → ACK 1005 (다음 기대: 1005)
Recv Seq: 1010 → ACK 1005 (여전히 1005 기대) ← Duplicate ACK
송신자:
Duplicate ACK 3번 수신 → Fast Retransmit
Retx Seq: 1005 → [Data: World] (동일한 시퀀스 번호)
문제점:
1. 재전송인지 원본인지 구별 불가능
→ RTT 측정 오차
2. 재전송 모호성 (Retransmission Ambiguity):
Seq 1005 전송 → Lost
Seq 1005 재전송 → ACK 수신
질문: 어느 패킷에 대한 ACK인가?
→ 원본? 재전송?
→ RTT 계산 불가능
QUIC의 패킷 손실 복구:
QUIC 패킷 번호 (Packet Number) 기반:
전송:
Pkt #1 → [Stream 1: Hello]
Pkt #2 → [Stream 2: World] ❌ Lost
Pkt #3 → [Stream 1: !!!!]
Pkt #4 → [Stream 3: Foo]
수신자:
Recv Pkt #1 → ACK 1
Recv Pkt #3 → ACK 3 (Pkt #2 누락 감지)
Recv Pkt #4 → ACK 4
송신자:
ACK 3 수신 → Pkt #2 손실 감지
Retx Pkt #5 → [Stream 2: World] (새 패킷 번호!)
수신자:
Recv Pkt #5 → ACK 5
→ Stream 2 데이터 복구 완료
장점:
1. 패킷 번호가 계속 증가
→ 재전송도 새 번호
→ 원본/재전송 구별 가능
2. 정확한 RTT 측정:
Pkt #2 전송 시각: T1
Pkt #5 재전송 시각: T2
ACK 5 수신 시각: T3
RTT = T3 - T2 (재전송에 대한 RTT)
원본 손실 시간 = T2 - T1
3. 스트림 독립성:
Stream 1: Pkt #1, #3 → 계속 처리 ✅
Stream 2: Pkt #2 손실 → 대기 ⏸️
Stream 3: Pkt #4 → 계속 처리 ✅
→ Stream 1, 3는 Stream 2의 손실에 영향 없음
QUIC ACK 프레임 구조:
ACK Frame:
┌─────────────────────────────────┐
│ Type: ACK (0x02) │
│ Largest Acknowledged: 10 │ ← 수신한 최대 패킷 번호
│ ACK Delay: 25 microseconds │ ← ACK 전송 지연
│ ACK Range Count: 2 │ ← 범위 개수
│ │
│ First ACK Range: 2 │ ← Pkt #10, #9, #8 수신
│ (Largest - 2 to Largest) │
│ │
│ Gap 1: 1 │ ← Pkt #7 누락
│ ACK Range 1: 3 │ ← Pkt #6, #5, #4 수신
│ │
│ Gap 2: 1 │ ← Pkt #3 누락
│ ACK Range 2: 2 │ ← Pkt #2, #1 수신
└─────────────────────────────────┘
해석:
수신: Pkt #1, #2, (X), #4, #5, #6, (X), #8, #9, #10
누락: Pkt #3, #7
송신자 액션:
- Pkt #3, #7 재전송 (새 패킷 번호 사용)
- Pkt #11 → [Stream A: Data from Pkt #3]
- Pkt #12 → [Stream B: Data from Pkt #7]
TCP vs QUIC 손실 복구 비교:
시나리오: 100개 패킷 전송, 10번째 패킷 손실
TCP:
1. Pkt 1-9 수신 → 정상 처리
2. Pkt 10 손실
3. Pkt 11-100 수신 → 버퍼에 대기 ⏸️
→ TCP는 순서 보장 필수
→ 애플리케이션에 전달 불가
4. Pkt 10 재전송 수신
5. Pkt 10-100 모두 애플리케이션에 전달
→ 지연 발생!
QUIC:
1. Pkt 1-9 (Stream 1, 2, 3) → 정상 처리
2. Pkt 10 (Stream 2) 손실
3. Pkt 11-100:
- Stream 1: 계속 처리 ✅
- Stream 2: Pkt 10 대기 ⏸️
- Stream 3: 계속 처리 ✅
4. Pkt 101 (Pkt 10 재전송, Stream 2) 수신
5. Stream 2 데이터 처리 완료
→ Stream 1, 3는 영향 없음!
QUIC 손실 감지 메커니즘:
1. ACK 기반 감지:
Recv ACK: Pkt #5
Sent: Pkt #1, #2, #3
→ Pkt #1, #2, #3 중 #3보다 작은 패킷이
ACK되지 않으면 손실로 간주
2. 패킷 임계값 (Packet Threshold):
기본값: 3개
Recv ACK: Pkt #10
Sent: Pkt #5 (미ACK), #8, #9, #10
→ Pkt #10 - Pkt #5 = 5
→ 임계값(3) 초과 → Pkt #5 손실
3. 타임 임계값 (Time Threshold):
RTT의 9/8 배 대기
Sent Pkt #5: T = 0ms
Current RTT: 100ms
Threshold: 100 * 9/8 = 112.5ms
T = 112.5ms 시점에 ACK 없으면 → 손실
4. Probe Timeout (PTO):
모든 감지 실패 시:
PTO = smoothed_RTT + 4 * RTT_variance + max_ack_delay
PTO 초과 시 재전송
QUIC 프로토콜 스택
flowchart TB
subgraph App[Application Layer]
HTTP3[HTTP/3]
end
subgraph Transport[Transport Layer]
QUIC[QUIC]
subgraph QF[QUIC Features]
Crypto["암호화\nTLS 1.3"]
Stream["스트림\n멀티플렉싱"]
Flow[흐름 제어]
Congestion[혼잡 제어]
Loss[손실 복구]
end
end
subgraph Network[Network Layer]
UDP[UDP]
IP[IP]
end
HTTP3 --> QUIC
QUIC --> QF
QUIC --> UDP
UDP --> IP
HTTP/3 서버 설정 (Nginx)
# Nginx 1.25.0+ (HTTP/3 지원)
server {
listen 443 ssl;
listen 443 quic reuseport;
http2 on;
http3 on;
server_name example.com;
# SSL 인증서
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# TLS 1.3 (HTTP/3 필수)
ssl_protocols TLSv1.3;
# QUIC 설정
quic_retry on;
# Alt-Svc 헤더 (HTTP/3 지원 알림)
add_header Alt-Svc 'h3=":443"; ma=86400';
location / {
root /var/www/html;
index index.html;
}
}
# 방화벽 설정 (UDP 포트 443 열기)
sudo ufw allow 443/udp
HTTP/3 클라이언트 (curl)
# curl 8.0+ (HTTP/3 지원)
curl --http3 https://example.com/
# HTTP/3 강제
curl --http3-only https://example.com/
# 프로토콜 확인
curl -I --http3 https://cloudflare.com/
# HTTP/3 200
# 상세 정보
curl -v --http3 https://example.com/
# * using HTTP/3
# * Connected to example.com (203.0.113.1) port 443
# * using HTTP/3 stream 0
HTTP/3 클라이언트 (Python)
import httpx
async def http3_example():
"""HTTP/3 클라이언트"""
# HTTP/3 지원 클라이언트
async with httpx.AsyncClient(http2=True) as client:
response = await client.get('https://cloudflare.com/')
print(f"Protocol: {response.http_version}")
print(f"Status: {response.status_code}")
print(f"Headers: {response.headers}")
# Alt-Svc 헤더 확인
alt_svc = response.headers.get('alt-svc')
if alt_svc and 'h3' in alt_svc:
print("✅ Server supports HTTP/3")
import asyncio
asyncio.run(http3_example())
5. 프로토콜 비교
종합 비교표
| 특성 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 전송 계층 | TCP | TCP | UDP (QUIC) |
| 암호화 | 선택적 | 사실상 필수 | 필수 |
| 멀티플렉싱 | ❌ | ✅ | ✅ |
| 헤더 압축 | ❌ | ✅ HPACK | ✅ QPACK |
| 서버 푸시 | ❌ | ✅ | ✅ |
| HOL Blocking | ✅ 있음 | ⚠️ TCP 레벨 | ❌ 없음 |
| 연결 설정 | 3-RTT | 2-RTT | 1-RTT (0-RTT) |
| 연결 마이그레이션 | ❌ | ❌ | ✅ |
| 패킷 손실 영향 | 큼 | 중간 | 작음 |
| 브라우저 지원 | 100% | 97% | 75% |
성능 비교 (실제 측정)
환경: 100ms RTT, 1% 패킷 손실
페이지 로딩 시간:
HTTP/1.1: 3.2초
HTTP/2: 1.8초 (44% 개선)
HTTP/3: 1.2초 (63% 개선)
모바일 (200ms RTT, 2% 패킷 손실):
HTTP/1.1: 6.5초
HTTP/2: 4.2초 (35% 개선)
HTTP/3: 2.8초 (57% 개선)
연결 설정 시간:
HTTP/1.1 + TLS 1.2: 300ms (3-RTT)
HTTP/2 + TLS 1.3: 200ms (2-RTT)
HTTP/3 (첫 연결): 100ms (1-RTT)
HTTP/3 (재연결): 0ms (0-RTT)
프로토콜 진화 타임라인
timeline
title HTTP 프로토콜 진화
1991 : HTTP/0.9
: 단순 GET 요청
1996 : HTTP/1.0
: 헤더, POST 추가
1997 : HTTP/1.1
: Keep-Alive
: 파이프라이닝
2015 : HTTP/2
: 멀티플렉싱
: 헤더 압축
: 서버 푸시
2022 : HTTP/3
: QUIC (UDP)
: 0-RTT
: 연결 마이그레이션
6. 실전 구현
HTTP/2 서버 (Node.js)
const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const server = http2.createSecureServer({
key: fs.readFileSync('/etc/ssl/private/server.key'),
cert: fs.readFileSync('/etc/ssl/certs/server.crt')
});
server.on('stream', (stream, headers) => {
const reqPath = headers[':path'];
console.log(`📥 Request: ${reqPath}`);
if (reqPath === '/') {
// HTML 응답
stream.respond({
'content-type': 'text/html',
':status': 200
});
// 서버 푸시 (CSS, JS)
stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
if (!err) {
pushStream.respond({
'content-type': 'text/css',
':status': 200
});
pushStream.end(fs.readFileSync('./public/style.css'));
}
});
stream.pushStream({ ':path': '/script.js' }, (err, pushStream) => {
if (!err) {
pushStream.respond({
'content-type': 'application/javascript',
':status': 200
});
pushStream.end(fs.readFileSync('./public/script.js'));
}
});
stream.end('<html><head><link rel="stylesheet" href="/style.css"><script src="/script.js"></script></head><body><h1>Hello HTTP/2!</h1></body></html>');
} else {
stream.respond({ ':status': 404 });
stream.end('Not Found');
}
});
server.listen(443, () => {
console.log('✅ HTTP/2 Server running on port 443');
});
HTTP/3 서버 (Go)
package main
import (
"fmt"
"log"
"net/http"
"github.com/quic-go/quic-go/http3"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello HTTP/3!\n")
fmt.Fprintf(w, "Protocol: %s\n", r.Proto)
})
mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"message": "HTTP/3 response"}`)
})
// HTTP/3 서버
server := http3.Server{
Addr: ":443",
Handler: mux,
}
log.Println("✅ HTTP/3 Server running on :443")
err := server.ListenAndServeTLS("/etc/ssl/certs/server.crt", "/etc/ssl/private/server.key")
if err != nil {
log.Fatal(err)
}
}
프로토콜 협상 (Protocol Negotiation)
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: 첫 방문 (HTTP/1.1 또는 HTTP/2)
C->>S: GET / HTTP/1.1\nHost: example.com
S-->>C: HTTP/1.1 200 OK\nAlt-Svc: h3=":443"; ma=86400
Note over C: Alt-Svc 헤더로\nHTTP/3 지원 확인
Note over C,S: 다음 요청 (HTTP/3)
C->>S: QUIC Initial (UDP:443)
S-->>C: QUIC Handshake
C->>S: HTTP/3 Request
S-->>C: HTTP/3 Response
7. 성능 최적화
HTTP/1.1 최적화
<!-- 1. 리소스 번들링 -->
<link rel="stylesheet" href="bundle.css">
<script src="bundle.js"></script>
<!-- 2. 도메인 샤딩 (연결 제한 우회) -->
<img src="https://cdn1.example.com/img1.jpg">
<img src="https://cdn2.example.com/img2.jpg">
<img src="https://cdn3.example.com/img3.jpg">
<!-- 3. 인라인 CSS/JS (작은 리소스) -->
<style>
body { margin: 0; }
</style>
<!-- 4. 스프라이트 이미지 -->
<div class="icon icon-home"></div>
# Nginx 설정
server {
# Keep-Alive
keepalive_timeout 65;
keepalive_requests 100;
# Gzip 압축
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
# 캐싱
location ~* \.(jpg|jpeg|png|gif|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
HTTP/2 최적화
<!-- ❌ HTTP/1.1 방식 (불필요) -->
<link rel="stylesheet" href="bundle.css">
<!-- ✅ HTTP/2 방식 (개별 파일) -->
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="layout.css">
<link rel="stylesheet" href="components.css">
<!-- 리소스 힌트 -->
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://api.example.com">
<link rel="preload" href="/critical.css" as="style">
<link rel="prefetch" href="/next-page.html">
# Nginx HTTP/2 최적화
server {
listen 443 ssl http2;
# HTTP/2 푸시
location = /index.html {
http2_push /style.css;
http2_push /script.js;
http2_push /logo.png;
}
# 우선순위 설정
location ~* \.(css)$ {
add_header Link "</style.css>; rel=preload; as=style";
}
}
HTTP/3 최적화
# Nginx HTTP/3 설정
server {
listen 443 ssl;
listen 443 quic reuseport;
http2 on;
http3 on;
# Alt-Svc 헤더
add_header Alt-Svc 'h3=":443"; ma=86400';
# QUIC 설정
ssl_early_data on;
quic_retry on;
# 0-RTT 보안 (Replay 공격 방지)
location /api/ {
# POST 요청은 0-RTT 비활성화
if ($request_method = POST) {
add_header Early-Data "0";
}
}
}
실전 시나리오
시나리오 1: 성능 측정
// performance-test.js
const https = require('https');
const http2 = require('http2');
async function measureHTTP1(url) {
const start = Date.now();
return new Promise((resolve) => {
https.get(url, (res) => {
let data = ';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
const elapsed = Date.now() - start;
resolve({ protocol: 'HTTP/1.1', elapsed, size: data.length });
});
});
});
}
async function measureHTTP2(url) {
const start = Date.now();
return new Promise((resolve) => {
const client = http2.connect(url);
const req = client.request({ ':path': '/' });
let data = ';
req.on('data', (chunk) => data += chunk);
req.on('end', () => {
const elapsed = Date.now() - start;
client.close();
resolve({ protocol: 'HTTP/2', elapsed, size: data.length });
});
});
}
async function benchmark() {
const url = 'https://example.com';
console.log('🚀 Starting benchmark...\n');
// HTTP/1.1
const http1 = await measureHTTP1(url);
console.log(`HTTP/1.1: ${http1.elapsed}ms (${http1.size} bytes)`);
// HTTP/2
const http2 = await measureHTTP2(url);
console.log(`HTTP/2: ${http2.elapsed}ms (${http2.size} bytes)`);
const improvement = ((http1.elapsed - http2.elapsed) / http1.elapsed * 100).toFixed(1);
console.log(`\n✅ HTTP/2 is ${improvement}% faster`);
}
benchmark();
시나리오 2: 프로토콜 감지
// detect-protocol.js
const http = require('http');
const https = require('https');
function detectProtocol(url) {
return new Promise((resolve) => {
const client = url.startsWith('https') ? https : http;
const req = client.request(url, { method: 'HEAD' }, (res) => {
const protocol = res.httpVersion === '2.0' ? 'HTTP/2' : `HTTP/${res.httpVersion}`;
const altSvc = res.headers['alt-svc'];
const supportsHTTP3 = altSvc && altSvc.includes('h3');
resolve({
protocol,
supportsHTTP3,
altSvc,
statusCode: res.statusCode
});
});
req.end();
});
}
async function checkSites() {
const sites = [
'https://www.google.com',
'https://www.cloudflare.com',
'https://www.facebook.com',
'https://www.youtube.com'
];
for (const site of sites) {
const info = await detectProtocol(site);
console.log(`\n${site}`);
console.log(` Protocol: ${info.protocol}`);
console.log(` HTTP/3: ${info.supportsHTTP3 ? '✅ Supported' : '❌ Not supported'}`);
if (info.altSvc) {
console.log(` Alt-Svc: ${info.altSvc}`);
}
}
}
checkSites();
시나리오 3: HTTP/2 서버 푸시 최적화
const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const server = http2.createSecureServer({
key: fs.readFileSync('/etc/ssl/private/server.key'),
cert: fs.readFileSync('/etc/ssl/certs/server.crt')
});
// 리소스 의존성 맵
const pushMap = {
'/': ['/style.css', '/script.js', '/logo.png'],
'/about': ['/style.css', '/about.js'],
'/products': ['/style.css', '/products.js', '/product-images.css']
};
server.on('stream', (stream, headers) => {
const reqPath = headers[':path'];
// 서버 푸시 (의존 리소스)
if (pushMap[reqPath]) {
pushMap[reqPath].forEach(resource => {
const resourcePath = path.join('./public', resource);
if (fs.existsSync(resourcePath)) {
stream.pushStream({ ':path': resource }, (err, pushStream) => {
if (err) return;
const ext = path.extname(resource);
const contentType = {
'.css': 'text/css',
'.js': 'application/javascript',
'.png': 'image/png',
'.jpg': 'image/jpeg'
}[ext] || 'application/octet-stream';
pushStream.respond({
'content-type': contentType,
':status': 200
});
pushStream.end(fs.readFileSync(resourcePath));
console.log(`📤 Pushed: ${resource}`);
});
}
});
}
// 메인 응답
const filePath = path.join('./public', reqPath === '/' ? '/index.html' : reqPath);
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath);
stream.respond({
'content-type': 'text/html',
':status': 200
});
stream.end(content);
} else {
stream.respond({ ':status': 404 });
stream.end('Not Found');
}
});
server.listen(443, () => {
console.log('✅ HTTP/2 Server with Server Push running on port 443');
});
시나리오 4: 프로토콜 폴백
// multi-protocol-server.js
const http = require('http');
const https = require('https');
const http2 = require('http2');
const fs = require('fs');
const options = {
key: fs.readFileSync('/etc/ssl/private/server.key'),
cert: fs.readFileSync('/etc/ssl/certs/server.crt'),
allowHTTP1: true // HTTP/1.1 폴백 허용
};
// HTTP/2 서버 (HTTP/1.1 폴백 지원)
const server = http2.createSecureServer(options);
server.on('request', (req, res) => {
console.log(`📥 ${req.httpVersion} ${req.method} ${req.url}`);
res.writeHead(200, {
'Content-Type': 'text/plain',
'Alt-Svc': 'h3=":443"; ma=86400' // HTTP/3 지원 알림
});
res.end(`Hello from ${req.httpVersion}!\n`);
});
server.listen(443, () => {
console.log('✅ Multi-protocol server running on port 443');
console.log(' Supports: HTTP/1.1, HTTP/2');
});
// HTTP → HTTPS 리다이렉트
http.createServer((req, res) => {
res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });
res.end();
}).listen(80);
고급 주제
1. QUIC 연결 ID
TCP 연결:
(Client IP, Client Port, Server IP, Server Port)
→ IP 변경 시 연결 끊김
QUIC 연결:
Connection ID: 8바이트 랜덤 값
→ IP 변경해도 Connection ID로 연결 유지
예시:
Wi-Fi: 192.168.1.100:50000 → Server (Connection ID: abc123)
4G: 203.0.113.50:60000 → Server (Connection ID: abc123)
→ 동일한 Connection ID로 연결 유지
2. QPACK (헤더 압축)
HPACK (HTTP/2):
정적 테이블 + 동적 테이블
→ HOL Blocking 발생 (헤더 순서 의존)
QPACK (HTTP/3):
정적 테이블 + 동적 테이블 + 스트림 독립성
→ HOL Blocking 없음 (헤더 순서 독립)
예시:
Stream 1: [Header 1] → 동적 테이블 참조
Stream 2: [Header 2] → Stream 1 대기 불필요
Stream 3: [Header 3] → 독립적 처리
3. 0-RTT 보안 고려사항
# 0-RTT 활성화
ssl_early_data on;
# Replay 공격 방지
location /api/ {
# POST/PUT/DELETE는 0-RTT 비활성화
if ($request_method !~ ^(GET|HEAD)$) {
add_header Early-Data "0";
}
}
# 애플리케이션 레벨 검증
location /api/payment {
# 결제 API는 0-RTT 완전 차단
ssl_early_data off;
}
4. 혼잡 제어
TCP 혼잡 제어:
- Slow Start
- Congestion Avoidance
- Fast Retransmit
- Fast Recovery
QUIC 혼잡 제어:
- Cubic (기본)
- BBR (Bottleneck Bandwidth and RTT)
- Reno
- NewReno
BBR 장점:
- 패킷 손실에 덜 민감
- 대역폭 활용 극대화
- 버퍼 블로트 감소
프로토콜 마이그레이션 가이드
HTTP/1.1 → HTTP/2
# 1단계: HTTPS 활성화 (HTTP/2 필수)
server {
listen 443 ssl;
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
}
# 2단계: HTTP/2 활성화
server {
listen 443 ssl http2;
# 나머지 동일
}
# 3단계: 최적화
server {
listen 443 ssl http2;
# 서버 푸시
http2_push_preload on;
# 스트림 제한
http2_max_concurrent_streams 128;
# 번들링 해제 (HTTP/2에서는 불필요)
location / {
# 개별 파일로 분리
}
}
HTTP/2 → HTTP/3
# 1단계: HTTP/2 유지 + HTTP/3 추가
server {
listen 443 ssl;
listen 443 quic reuseport;
http2 on;
http3 on;
# Alt-Svc 헤더 (HTTP/3 지원 알림)
add_header Alt-Svc 'h3=":443"; ma=86400';
}
# 2단계: 방화벽 설정
# UDP 포트 443 열기
sudo ufw allow 443/udp
# 3단계: 모니터링
# HTTP/3 사용률 확인
tail -f /var/log/nginx/access.log | grep "HTTP/3"
문제 해결
문제 1: HTTP/2 연결 실패
# 증상
curl: (16) Error in the HTTP2 framing layer
# 원인 1: 서버가 HTTP/2를 지원하지 않음
# 확인
curl -I --http2 https://example.com/
# HTTP/1.1 200 OK (HTTP/2 미지원)
# 원인 2: ALPN (Application-Layer Protocol Negotiation) 문제
# 확인
openssl s_client -connect example.com:443 -alpn h2
# ALPN protocol: h2 (정상)
# ALPN protocol: http/1.1 (HTTP/2 미지원)
# 해결: Nginx 설정 확인
# listen 443 ssl http2; 확인
문제 2: HTTP/3 연결 안 됨
# 증상
curl --http3 https://example.com/
curl: (28) Failed to connect to example.com port 443: Connection timed out
# 원인 1: UDP 포트 443 차단
# 확인
sudo netstat -ulnp | grep :443
# (없으면 서버가 QUIC을 리스닝하지 않음)
# 방화벽 확인
sudo ufw status | grep 443
# 443/udp ALLOW Anywhere
# 원인 2: Alt-Svc 헤더 없음
# 확인
curl -I https://example.com/ | grep -i alt-svc
# Alt-Svc: h3=":443"; ma=86400 (정상)
# 해결: Nginx 설정
listen 443 quic reuseport;
http3 on;
add_header Alt-Svc 'h3=":443"; ma=86400';
문제 3: HTTP/2 서버 푸시 작동 안 함
# 증상
# 서버 푸시가 전송되지 않음
# 원인: 브라우저 캐시에 이미 존재
# 브라우저는 캐시된 리소스는 거부함
# 해결: Link 헤더 사용 (조건부 푸시)
add_header Link "</style.css>; rel=preload; as=style";
http2_push_preload on;
# 브라우저가 필요한 경우만 푸시 요청
보안 고려사항
HTTPS 보안 헤더
server {
listen 443 ssl http2;
# HSTS (HTTP Strict Transport Security)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# CSP (Content Security Policy)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
# X-Frame-Options (클릭재킹 방지)
add_header X-Frame-Options "SAMEORIGIN" always;
# X-Content-Type-Options
add_header X-Content-Type-Options "nosniff" always;
# Referrer-Policy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permissions-Policy
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
}
TLS 설정 최적화
server {
# TLS 1.3만 허용 (최신)
ssl_protocols TLSv1.3;
# 또는 TLS 1.2+ (호환성)
ssl_protocols TLSv1.2 TLSv1.3;
# 강력한 암호화 스위트
ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256';
ssl_prefer_server_ciphers off;
# OCSP Stapling (인증서 검증 속도 향상)
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/certs/ca-bundle.crt;
# 세션 재사용
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
# DH 파라미터
ssl_dhparam /etc/ssl/certs/dhparam.pem;
}
모니터링 및 디버깅
Chrome DevTools
// Performance 탭에서 프로토콜 확인
// 1. F12 → Network 탭
// 2. Protocol 컬럼 추가 (우클릭 → Protocol)
// 3. 페이지 새로고침
// 결과:
// index.html h2 200 1.2KB 50ms
// style.css h2 200 5.4KB 52ms (pushed)
// script.js h2 200 8.1KB 53ms (pushed)
// image.png h2 200 45KB 120ms
// Timing 탭:
// - Queueing: 대기 시간
// - Stalled: 연결 대기
// - DNS Lookup: DNS 조회
// - Initial connection: TCP 핸드셰이크
// - SSL: TLS 핸드셰이크
// - Request sent: 요청 전송
// - Waiting (TTFB): 첫 바이트까지 대기
// - Content Download: 다운로드
curl 디버깅
# HTTP/1.1
curl -v https://example.com/
# > GET / HTTP/1.1
# < HTTP/1.1 200 OK
# HTTP/2
curl -v --http2 https://example.com/
# > GET / HTTP/2
# < HTTP/2 200
# HTTP/3
curl -v --http3 https://example.com/
# > GET / HTTP/3
# < HTTP/3 200
# 타이밍 정보
curl -w "time_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_starttransfer: %{time_starttransfer}\ntime_total: %{time_total}\n" -o /dev/null -s https://example.com/
# 헤더만 확인
curl -I --http2 https://example.com/
# 서버 푸시 확인
curl -v --http2 https://example.com/ 2>&1 | grep -i push
Wireshark 패킷 분석
# HTTP/2 트래픽 캡처
sudo tcpdump -i eth0 -w http2.pcap port 443
# Wireshark 필터
# HTTP/2
http2
# QUIC
quic
# TLS 핸드셰이크
ssl.handshake.type == 1
# HTTP/2 프레임 타입
http2.type == 0 # DATA
http2.type == 1 # HEADERS
http2.type == 5 # PUSH_PROMISE
실전 벤치마크
Apache Bench
# HTTP/1.1
ab -n 1000 -c 10 https://example.com/
# HTTP/2 (h2load)
h2load -n 1000 -c 10 -m 10 https://example.com/
# 결과 비교
# HTTP/1.1:
# Requests per second: 150
# Time per request: 66.7ms
#
# HTTP/2:
# Requests per second: 450
# Time per request: 22.2ms
# → 3배 향상
WebPageTest
# WebPageTest API
curl "https://www.webpagetest.org/runtest.php?url=https://example.com&k=API_KEY&f=json"
# 결과:
# HTTP/1.1:
# Load Time: 3.2s
# First Byte: 0.8s
# Start Render: 1.5s
#
# HTTP/2:
# Load Time: 1.8s
# First Byte: 0.5s
# Start Render: 0.9s
#
# HTTP/3:
# Load Time: 1.2s
# First Byte: 0.3s
# Start Render: 0.6s
프로토콜 선택 가이드
의사 결정 플로우차트
flowchart TD
Start[HTTP 프로토콜 선택] --> Q1{레거시 지원 필요?}
Q1 -->|Yes| HTTP1["HTTP/1.1\n최대 호환성"]
Q1 -->|No| Q2{서버 인프라는?}
Q2 -->|최신| Q3{모바일 트래픽 많음?}
Q2 -->|구형| HTTP2["HTTP/2\n성능 개선"]
Q3 -->|Yes| HTTP3["✅ HTTP/3\n최고 성능\n연결 마이그레이션"]
Q3 -->|No| HTTP2
Start --> Q4{보안 필요?}
Q4 -->|Yes| HTTPS["✅ HTTPS 필수\nTLS 1.3"]
Q4 -->|No| WARN["⚠️ HTTP는\n권장하지 않음"]
사용 사례별 추천
✅ 공개 웹사이트:
→ HTTP/3 (Cloudflare, Nginx 1.25+)
→ 폴백: HTTP/2
✅ 내부 API:
→ HTTP/2 (충분한 성능)
✅ 모바일 앱:
→ HTTP/3 (연결 마이그레이션)
✅ 실시간 스트리밍:
→ HTTP/3 (낮은 지연)
✅ 레거시 시스템:
→ HTTP/1.1 (호환성)
✅ IoT 디바이스:
→ HTTP/1.1 (낮은 리소스)
성능 최적화 체크리스트
HTTP/1.1
- [ ] Keep-Alive 활성화
- [ ] Gzip/Brotli 압축
- [ ] 리소스 번들링 (CSS, JS)
- [ ] 스프라이트 이미지
- [ ] 도메인 샤딩 (CDN)
- [ ] 캐싱 헤더 설정
- [ ] 조건부 요청 (ETag, Last-Modified)
HTTP/2
// 실행 예제
- [ ] HTTPS 활성화 (필수)
- [ ] 번들링 해제 (개별 파일)
- [ ] 서버 푸시 활용
- [ ] 리소스 힌트 (preload, prefetch)
- [ ] 스트림 우선순위 설정
- [ ] 헤더 압축 (HPACK)
- [ ] 도메인 샤딩 제거
HTTP/3
- [ ] UDP 포트 443 열기
- [ ] Alt-Svc 헤더 설정
- [ ] 0-RTT 활성화 (보안 고려)
- [ ] QUIC 재시도 활성화
- [ ] 연결 마이그레이션 테스트
- [ ] 패킷 손실 시나리오 테스트
- [ ] 모바일 환경 테스트
실전 예제
예제 1: HTTP/2 클라이언트 (Python)
import httpx
import asyncio
import time
async def compare_protocols():
"""HTTP/1.1 vs HTTP/2 성능 비교"""
urls = [
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3',
'https://example.com/page4',
'https://example.com/page5',
]
# HTTP/1.1
start = time.time()
async with httpx.AsyncClient(http2=False) as client:
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
http1_time = time.time() - start
# HTTP/2
start = time.time()
async with httpx.AsyncClient(http2=True) as client:
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
http2_time = time.time() - start
print(f"HTTP/1.1: {http1_time:.2f}s")
print(f"HTTP/2: {http2_time:.2f}s")
print(f"Improvement: {(1 - http2_time/http1_time) * 100:.1f}%")
asyncio.run(compare_protocols())
예제 2: WebSocket over HTTP/2
// WebSocket은 HTTP/1.1만 지원
// HTTP/2에서는 Server-Sent Events 또는 gRPC 사용
// Server-Sent Events (HTTP/2)
const express = require('express');
const app = express();
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const sendEvent = (data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
// 주기적 이벤트 전송
const interval = setInterval(() => {
sendEvent({ time: new Date().toISOString(), value: Math.random() });
}, 1000);
req.on('close', () => {
clearInterval(interval);
});
});
app.listen(443);
예제 3: HTTP/3 로드 밸런서
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
"github.com/quic-go/quic-go/http3"
)
func main() {
// 백엔드 서버 목록
backends := []*url.URL{
{Scheme: "https", Host: "backend1.example.com"},
{Scheme: "https", Host: "backend2.example.com"},
{Scheme: "https", Host: "backend3.example.com"},
}
var currentBackend int
// 라운드 로빈 로드 밸런서
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
backend := backends[currentBackend]
currentBackend = (currentBackend + 1) % len(backends)
proxy := httputil.NewSingleHostReverseProxy(backend)
proxy.ServeHTTP(w, r)
log.Printf("📤 Proxied to %s", backend.Host)
})
// HTTP/3 서버
server := http3.Server{
Addr: ":443",
Handler: handler,
}
log.Println("✅ HTTP/3 Load Balancer running on :443")
err := server.ListenAndServeTLS("/etc/ssl/certs/server.crt", "/etc/ssl/private/server.key")
if err != nil {
log.Fatal(err)
}
}
프로토콜 협상 (ALPN)
ALPN (Application-Layer Protocol Negotiation)
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello\nALPN: [h2, http/1.1]
Note over S: 지원 프로토콜 확인\n우선순위: h2 > http/1.1
S-->>C: ServerHello\nALPN: h2
Note over C,S: HTTP/2 사용
ALPN 확인
# OpenSSL로 ALPN 확인
openssl s_client -connect example.com:443 -alpn h2,http/1.1
# ALPN protocol: h2
# curl로 확인
curl -v --http2 https://example.com/ 2>&1 | grep ALPN
# * ALPN, server accepted to use h2
실전 배포
Cloudflare (HTTP/3 자동 지원)
// Cloudflare Workers (HTTP/3 자동)
export default {
async fetch(request) {
const url = new URL(request.url);
// 프로토콜 확인
const protocol = request.cf?.httpProtocol || 'HTTP/1.1';
return new Response(JSON.stringify({
protocol,
url: url.pathname,
headers: Object.fromEntries(request.headers)
}), {
headers: {
'Content-Type': 'application/json',
'Alt-Svc': 'h3=":443"; ma=86400'
}
});
}
};
Docker Compose (HTTP/3 스택)
version: '3.8'
services:
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 (QUIC)
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/ssl:ro
- ./html:/usr/share/nginx/html:ro
restart: unless-stopped
app:
image: node:20-alpine
working_dir: /app
volumes:
- ./app:/app
command: node server.js
environment:
- NODE_ENV=production
expose:
- "3000"
내부 동작과 핵심 메커니즘
이 글의 주제는 「HTTP 프로토콜 완벽 가이드 | HTTP/1.1·HTTP/2·HTTP/3·QUIC 비교」입니다. 앞선 튜토리얼을 구현·런타임 관점에서 다시 압축합니다. 요청 경로와 상태 전이를 기준으로 “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)·동시성이 어디서 터지는가”를 한 장면으로 그리면 장애 분석이 빨라집니다.
처리 파이프라인(개념도)
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
경계에서의 지연·실패(시퀀스 관점)
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(프로세스·런타임·게이트웨이) participant D as 의존성(외부 API·DB·큐) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
알고리즘·프로토콜·리소스 관점 체크포인트
- 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(버퍼 경계, 프로토콜 상태, 트랜잭션 격리, 파일 디스크립터 상한)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 동일 입력에 동일 출력이 보장되는 순수 층과, 시간·네트워크·스레드 스케줄에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합, GC·할당, 캐시 미스처럼 누적 비용을 의심 목록에 넣습니다.
- 백프레셔: 생산자가 소비자보다 빠를 때(소켓 버퍼, 큐 깊이, 스트림) 어디서 어떤 신호로 속도를 줄일지 정의합니다.
프로덕션 운영 패턴
실서비스에서는 기능과 함께 관측·배포·보안·비용·규제가 동시에 요구됩니다.
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율/지연 분위수(p95/p99), 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시 계층·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션 호환성·플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·파일 디스크립터·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 가능한 한 프로덕션에 가깝게 맞추는 것이 재현율을 높입니다.
확장 예시: 엔드투엔드 미니 시나리오
「HTTP 프로토콜 완벽 가이드 | HTTP/1.1·HTTP/2·HTTP/3·QUIC 비교」을 실제 배포·운영 흐름으로 옮긴 체크리스트형 시나리오입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드 표를 API 또는 이벤트 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 한 화면(로그+메트릭+트레이스)에서 추적한다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지(또는 피처 플래그) 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값이 기대 범위인지 본다.
의사코드 스케치(프레임워크 무관)
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request) // 경계에서 거절
authorize(validated, ctx) // 권한·테넌트
result = domainCore(validated) // 순수에 가까운 규칙
persistOrEmit(result, idempotentKey) // I/O: 멱등·재시도 정책
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성 불안정, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정이 로컬과 다름 | 프로필·시크릿·기본값, 지역 리전 | 단일 소스(예: 스키마 검증된 설정)와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
정리
프로토콜 진화 요약
flowchart LR
HTTP1["HTTP/1.1\n1997"] -->|문제: HOL Blocking\n연결 제한| HTTP2["HTTP/2\n2015"]
HTTP2 -->|문제: TCP HOL\n느린 핸드셰이크| HTTP3["HTTP/3\n2022"]
HTTP1 -.->|개선| K1["Keep-Alive\n파이프라이닝"]
HTTP2 -.->|개선| K2["멀티플렉싱\n헤더 압축\n서버 푸시"]
HTTP3 -.->|개선| K3["QUIC\n0-RTT\n연결 마이그레이션"]
style HTTP3 fill:#6c6
핵심 명령어
# 프로토콜 확인
curl -I --http2 https://example.com/
curl -I --http3 https://example.com/
# 성능 측정
h2load -n 1000 -c 10 https://example.com/
# TLS 정보
openssl s_client -connect example.com:443 -alpn h2
# Nginx 설정 테스트
sudo nginx -t
# Nginx 재시작
sudo systemctl reload nginx
# 방화벽 (HTTP/3)
sudo ufw allow 443/udp
마이그레이션 로드맵
1단계: HTTPS 활성화 (Let's Encrypt)
↓
2단계: HTTP/2 활성화 (Nginx 설정)
↓
3단계: 최적화 (서버 푸시, 헤더 압축)
↓
4단계: HTTP/3 추가 (Nginx 1.25+)
↓
5단계: 모니터링 및 튜닝
프로토콜별 적용 시기
2026년 현재:
✅ HTTP/1.1:
- 레거시 시스템
- IoT 디바이스
- 제한된 환경
✅ HTTP/2:
- 대부분의 웹사이트 (표준)
- API 서버
- 내부 서비스
✅ HTTP/3:
- 고성능 웹사이트
- 모바일 우선 서비스
- 실시간 애플리케이션
- CDN (Cloudflare, Fastly)
채택률:
HTTP/1.1: 40%
HTTP/2: 55%
HTTP/3: 5% (빠르게 증가 중)
디버깅·면접과 이어 붙이기
curl, openssl s_client, h2load로 프로토콜을 확인하는 방법은 리눅스·맥 명령어 가이드의 네트워크 절과 함께 보면 좋습니다. 백엔드 면접에서 HTTP/2·HTTP/3·TLS를 말로 설명하는 연습은 기술 면접 완벽 대비 가이드의 CS·네트워크 준비 흐름에 넣을 수 있습니다.
관련 글 (내부 링크)
HTTP 프로토콜과 함께 보면 좋은 관련 기술 가이드입니다:
- TCP 연결 상태 완벽 가이드 | 3-way Handshake·타임아웃·소켓 상태 - HTTP 하위 프로토콜
- DRM 완벽 가이드 | Widevine·FairPlay·PlayReady·EME 실전 - HTTP 기반 콘텐츠 보호
- Next.js App Router 완벽 가이드 | 서버 컴포넌트·스트리밍·캐싱 - HTTP/2·HTTP/3 활용
- Cloudflare Workers 완벽 가이드 | Edge Computing - HTTP API 엣지 배포
- API 설계 완벽 가이드 | REST vs GraphQL vs gRPC 비교 - HTTP 기반 API 설계
- C++ 소켓 프로그래밍 | TCP·UDP·비동기 I/O - HTTP 저수준 구현
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. HTTP/1.1, HTTP/2, HTTP/3, QUIC의 동작 원리와 진화 과정. 멀티플렉싱, 헤더 압축, 0-RTT, 패킷 손실 복구까지. 실전 성능 비교와 최적화 전략 완벽 마스터. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
참고 자료
- RFC 9114 - HTTP/3
- RFC 9000 - QUIC
- RFC 7540 - HTTP/2
- RFC 2616 - HTTP/1.1
- HTTP/3 Explained
- QUIC Working Group
- Can I Use HTTP/3 한 줄 요약: HTTP/2는 멀티플렉싱으로 성능을 개선했고, HTTP/3는 QUIC으로 HOL Blocking을 해결하고 연결 설정을 더 빠르게 만들었습니다. 현대 웹사이트는 HTTP/2를 기본으로, 모바일 트래픽이 많으면 HTTP/3를 추가하세요.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- HTTP 프로토콜 완전 가이드 | HTTP/1.1·HTTP/2·HTTP/3·REST·HTTPS·캐시 실전
- C++ 네트워크 | ‘소켓 프로그래밍’ 기초 [TCP/UDP]
- C++ HTTP 클라이언트·서버 완벽 가이드 | Beast·파싱·Keep-Alive·청크 인코딩
이 글에서 다루는 키워드 (관련 검색어)
HTTP, HTTPS, HTTP2, HTTP3, QUIC, TLS, 웹프로토콜, 네트워크 등으로 검색하시면 이 글이 도움이 됩니다.