HTTP/1.1, HTTP/2, HTTP/3 프로토콜 비교 정리

2026. 3. 31. 19:55·기술 학습

웹 개발자라면 HTTP를 매일 사용한다. 하지만 "HTTP/2가 HTTP/1.1보다 빠르다" 수준의 이해에 머물러 있다면, 각 버전이 왜 등장했고 어떤 문제를 해결했는지를 놓치고 있는 것이다. 이 글에서는 HTTP/1.1 → HTTP/2 → HTTP/3로 이어지는 진화의 맥락을 짚고, 각 버전의 커넥션 관리, 멀티플렉싱, 헤더 압축, 전송 계층, 한계점을 깊이 있게 비교한다.


1. HTTP/1.0 — 초기 문제점

HTTP/1.0은 요청 하나를 보내고 응답을 받으면 TCP 커넥션(Connection)을 즉시 닫는다.

단순하지만, 현실의 웹에서는 치명적이다.

요청 하나에도 수십 수백개의 요청들이 가고있다

브라우저가 웹페이지 하나를 로드할 때 필요한 것은 HTML 파일 하나가 아니다.

HTML 안에 포함된 CSS, JavaScript, 이미지, 폰트, API 응답 등 수십~수백 개의 리소스를 서버에 요청해야 한다.

요즘 평균적인 웹페이지는 약 70~100개의 리소스를 요청한다.

 

문제는 이때 연결을 위한 TCP 커넥션이 공짜가 아니라는 것이다. 연결을 맺으려면 3-way Handshake가 필요하다.

클라이언트 → 서버:  SYN
서버 → 클라이언트:  SYN-ACK
클라이언트 → 서버:  ACK

이 과정에 1 RTT(Round-Trip Time)가 소요된다.

서울에서 미국 서버까지 RTT가 약 150~200ms라고 가정하면, 리소스 70개를 요청할 때 순수 연결 비용만 10초 이상이다. HTTPS를 사용하면 TLS Handshake가 추가로 1~2 RTT 더 필요하다.


2. HTTP/1.1 — Keep-Alive 도입 및 한계

1. Persistent Connection (Keep-Alive)

HTTP/1.1의 핵심 해결책은 단순하다. TCP 커넥션을 끊지 말고 재사용하자.

이것이 Persistent Connection, 흔히 Keep-Alive라 불리는 메커니즘이다. HTTP/1.1에서는 해당값이 켜지는게 디폴트이다. 이를 통해 별도로 설정하지 않아도 커넥션이 유지된다.

[TCP 연결] ─────────────────────────────── [TCP 종료]
  GET /index.html  →  응답
  GET /style.css   →  응답
  GET /app.js      →  응답
  GET /logo.png    →  응답

 

  • 하나의 커넥션에서 여러 요청-응답을 순차적으로 처리한다. 각 요청마다 Handshake를 반복하는 HTTP/1.0에 비하면 훨씬 큰 개선이라고 볼 수 있다.

2. HOL Blocking (Head-of-Line Blocking)

하지만 여기서도 문제가 있다. 바로 동기/블로킹 문제이다.

위 그림을 자세히 보면, 요청-응답이 순서대로 하나씩 처리된다. 첫 번째 요청의 응답이 완료될 때까지 두 번째 요청은 대기해야 한다.

[TCP 커넥션]
  GET /heavy-api  →  ⏳⏳⏳ (3초)  →  응답
  GET /style.css  →  (대기)         →  응답
  GET /app.js     →  (대기)         →  응답

style.css는 10ms면 받을 수 있는데도, 앞에 3초짜리 요청이 있다는 이유만으로 3초를 기다려야 한다. 대기줄 맨 앞(Head-of-Line)이 막히면 뒤가 전부 멈추는 이 현상을 HOL Blocking이라 한다.

운영체제에서 스레드 스케줄링 시 언급되는 Convoy Effect와 유사한 현상이다.

3. 파이프라이닝

HTTP/1.1 스펙에는 파이프라이닝이라는 해결책도 포함되어 있다. 응답을 기다리지 않고 요청을 미리 연달아 보내는 방식이다. 즉, 논블로킹으로 요청을 보낼 수 있게 되는것이다.

→ GET /heavy-api
→ GET /style.css   (응답 안 기다리고 전송)
→ GET /app.js      (응답 안 기다리고 전송)

← 응답: /heavy-api
← 응답: /style.css
← 응답: /app.js

 

논블로킹으로 이제 해결이 다 된거같지만 문제가 있다. 블로킹 없이 바로 요청이 가능하므로 전송은 빨라졌지만, 응답은 반드시 요청 순서대로 와야 한다는 제약이 있기 때문이다. 즉, 논블로킹으로 다 요청은 보내놨는데 전부 응답이 올 때까지 join()이 걸리고 동기로 응답을 받아야 하는 것이다.

이로인해 style.css 응답이 먼저 준비되어도 heavy-api 응답이 먼저 나가야 한다. 결국 응답 쪽에서 HOL Blocking이 그대로 발생하는 것이다. 이 문제 때문에 대부분의 브라우저가 파이프라이닝을 기본 비활성화했다. 스펙에는 있지만 사실상 죽은 기능이다.

4. 브라우저의 블로킹 우회 - 다중 커넥션

이러한 블로킹으로 인한 HOL 문제를 해결하고자 브라우저들은 하나의 도메인에 대해 TCP 커넥션을 여러 개(보통 6개) 동시에 여는 방식으로 우회했다.

커넥션1: GET /heavy-api  →  ⏳⏳⏳  →  응답
커넥션2: GET /style.css  →  응답 ✅
커넥션3: GET /app.js     →  응답 ✅
커넥션4: GET /logo.png   →  응답 ✅

 

사실상 병렬처리 형태로, 한 커넥션이 막혀도 다른 커넥션은 독립적으로 진행된다. 마치 멀티 스레드로 요청을 처리하면 하나의 스레드가 블로킹 되어도 나머지가 일할 수 있는것과 동일하다.

하지만 70~100개 리소스에 커넥션 6개로는 여전히 부족하다. 각 커넥션이 독립적으로 3-way Handshake + TLS Handshake를 수행해야 하고, 서버 입장에서는 클라이언트 한 명이 커넥션을 6개씩 점유하므로 리소스 부담도 크다.

 

따라서 브라우저의 다중 커넥션 사용 외에도 프로토콜의 한계를 애플리케이션 레벨에서 보완하고자 여러 방안들이 나왔다.

  • Domain Sharding — img1.shop.com, img2.shop.com처럼 도메인을 쪼개어 "도메인당 6개" 제한을 우회
  • CSS Sprite — 이미지 수십 개를 하나의 큰 이미지로 합쳐서 요청 수를 줄임
  • JS/CSS 번들링 — 여러 파일을 하나로 합침
  • 인라이닝(Inlining) — 작은 이미지를 Base64로 HTML에 직접 삽입

이러한 방안들은 전부 "요청 수를 줄이자" 라는 목적 하나를 위한 우회 기법들이다.

5. HTTP/1.1의 한계: 비효율적인 헤더

HTTP/1.1의 Header는 평문 텍스트(Plain Text)이다. 즉, 매 요청마다 Cookie, User-Agent, Accept 등 거의 동일한 헤더가 반복 전송된다. 일반적으로 헤더 한 번에 500바이트~수 KB인데, 100개 요청이면 순수 헤더만으로 수백 KB가 낭비된다.


3. HTTP/2 — 프로토콜 레벨의 혁신

HTTP/2는 HTTP/1.1의 핵심 한계 세 가지를 정면으로 해결한다.

1. Binary Framing Layer

HTTP/1.1은 메시지가 텍스트였다. 사람은 읽을 수 있지만 컴퓨터가 파싱하기엔 비효율적이다. HTTP/2는 텍스트 메시지를 Binary Frame이라는 작은 단위로 분해했다.

 

HTTP/2의 핵심 개념은 세 가지 계층 구조로 정리된다.

  • Frame — HTTP/2에서 가장 작은 통신 단위. HEADERS Frame, DATA Frame 등 종류가 있으며, 각 Frame에는 자신이 속한 Stream ID가 태그되어 있다.
  • Stream — 하나의 요청-응답 쌍을 의미하는 논리적 통로. GET /style.css가 Stream 1, GET /app.js가 Stream 3 같은 식이다.
  • Connection — 모든 Stream을 담는 단 하나의 TCP 커넥션.
┌─ Connection (TCP 커넥션 1개) ─────────────────────────┐
│                                                     │
│  ┌─ Stream 1 ─────────────────────────────────────┐ │
│  │  [HEADERS Frame]  [DATA Frame]  [DATA Frame]   │ │
│  └────────────────────────────────────────────────┘ │
│                                                     │
│  ┌─ Stream 2 ─────────────────────────────────────┐ │
│  │  [HEADERS Frame]  [DATA Frame]                 │ │
│  └────────────────────────────────────────────────┘ │
│                                                     │
│  ┌─ Stream 3 ─────────────────────────────────────┐ │
│  │  [HEADERS Frame]  [DATA Frame]  [DATA Frame]   │ │
│  └────────────────────────────────────────────────┘ │
│                                                     │
└─────────────────────────────────────────────────────┘

 

2. Multiplexing (멀티 플렉싱) — HOL Blocking 해결

HTTP/1.1에서는 하나의 커넥션에서 요청-응답이 순서대로만 처리되어 HOL Blocking이 발생했다.

HTTP/2에서는 Stream ID로 각 요청-응답을 식별하기 때문에, 서로 다른 Stream의 Frame이 뒤섞여서(Interleaving) 전송될 수 있다.

TCP 위의 Frame 전송 순서 (시간 →):

[S1:HEADERS] [S2:HEADERS] [S1:DATA] [S3:HEADERS] [S2:DATA] [S1:DATA] [S3:DATA]

수신 측에서 Stream별로 분류·재조립:
  Stream 1: HEADERS → DATA → DATA   ✅
  Stream 2: HEADERS → DATA          ✅
  Stream 3: HEADERS → DATA          ✅

같은 Stream 내 Frame 순서는 TCP가 보장하고, 서로 다른 Stream 간에는 순서 제약이 없다. 응답 B가 먼저 준비되면 먼저 보내면 된다. 응답 A가 느려도 B와 C는 영향받지 않는다.

HTTP/1.1에서는 "순서"가 곧 "의존"이었지만, HTTP/2에서는 Stream ID로 "식별"하기 때문에 순차 처리(동기처리)에서 벗어날 수 있다.

3. HPACK — 헤더 압축

HTTP/1.1에서 매 요청마다 중복 헤더를 텍스트로 반복 전송하는 문제를 HPACK이 해결한다. 핵심 아이디어는 두 가지다.

  • Static Table (정적 테이블) — 자주 사용되는 헤더 61개가 미리 번호로 등록되어 있다.
    • ":method: GET"이라는 텍스트 대신 인덱스 2만 보내면 된다.
  • Dynamic Table (동적 테이블) — 통신하면서 새로 등장한 헤더를 양쪽에 동일한 테이블에 기록한다.
첫 번째 요청:  Cookie: session=abc123  → 전체 전송 + 동적 테이블 62번 등록
두 번째 요청:  Cookie: session=abc123  → 인덱스 62만 전송

 

추가로 Huffman Encoding을 적용하여 문자열 자체도 압축한다. 두 번째 요청부터는 헤더 대부분이 인덱스 번호 몇 개로 줄어든다. HTTP/1.1에서 수 KB였던 헤더가 수십 바이트로 압축되는 효과를 얻는다.

4. Server Push (폐기된 기능)

HTTP/1.1에서 서버는 클라이언트가 요청해야만 응답할 수 있었다. HTTP/2의 Server Push는 클라이언트가 요청하기 전에 서버가 필요한 리소스를 선제 전송하는 기능이다.

클라이언트: GET /index.html
서버:
  → 응답: /index.html     (Stream 1)
  → 푸시: /style.css      (Stream 2 — 요청 없이 전송)
  → 푸시: /app.js         (Stream 4 — 요청 없이 전송)

아이디어는 좋았지만 실무에서는 기대만큼 효과적이지 못했다.

  • 캐시 낭비 — 서버는 클라이언트의 Cache 상태를 알 수 없다. 이미 Cache에 있는 리소스를 Push하면 대역폭만 낭비된다.
  • 구현 복잡성 — 서버가 "이 HTML에 어떤 리소스가 필요한지"를 미리 파악하는 로직을 별도로 구현해야 한다.
  • CDN 충돌 — 실무에서 정적 리소스는 보통 CDN에서 서빙하는데, Origin Server가 CDN의 Cache 상태까지 알 수 없어 Push 판단이 어렵다.

실제로 Chrome은 Server Push 지원을 제거했다.

5. Flow Control (흐름 제어) — 커넥션별/스트림별 흐름 제어

TCP에도 흐름 제어(Flow Control)가 있지만, 이는 커넥션 단위이다. HTTP/2에서는 하나의 커넥션 안에 수십~수백 개의 Stream이 공존하므로, Stream별 독립적인 흐름 제어가 필요하다.

 

HTTP/2는 두 단계의 흐름 제어를 운영한다.

  • 커넥션 레벨 윈도우(Connection-level Window) — 커넥션 전체의 전송 허용량
  • 스트림 레벨 윈도우(Stream-level Window) — 각 Stream의 전송 허용량

초기 윈도우 크기는 65,535 바이트이다. 데이터 전송 시 커넥션과 스트림의 윈도우를 둘다 사용하고, 수신 측이 데이터를 처리한 후 WINDOW_UPDATE Frame으로 윈도우를 채우게 된다.

  • 즉, 커넥션 전체 전송량을 조절하여 과한 요청이 가는 흐름 제어를 하는 것.
  • 커넥션 내 스트림 별로 자원을 독식하지 못하게 흐름 제어하는 것.

 

이 흐름 제어에 핵심 설계 원칙이 있다.

  • 흐름 제어는 DATA Frame에만 적용되고, HEADERS Frame에는 적용되지 않는다.

HEADERS Frame은 새로운 Stream을 여는 행위 자체이기 때문에, 이를 흐름 제어 대상에 포함시키면 새 요청 자체가 차단되는 문제가 발생한다. 기존 Stream이 윈도우를 소진했을 때 새 요청조차 보낼 수 없게 되면, 커넥션의 자원 독점과 교착 상태(Deadlock)로 이어질 수 있다.

즉, "데이터의 양은 조절하되, 의사소통 자체는 절대 막지 않는다" 는 원칙이다.

6. HTTP/2의 치명적 한계: TCP-레벨 HOL Blocking

HTTP/2는 Application Layer에서 스트림을 통한 Multiplexing으로 HOL Blocking을 해결했다. 하지만 TCP 레이어에서 고려해야할 문제가 또 있다.

 

HTTP/2의 모든 Stream은 하나의 TCP 커넥션 위에서 흐른다.

  • TCP는 바이트 스트림을 순서대로 전달하는 프로토콜이다.
  • TCP 입장에서 Stream이라는 개념은 존재하지 않는다.
TCP 위의 Frame 전송:
[S1:DATA] [S2:DATA] [S3:DATA] [S1:DATA] [S2:DATA]
    ↑
  이 패킷 유실

 

스트림으로 전달하는 상황에서 패킷이 유실된 경우를 봐보자. 이때 HTTP/2 TCP는 유실된 패킷이 재전송되어 도착할 때까지 그 뒤의 모든 바이트 전달을 멈춘다. Stream 1의 패킷이 유실되었을 뿐인데, Stream 2와 Stream 3까지 블로킹된다.

 

이게 TCP-레벨 HOL Blocking 문제이다. HTTP/2가 커넥션을 1개로 통합했기 때문에, 패킷 유실률이 2%만 넘어도 6개 커넥션을 분산 사용하는 HTTP/1.1보다 오히려 느려질 수 있다. 이 한계가 HTTP/3의 등장 배경이다.

심화: HTTP/2에서의 다중 커넥션을 사용하는건 어떨까?

더보기

HTTP/1.1처럼 커넥션을 여러 개 열면 TCP-레벨 HOL Blocking을 분산시킬 수 있지 않을까?

  • 결론부터 말하면, 스펙상 금지는 아니지만 권장하지 않는다.

HTTP/2에서는 클라이언트가 특정 서버에 대해 "하나의 HTTP/2 커넥션만 열어야 한다"고 규정한다. 단, MUST가 아니므로 기술적으로 여러 개를 열 수 있고, 브라우저도 TLS 인증서가 다르거나 기존 커넥션이 비정상적일 때 등 예외적으로 2개 이상을 열기도 한다. 하지만 의도적으로 다중 커넥션을 사용하면 HTTP/2의 핵심 이점이 훼손된다.

 

[단점]

  • 멀티플렉싱 대상이 분산됨 — 하나의 커넥션에 모든 Stream이 있어야 스케줄링 효율이 극대화된다.
  • HPACK 동적 테이블(61개 헤더)이 커넥션마다 분리됨 — 헤더 압축 효율이 저하된다. 동적 테이블은 커넥션 단위로 유지되므로, 커넥션이 나뉘면 학습된 헤더 정보도 나뉜다.
  • Slow Start를 커넥션마다 따로 겪음 — TCP 혼잡 제어의 초기 속도 저하가 커넥션 수만큼 반복된다.

[유일한 장점]

  • 커넥션이 분산되므로 패킷 유실 시 영향 범위가 줄어든다.
    하지만 이는 HTTP/2의 설계 의도를 거스르는 판단이다. (커넥션 부하로 인해 하나로 묶고 스트림으로 처리하고자 한것인데)

TCP-레벨 HOL Blocking을 프로토콜 차원에서 근본적으로 해결한 것은 HTTP/3(QUIC)이다.

심화: TCP 재전송 메커니즘의 이해

더보기

HTTP/3의 QUIC가 왜 TCP를 대체했는지를 깊이 이해하려면, TCP가 패킷 유실을 어떻게 감지하고 재전송하는지를 알아야 한다. TCP의 재전송은 네 가지 메커니즘이 계층적으로 협력하는 구조이다.

타임아웃 기반 재전송 (RTO Retransmission)

가장 기본적인 메커니즘이다. 세그먼트를 보낸 후 RTO(Retransmission Timeout) 시간 내에 ACK가 오지 않으면 재전송한다.

송신 →  [SEQ=1000]  → (유실 ❌)
         ⏳ RTO 대기... (수백 ms ~ 수 초)
         ⏳ 타임아웃!

송신 →  [SEQ=1000]  → 수신 (재전송)

이때 RTO는 고정값이 아니라 RTT 측정값을 기반으로 동적 계산된다.

SRTT = Smoothed RTT (평활화된 RTT)
RTTVAR = RTT 변동폭
RTO = SRTT + 4 × RTTVAR

RTT가 안정적이면 RTO가 짧아지고, 변동이 크면 RTO가 길어진다. 보통 수백 ms~수 초로 설정되므로, 유실 감지까지 시간이 오래 걸린다는 한계가 있다. 이 한계를 보완하기 위해 이후 메커니즘들이 추가되었다.

Fast Retransmit (빠른 재전송)

중복 ACK(Duplicate ACK)가 3개 쌓이면, 타임아웃을 기다리지 않고 즉시 재전송한다.

송신 →  [SEQ=1000]  → 수신 ✅
송신 →  [SEQ=1100]  → ❌ 유실
송신 →  [SEQ=1200]  → 수신 ✅
송신 →  [SEQ=1300]  → 수신 ✅
송신 →  [SEQ=1400]  → 수신 ✅

수신: 1200·1300·1400은 도착했지만 1100이 빠짐
  ← ACK=1100  (중복 1)
  ← ACK=1100  (중복 2)
  ← ACK=1100  (중복 3)

송신: 중복 ACK 3개 → 즉시 재전송!
송신 →  [SEQ=1100]  → 수신 ✅

중복 ACK 임계값이 1이 아니라 3인 이유는, 네트워크에서 패킷이 서로 다른 경로를 타면서 순서가 바뀌는 재정렬(Reordering) 이 발생할 수 있기 때문이다. 재정렬 시에도 중복 ACK가 1~2개 발생할 수 있으므로, 이를 유실로 오판하지 않기 위해 경험적으로 3개를 임계값으로 설정한 것이다.

SACK (Selective Acknowledgment)

TCP의 기본 ACK는 누적 ACK(Cumulative ACK) 이다. "여기까지 받았다"만 알려줄 뿐, 그 뒤에 어떤 패킷이 도착했는지는 알려주지 않는다.

[누적 ACK만 있는 경우]
  수신 상태: 1✅ 2✅ 3❌ 4✅ 5❌ 6✅
  ACK=3 ("3번부터 보내줘")
  → 송신 측은 3이 빠진 것만 알고, 4·5·6 상태를 모름
  → 최악의 경우: 3~6을 전부 재전송 (불필요한 재전송)

 

SACK은 수신 측이 "어디어디를 받았다"를 구체적으로 알려주는 TCP 옵션이다.

[SACK 활성화된 경우]
  수신 상태: 1✅ 2✅ 3❌ 4✅ 5❌ 6✅
  ACK=3, SACK=[4-4, 6-6]
  → "3과 5만 빠졌구나" → 3과 5만 재전송

SACK는 TCP의 선택적 옵션이며 헤더 공간 제한으로 최대 4개 블록까지만 표현 가능하다. QUIC의 ACK Range는 이 SACK의 개념을 기본 스펙으로 흡수하면서 공간 제한도 크게 완화한 것이다.

Tail Loss Probe (TLP)

앞의 메커니즘들로도 해결이 어려운 특수한 케이스가 있다. 커넥션에서 마지막으로 보낸 패킷이 유실된 경우이다.

송신 →  [SEQ=1000]  → 수신 ✅
송신 →  [SEQ=1100]  → 수신 ✅
송신 →  [SEQ=1200]  → ❌ 유실 (마지막 전송)

수신: 더 이상 받을 패킷이 없으므로 중복 ACK를 보낼 계기가 없음
송신: 중복 ACK가 안 오니 Fast Retransmit 발동 불가
      → RTO 타임아웃(수백 ms~수 초)까지 기다려야 함

마지막 패킷 뒤에 후속 패킷이 없으므로, 수신 측에서 중복 ACK가 발생하지 않는다. 따라서 Fast Retransmit이 작동할 수 없다.

TLP는 RTO 발동 전에 Probe 패킷을 먼저 보내 수신 측의 ACK를 유도한다.

송신 →  [SEQ=1200]  → ❌ 유실
         ⏳ PTO(Probe Timeout, RTO보다 짧음) 대기...

송신 →  [Probe 패킷]  → 수신
         (마지막 데이터 또는 새 데이터를 전송하여 ACK 유도)

수신 ←  ACK=1200  ("1200번부터 보내줘")
송신: "1200이 유실됐구나" → 재전송

재전송 메커니즘의 계층 구조

네 가지 메커니즘은 서로 대체하는 것이 아니라, 상황에 따라 단계적으로 발동한다.

패킷 유실 발생!

① Fast Retransmit (가장 빠름, 수 ms ~ 수십 ms)
   조건: 중복 ACK 3개
   한계: 마지막 패킷 유실 시 발동 불가

② TLP — Tail Loss Probe (수십 ms ~ 수백 ms)
   조건: 일정 시간 ACK 없음 (PTO)
   한계: Probe 응답이 없으면 RTO로 넘어감

③ RTO — 타임아웃 재전송 (최후의 보루, 수백 ms ~ 수 초)
   조건: ACK 타임아웃
   특징: 느리지만 가장 확실한 안전망

+ SACK (보조 메커니즘)
  역할: 위 메커니즘들에서 "어떤 패킷을 재전송할지" 정밀 판단 보조

이 모든 메커니즘이 OS 커널에 구현되어 있다. QUIC는 이 개념들을 유저 스페이스에서 재구현하면서, Packet Number 단조 증가로 재전송 모호성을 제거하고 ACK Range를 기본 탑재하여 재전송 판단의 정밀도를 높였다.


4. HTTP/3 — 전송 계층 프로토콜 교체

1. TCP를 버리고 UDP로

HTTP/1.1은 다중 커넥션으로, HTTP/2는 스트림 기반 Multiplexing으로 HOL Blocking에 대응했다.

하지만 근본 원인은 TCP가 바이트 스트림을 순서대로만 전달하는 프로토콜이라는 점이다. 이 한계는 Application Layer에서 아무리 고쳐도 해결할 수 없다. HTTP/3는 TCP 자체를 버리고 QUIC이라는 새로운 전송 프로토콜 위에서 동작한다.

2. 왜 UDP일까?

새로운 전송 프로토콜을 만든다면 커널에 구현해야 하고, 전 세계 OS와 중간 네트워크 장비를 업데이트해야 한다. 현실적으로 불가능하다.

QUIC는 UDP 위에서 동작하는 전략을 택했다.

UDP는 거의 모든 네트워크 장비가 이미 통과시켜주고, 애플리케이션 레벨에서 구현할 수 있다. QUIC는 UDP라는 "최소한의 껍데기"만 빌리고, 그 위에 신뢰성·순서 보장·혼잡 제어 등 TCP가 하던 일을 직접 구현한 것이다.

HTTP/2 스택:             HTTP/3 스택:
┌──────────┐            ┌──────────┐
│  HTTP/2  │            │  HTTP/3  │
├──────────┤            ├──────────┤
│   TLS    │            │   QUIC   │  ← TCP + TLS를 하나로
├──────────┤            ├──────────┤
│   TCP    │            │   UDP    │  ← 최소한의 전송 껍데기
├──────────┤            ├──────────┤
│    IP    │            │    IP    │
└──────────┘            └──────────┘
  • 특징. HTTP/2에서 별도 계층이던 TCP와 TLS가, QUIC에서는 하나로 통합되었다.

3. 스트림 인식 — TCP 레벨 HOL Blocking 해결

QUIC는 Transport Layer에서 Stream을 직접 인식한다. TCP가 Stream을 인식하지 못하고 바이트스트림으로만 인식했던것과 결정적인 차이이다.

[HTTP/2 + TCP]
TCP가 보는 것:  [바이트][바이트][바이트][바이트]...
→ 하나의 연속된 흐름. 중간이 빠지면 전부 대기.
→ 어느 바이트가 어디 스트림인지 구분할수 가 없음.

[HTTP/3 + QUIC]
QUIC가 보는 것: [S1:DATA] [S2:DATA] [S3:DATA]
→ Stream별 독립적 흐름. S1이 빠져도 S2, S3는 계속 진행.

 

패킷 유실 시 동작을 비교하면 차이가 명확하다.

HTTP/2 + TCP:
  S1 패킷 유실 → TCP 재전송 대기 → S2, S3 전부 블로킹 ❌

HTTP/3 + QUIC:
  S1 패킷 유실 → S1만 재전송 대기
                → S2 계속 수신 ✅
                → S3 계속 수신 ✅

4. QUIC 패킷 구조 

TCP Segment와 QUIC Packet을 나란히 놓으면 설계의 차이가 보인다.

[TCP Segment]
┌──────────────────────────────┐
│ TCP Header                   │
│  - Source/Dest Port          │
│  - Sequence Number           │
│  - ACK Number                │
│  - Window Size               │
├──────────────────────────────┤
│ Payload (바이트 스트림)         │
│  → Stream 구분 없음            │
└──────────────────────────────┘

[QUIC Packet]
┌──────────────────────────────┐
│ QUIC Header                  │
│  - Connection ID             │
│  - Packet Number             │
├──────────────────────────────┤
│ Frame 1: STREAM              │
│  - Stream ID: 1              │
│  - Offset: 0                 │
│  - Data: ...                 │
├──────────────────────────────┤
│ Frame 2: STREAM              │
│  - Stream ID: 3              │
│  - Offset: 500               │
│  - Data: ...                 │
├──────────────────────────────┤
│ Frame 3: ACK                 │
│  - 수신 확인 정보               │
└──────────────────────────────┘

 

TCP vs. QUIC 비교

  1. Connection ID - IP/Port 조합이 아니라 이 ID로 커넥션을 식별한다. 네트워크 전환(Wi-Fi → LTE)으로 IP가 바뀌어도 커넥션이 유지된다.
    (길에서 와이파이에서 LTE로 변환될 때 끊기는 현상이 바로 이것 때문이다. IP/Port 가 바뀌면서 TCP 커넥션을 새로 맺어야 하기 때문이다.)
  2. Stream ID + Offset - 하나의 패킷 안에 여러 Stream의 Frame이 공존하며, 각 Frame이 자신이 속한 Stream과 해당 Stream 내 위치(Offset)를 명시한다. TCP는 하나의 연속된 바이트 스트림이므로 이런 구분이 불가능하다.
  3. Packet Number의 순 증가. - TCP의 Sequence Number는 재전송 시 같은 번호를 재사용한다. 이로인해 ACK가 돌아왔을 때 원본에 대한 것인지 재전송에 대한 것인지 구분할 수 없다.
    반면에 QUIC의 Packet Number는
    절대 재사용되지 않는다. 재전송에도 새 번호를 부여한다. 데이터의 위치는 Stream Offset으로 식별하므로 수신 측이 동일 데이터의 재전송임을 알 수 있고, 동시에 어떤 패킷에 대한 ACK인지를 정확히 구분할 수 있다.
[TCP 재전송]
  원본:   SEQ=1100, 데이터 A
  재전송: SEQ=1100, 데이터 A   ← 같은 번호
  → ACK=1200이 원본에 대한 응답인지 재전송에 대한 응답인지 불분명 (RTT 측정 부정확)

[QUIC 재전송]
  원본:   Packet 10, [Stream 1, Offset 100, 데이터 A]
  재전송: Packet 25, [Stream 1, Offset 100, 데이터 A]   ← 새 번호
  → Packet 25에 대한 ACK → 재전송의 RTT를 정확히 측정 가능

5. QUIC 혼잡 제어

QUIC의 혼잡 제어는 TCP의 개념을 따른다. 기본 알고리즘으로 Slow Start, Congestion Avoidance, Recovery Period를 규정하고 있고, TCP의 구조와 유사하다.

 

개념은 비슷하지만 정밀도가 다르다. Packet Number의 순증가 덕분에 재전송 모호성이 없고, RTT를 매우 정확하게 측정할 수 있다. 이를 통해 혼잡 상태를 정밀하게 판단하여 불필요한 전송 속도 감소를 줄이고 ACK 방식도 개선되었다.

[TCP — 누적 ACK(Cumulative ACK)]
  패킷 상태: 1✅ 2✅ 3❌ 4✅ 5✅ 6✅
  ACK=3 ("3번부터 보내줘")
  → 4·5·6이 도착했는지 알 수 없음

[QUIC — ACK Range]
  패킷 상태: 1✅ 2✅ 3❌ 4✅ 5✅ 6✅
  ACK Frame: [1-2 수신, 4-6 수신]
  → "3만 빠졌구나" 정확히 파악 → 3만 재전송

 

추가로 TCP와 QUIC의 구현에 있어 전략적인 차이가 있다.

- TCP의 혼잡 제어는 OS 커널에 구현되어 있어 알고리즘 변경에 OS 업데이트가 필요하다.

- 반면에, QUIC는 User Space(애플리케이션)에서 동작하므로, 애플리케이션 업데이트만으로 새 알고리즘을 적용할 수 있다.
Google은 Chrome 업데이트 한 번으로 전 세계에 새 혼잡 제어 알고리즘을 배포하고 테스트할 수 있다.

6. 핸드셰이크 혁신: 1-RTT와 0-RTT

1-RTT — 최초 연결

TCP + TLS 1.3은 TCP Handshake에 1 RTT, TLS Handshake에 1 RTT로 총 2 RTT가 필요하다. QUIC는 전송 핸드셰이크와 TLS 핸드셰이크를 하나로 통합하여 1 RTT만에 연결을 수립한다.

[TCP + TLS 1.3 — 2 RTT]
  RTT 1: SYN → SYN-ACK → ACK          (TCP 연결)
  RTT 2: ClientHello → ServerHello     (TLS 연결)
  → 첫 데이터 전송

[QUIC — 1 RTT]
  RTT 1: Initial Packet (전송+TLS 통합 핸드셰이크)
  → 첫 데이터 전송

서울에서 미국 서버까지 RTT가 150ms라면, 이것만으로 150ms를 절약하는 셈이다.

0-RTT — 재연결

한번 연결했던 서버에 다시 접속할 때 더 극적이다. 클라이언트가 이전 연결에서 받은 Session Ticket 또는 Pre-Shared Key를 로컬에 저장해뒀다가, 재연결 시 첫 패킷에 암호화된 요청 데이터를 바로 실어 보낸다. 핸드셰이크 완료를 기다리지 않으므로 0-RTT에 데이터 전송이 시작된다.

0-RTT의 보안 취약점: Replay Attack

다만, 0-RTT에는 보안 취약점이 존재한다. 공격자가 0-RTT의 첫 패킷을 암호 해독 없이 그대로 복사하여 서버에 재전송하면, 서버는 이를 정상 요청으로 인식하여 처리할 수 있다.

정상 사용자:  [암호화된 "POST /payment {amount:100000}"]  → 서버
공격자:      [동일 패킷 복사]                             → 서버 (결제 2회 실행)

 

공격자는 응답을 복호화할 수 없지만, 요청이 서버에서 재실행되는 것 자체가 문제이다.

이 때문에 0-RTT에는 멱등한 요청만 허용하는 것이 원칙이다. GET처럼 동일 요청을 두 번 해도 결과가 같은 요청은 0-RTT에 실을 수 있지만, POST처럼 중복 실행 시 사이드 이펙트가 있는 요청은 1-RTT Handshake가 완료된 후에만 전송해야 한다.

7. 네트워크 전환에도 끊기지 않는 연결

TCP 커넥션은 (Source IP, Source Port, Dest IP, Dest Port) 4-Tuple로 식별된다. Wi-Fi에서 LTE로 전환되면 클라이언트의 IP가 바뀌므로 TCP 커넥션이 끊어지고 새로 연결해야 한다.

QUIC 커넥션은 Connection ID로 식별한다. IP/Port가 바뀌어도 Connection ID가 동일하면 같은 커넥션으로 인식한다. 네트워크가 전환되어도 TCP 핸드셰이크 없이 데이터 전송이 이어진다.


5. 전체 비교

  HTTP/1.1 HTTP/2 HTTP/3
전송 계층 TCP TCP QUIC (UDP 기반)
TLS 별도 계층 (선택) 별도 계층 (사실상 필수) QUIC에 내장 (필수)
커넥션 모델 도메인당 최대 6개 TCP 커넥션 도메인당 TCP 커넥션 1개 도메인당 QUIC 커넥션 1개
멀티플렉싱 없음 (순차 처리) Stream 기반 멀티플렉싱 Stream 기반 멀티플렉싱
HOL Blocking Application 레벨 ❌ App 레벨 ✅ / TCP 레벨 ❌ (재전송) App 레벨 ✅ / Transport 레벨 ✅
헤더 처리 평문 텍스트, 중복 전송 HPACK (정적+동적 테이블) QPACK (HPACK의 QUIC 적응)
핸드셰이크 TCP 1 RTT + TLS 1~2 RTT TCP 1 RTT + TLS 1 RTT = 2 RTT 최초 1 RTT / 재연결 0 RTT
Server Push 없음 지원 (하지만 사실상 폐기) 지원하지 않음
흐름 제어 TCP 커넥션 단위 TCP + Stream별 이중 구조 QUIC Stream별 독립 제어
네트워크 전환 커넥션 끊김 (IP 기반 식별) 커넥션 끊김 (IP 기반 식별) Connection ID로 유지
혼잡 제어 진화 OS 커널 의존 (느린 개선) OS 커널 의존 (느린 개선) User Space (빠른 개선)
반응형
저작자표시 비영리 변경금지 (새창열림)

'기술 학습' 카테고리의 다른 글

Redis Cluster _ Lettuce Client의 Topology Refresh  (1) 2026.04.25
분산 시스템 CAP 정리  (0) 2026.03.31
MSA에서 CORS 문제를 해결하는 4가지 전략  (0) 2026.03.27
TCP/IP 체크섬(Checksum) 내부 동작 원리  (0) 2026.03.23
Java 기반 동기/비동기, 블로킹/논블로킹 정리  (0) 2026.03.23
'기술 학습' 카테고리의 다른 글
  • Redis Cluster _ Lettuce Client의 Topology Refresh
  • 분산 시스템 CAP 정리
  • MSA에서 CORS 문제를 해결하는 4가지 전략
  • TCP/IP 체크섬(Checksum) 내부 동작 원리
구름뭉치
구름뭉치
구름의 개발일기장
  • 구름뭉치
    구름 개발일기장
    구름뭉치
  • 전체
    오늘
    어제
    • ALL (294)
      • 프로젝트 (23)
        • 토스페이먼츠 PG 연동 시리즈 (12)
        • JWT 방식 인증&인가 시리즈 (6)
        • 스우미 웹 애플리케이션 프로젝트 (1)
        • 스프링부트 기본 보일러 플레이트 구축 시리즈 (2)
        • 마이크로서비스 아키텍쳐 시리즈 (1)
      • 스프링 (43)
        • 스프링부트 API 설계 정리 (8)
        • 스프링부트 RestAPI 프로젝트 (18)
        • 스프링부트 WebSocket 적용기 (3)
        • 스프링 JPA 정리 시리즈 (5)
        • 스프링 MVC (5)
        • 스프링 배치 (2)
        • 토비의 스프링 정리 (2)
      • 기술 학습 (10)
        • 아파치 카프카 (9)
        • 클린 코드 (4)
        • 디자인 패턴의 아름다움 (2)
        • 모던 자바 인 액션 (7)
        • JVM 스레드 딥다이브 (7)
      • Web (25)
        • 정리글 (20)
        • GraphQL 정리글 (2)
        • Jenkins 정리글 (3)
      • 취업 (6)
      • CS (77)
        • 네트워크 전공 수업 정리 (11)
        • OSI 7계층 정리 (12)
        • 운영체제 정리 (19)
        • 데이터베이스 정리 (5)
        • MySql 정리 (17)
        • GoF의 Design Pattern 정리 (12)
      • 알고리즘 (70)
        • 백준 (56)
        • 프로그래머스 (12)
        • 알고리즘 정리본 (1)
      • 기초 지식 정리 (2)
      • 일상 (8)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    부다페스트
    마우스
    마우스 패드
    mx master s3 for mac
    크로아티아
    레이저
    동유럽
    키보드 손목 받침대
    류블라냐
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
구름뭉치
HTTP/1.1, HTTP/2, HTTP/3 프로토콜 비교 정리
상단으로

티스토리툴바