HTTP·2 — 멀티플렉싱
브라우저에서 웹페이지 하나를 열면 수십 개의 리소스(HTML, CSS, JS, 이미지)를 동시에 받아와야 한다. HTTP/1.1에서는 이걸 어떻게 처리했고, 왜 HTTP/2가 필요해졌을까?
HTTP/1.1의 한계 — 왜 HTTP/2가 필요한가
HTTP/1.1에서 하나의 TCP 연결은 한 번에 하나의 요청-응답만 처리할 수 있습니다. 응답이 올 때까지 다음 요청을 보내지 못하는 이 문제를 Head-of-Line(HOL) Blocking 이라고 합니다.
브라우저들은 이 문제를 우회하기 위해 도메인당 6~8개의 TCP 연결 을 동시에 맺습니다. 하지만 이 방식에는 근본적인 한계가 있습니다.
- **TCP 핸드셰이크 오버헤드 **: 연결마다 3-way 핸드셰이크 + TLS 핸드셰이크가 필요합니다
- ** 커넥션 수 제한 **: 브라우저가 열 수 있는 연결 수에 한계가 있습니다
- ** 텍스트 기반 프로토콜 **: 헤더가 텍스트이므로 파싱 비용이 크고, 매 요청마다 중복된 헤더(Cookie, User-Agent 등)를 반복해서 보냅니다
- ** 서버에서 클라이언트로 먼저 보낼 수 없음 **: 클라이언트가 요청해야만 응답할 수 있습니다
HTTP/2는 이 모든 문제를 ** 하나의 TCP 연결** 위에서 해결합니다.
HTTP/2의 핵심 개념 네 가지
1. 바이너리 프레이밍 (Binary Framing)
HTTP/1.1은 텍스트 기반입니다. GET /index.html HTTP/1.1\r\n처럼 사람이 읽을 수 있는 형태죠. HTTP/2는 이를 ** 바이너리 프레임 **으로 바꿨습니다.
HTTP/1.1 (텍스트) HTTP/2 (바이너리)
+-----------------------+ +--------+--------+
| GET /index.html | | Length | Type |
| Host: example.com | → | Flags | Stream |
| Accept: text/html | | ID | Payload|
+-----------------------+ +--------+--------+
프레임의 구조는 고정된 9바이트 헤더로 시작합니다.
| 필드 | 크기 | 설명 |
|---|---|---|
| Length | 3바이트 | 페이로드 길이 (최대 16,384바이트) |
| Type | 1바이트 | 프레임 타입 (HEADERS, DATA, SETTINGS 등) |
| Flags | 1바이트 | 프레임별 플래그 (END_STREAM, END_HEADERS 등) |
| Reserved | 1비트 | 예약 비트 |
| Stream ID | 31비트 | 이 프레임이 속한 스트림 식별자 |
바이너리라서 파싱이 훨씬 빠르고, 필드 경계가 명확해서 오류가 줄어듭니다.
2. 멀티플렉싱 (Multiplexing)
HTTP/2의 가장 핵심적인 변화입니다. ** 하나의 TCP 연결에서 여러 요청/응답을 동시에** 주고받을 수 있습니다.
HTTP/1.1 — 요청이 순서대로 기다림 (HOL Blocking)
연결 1: [요청A] ────→ [응답A] ────→ [요청B] ────→ [응답B]
연결 2: [요청C] ────→ [응답C] ────→ [요청D] ────→ [응답D]
HTTP/2 — 하나의 연결에서 동시에 처리 (멀티플렉싱)
연결 1: [A 헤더][C 데이터][B 헤더][A 데이터][B 데이터][C 헤더]
← 프레임 단위로 섞여서 전송 →
이게 가능한 이유는 ** 스트림(Stream)** 개념 덕분입니다. 각 요청-응답 쌍이 고유한 Stream ID를 가지고, 프레임에 Stream ID가 포함되어 있어서 수신 측에서 프레임을 올바른 스트림에 재조립할 수 있습니다.
- ** 스트림 **: 하나의 요청-응답을 나타내는 양방향 프레임 흐름
- ** 홀수 Stream ID**: 클라이언트가 시작한 스트림
- ** 짝수 Stream ID**: 서버가 시작한 스트림 (서버 푸시)
- Stream 0: 연결 전체에 대한 제어 프레임 (SETTINGS, PING 등)
3. 헤더 압축 — HPACK
HTTP/1.1에서는 매 요청마다 Cookie, User-Agent 같은 동일한 헤더를 반복해서 보냅니다. 쿠키가 크면 헤더만 수 KB가 됩니다.
HPACK은 두 가지 기법으로 헤더를 압축합니다.
- ** 정적 테이블 **: 자주 쓰이는 헤더 61개를 인덱스로 매핑 (
:method: GET→ 인덱스 2) - ** 동적 테이블 **: 연결 동안 주고받은 헤더를 기억하여 이후 요청에서 인덱스로 참조
첫 번째 요청: content-type: application/json → 전체 전송 + 동적 테이블에 저장
두 번째 요청: content-type: application/json → 인덱스 번호 하나만 전송
같은 연결에서 요청을 반복할수록 헤더 크기가 줄어드는 구조입니다.
4. 서버 푸시 (Server Push)
서버가 클라이언트의 요청 없이도 리소스를 미리 보낼 수 있는 기능입니다. HTML을 요청하면 서버가 CSS와 JS를 함께 밀어 넣는 식이죠.
클라이언트: GET /index.html
서버: PUSH_PROMISE (style.css를 보내겠다)
HEADERS + DATA (index.html 응답)
HEADERS + DATA (style.css 응답) ← 클라이언트가 요청하기 전에 도착
다만 실무에서는 ** 서버 푸시를 잘 쓰지 않는 추세 **입니다. 클라이언트 캐시에 이미 있는 리소스를 중복 전송하는 문제가 있고, 브라우저 호환성도 일관적이지 않아서 Chrome은 서버 푸시 지원을 제거했습니다.
스트림의 생명주기
스트림은 생성부터 종료까지 상태를 가집니다. 이 상태 흐름을 알아야 HTTP/2 에러를 디버깅할 수 있습니다.
+--------+
send | | recv
HEADERS | idle | HEADERS
| |
+--------+
/ \
/ \
v v
+--------+ +--------+
send | open | | open | recv
DATA | (half | | (half | DATA
| closed)| | closed)|
+--------+ +--------+
\ /
\ /
v v
+--------+
| closed |
+--------+
주요 상태를 정리하면 다음과 같습니다.
| 상태 | 설명 |
|---|---|
| idle | 스트림이 아직 사용되지 않은 초기 상태 |
| open | 양쪽 모두 프레임을 보내고 받을 수 있는 상태 |
| half-closed (local) | 로컬에서 END_STREAM을 보냄. 받기만 가능 |
| half-closed (remote) | 원격에서 END_STREAM을 보냄. 보내기만 가능 |
| closed | 양쪽 모두 END_STREAM을 보냈거나 RST_STREAM으로 즉시 종료 |
Netty의 HTTP/2 지원 구조
네티는 HTTP/2를 두 가지 핵심 핸들러로 지원합니다.
[SslHandler] ← TLS + ALPN 프로토콜 협상
↓
[Http2FrameCodec] ← HTTP/2 프레임 인코딩/디코딩
↓
[Http2MultiplexHandler] ← 스트림별 자식 Channel 생성
↓
[Http2StreamChannel] ← 스트림 #1, #3, #5, ...
└── 각각 독립된 ChannelPipeline
Http2FrameCodec — 프레임 처리의 핵심
Http2FrameCodec은 HTTP/2 바이너리 프레임을 인코딩/디코딩하는 코덱입니다. TCP 바이트 스트림을 받아서 네티의 Http2Frame 객체로 변환하고, 반대 방향으로는 Http2Frame을 바이트로 직렬화합니다.
처리하는 주요 프레임 타입은 다음과 같습니다.
| 프레임 타입 | 네티 클래스 | 역할 |
|---|---|---|
| SETTINGS | Http2SettingsFrame | 연결 설정 (최대 스트림 수, 윈도우 크기 등) |
| HEADERS | Http2HeadersFrame | 요청/응답 헤더 전송 |
| DATA | Http2DataFrame | 바디 데이터 전송 |
| RST_STREAM | Http2ResetFrame | 스트림 강제 종료 |
| GOAWAY | Http2GoAwayFrame | 연결 종료 통보 |
| WINDOW_UPDATE | Http2WindowUpdateFrame | 흐름 제어 윈도우 갱신 |
// Http2FrameCodec 빌더로 생성 — 서버용
Http2FrameCodec frameCodec = Http2FrameCodecBuilder.forServer()
.initialSettings(Http2Settings.defaultSettings()
.maxConcurrentStreams(100) // 동시 스트림 최대 100개
.initialWindowSize(65535)) // 초기 흐름 제어 윈도우 크기
.build();
빌더 패턴으로 SETTINGS 프레임의 초기값을 설정할 수 있습니다. maxConcurrentStreams는 한 연결에서 동시에 열 수 있는 스트림 수를 제한합니다.
Http2MultiplexHandler — 스트림을 Channel로 분리
Http2MultiplexHandler는 HTTP/2의 각 스트림을 ** 독립된 자식 Channel(Http2StreamChannel)로** 생성합니다. 이게 네티 HTTP/2 프로그래밍의 핵심 아이디어입니다.
// 각 스트림이 생성될 때 실행되는 초기화 핸들러
Http2MultiplexHandler multiplexHandler = new Http2MultiplexHandler(
new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 각 스트림에 독립된 핸들러를 구성할 수 있다
pipeline.addLast(new Http2StreamHandler());
}
}
);
이렇게 하면 각 스트림이 별도의 ChannelPipeline을 가지게 됩니다.
- ** 스트림 독립성 **: 스트림 #1의 처리가 느려도 스트림 #3에 영향을 주지 않습니다
- ** 기존 모델 재사용 **:
SimpleChannelInboundHandler같은 기존 핸들러를 스트림 단위로 그대로 쓸 수 있습니다 - ** 자원 격리 **: 스트림별로 메모리 할당과 해제가 독립적입니다
HTTP/2 서버 구현
전체적인 서버 구현 코드를 살펴봅니다.
스트림 핸들러
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http2.*;
import io.netty.util.CharsetUtil;
// 각 HTTP/2 스트림에서 요청을 처리하는 핸들러
public class Http2StreamHandler extends SimpleChannelInboundHandler<Http2Frame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Http2Frame frame) {
if (frame instanceof Http2HeadersFrame) {
Http2HeadersFrame headersFrame = (Http2HeadersFrame) frame;
handleHeaders(ctx, headersFrame);
} else if (frame instanceof Http2DataFrame) {
Http2DataFrame dataFrame = (Http2DataFrame) frame;
handleData(ctx, dataFrame);
}
}
private void handleHeaders(ChannelHandlerContext ctx,
Http2HeadersFrame headersFrame) {
Http2Headers headers = headersFrame.headers();
String method = headers.method() != null ? headers.method().toString() : "";
String path = headers.path() != null ? headers.path().toString() : "";
System.out.println("요청 수신: " + method + " " + path);
// GET 요청이면서 END_STREAM이면 바로 응답
if (headersFrame.isEndStream()) {
sendResponse(ctx, "Hello, HTTP/2!");
}
}
private void handleData(ChannelHandlerContext ctx,
Http2DataFrame dataFrame) {
// POST 바디 등 데이터 프레임 처리
ByteBuf content = dataFrame.content();
System.out.println("데이터 수신: " + content.toString(CharsetUtil.UTF_8));
if (dataFrame.isEndStream()) {
sendResponse(ctx, "데이터를 받았습니다");
}
}
private void sendResponse(ChannelHandlerContext ctx, String body) {
// 1. 응답 헤더 전송
Http2Headers responseHeaders = new DefaultHttp2Headers()
.status("200")
.add("content-type", "text/plain; charset=utf-8");
ctx.write(new DefaultHttp2HeadersFrame(responseHeaders));
// 2. 응답 바디 전송 (END_STREAM 플래그로 스트림 종료)
ByteBuf responseBody = Unpooled.copiedBuffer(body, CharsetUtil.UTF_8);
ctx.writeAndFlush(new DefaultHttp2DataFrame(responseBody, true));
}
}
요청/응답 모두 프레임 단위로 처리한다는 점이 HTTP/1.1과 다릅니다. HEADERS 프레임에서 메서드와 경로를 읽고, DATA 프레임에서 바디를 읽습니다. 응답도 HEADERS 프레임과 DATA 프레임을 나눠서 보냅니다.
isEndStream()체크가 중요합니다. 이 플래그가true여야 요청이 완전히 도착한 것입니다. POST 요청처럼 바디가 있으면 HEADERS 프레임의isEndStream()은false이고, 마지막 DATA 프레임에서true가 됩니다.
서버 부트스트랩
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http2.*;
import io.netty.handler.ssl.*;
import io.netty.handler.ssl.util.SelfSignedCertificate;
public class Http2Server {
private static final int PORT = 8443;
public static void main(String[] args) throws Exception {
// 자체 서명 인증서 생성 (개발용)
SelfSignedCertificate ssc = new SelfSignedCertificate();
// HTTP/2는 TLS + ALPN이 필요하다
SslContext sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
.applicationProtocolConfig(new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2 // "h2"를 ALPN에 등록
))
.build();
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 1. TLS 핸들러 — ALPN 프로토콜 협상 포함
pipeline.addLast("ssl", sslCtx.newHandler(ch.alloc()));
// 2. HTTP/2 프레임 코덱
pipeline.addLast("frameCodec",
Http2FrameCodecBuilder.forServer().build());
// 3. 멀티플렉스 핸들러 — 스트림별 자식 Channel 생성
pipeline.addLast("multiplexHandler",
new Http2MultiplexHandler(
new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(
new Http2StreamHandler());
}
}
));
}
});
ChannelFuture future = bootstrap.bind(PORT).sync();
System.out.println("HTTP/2 서버 시작 — 포트: " + PORT);
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
파이프라인 순서가 중요합니다. SslHandler → Http2FrameCodec → Http2MultiplexHandler 순서로 배치해야 합니다. SslHandler가 TLS 복호화를 먼저 하고, Http2FrameCodec이 바이너리 프레임을 파싱하고, Http2MultiplexHandler가 스트림을 분리합니다.
ALPN — 프로토콜 협상
ALPN이 필요한 이유
클라이언트가 서버에 연결할 때, ** 이 서버가 HTTP/2를 지원하는지 어떻게 알 수 있을까요?** 먼저 HTTP/1.1로 연결하고 Upgrade 헤더로 HTTP/2를 시도하는 방법도 있지만, 이러면 왕복이 한 번 더 필요합니다.
ALPN(Application-Layer Protocol Negotiation)은 TLS 핸드셰이크 안에서 프로토콜을 협상합니다. 추가 왕복 없이 TLS 연결이 맺어지는 동시에 프로토콜이 결정됩니다.
클라이언트 → 서버: ClientHello + ALPN 확장 ["h2", "http/1.1"]
서버 → 클라이언트: ServerHello + ALPN 확장 ["h2"]
↑ 서버가 h2를 선택
Netty에서의 ALPN 설정
// ALPN을 포함한 SSL 컨텍스트 구성
SslContext sslCtx = SslContextBuilder.forServer(certFile, keyFile)
.applicationProtocolConfig(new ApplicationProtocolConfig(
// ALPN 프로토콜 사용
ApplicationProtocolConfig.Protocol.ALPN,
// 클라이언트가 지원하지 않으면 광고하지 않음
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
// 선택된 프로토콜이 없어도 연결 수락
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
// 지원하는 프로토콜 목록 (우선순위 순서)
ApplicationProtocolNames.HTTP_2, // "h2"
ApplicationProtocolNames.HTTP_1_1 // "http/1.1"
))
.build();
ApplicationProtocolNames.HTTP_2는 문자열 "h2"에 해당합니다. ALPN에서 사용하는 프로토콜 식별자입니다.
HTTP/1.1과 HTTP/2 동시 지원
실제 서비스에서는 HTTP/1.1만 지원하는 클라이언트도 있으므로, 양쪽을 동시에 지원해야 합니다. ApplicationProtocolNegotiationHandler를 사용하면 ALPN 결과에 따라 파이프라인을 분기할 수 있습니다.
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http2.*;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
// ALPN 협상 결과에 따라 파이프라인을 동적으로 구성하는 핸들러
public class Http2OrHttp1Handler extends ApplicationProtocolNegotiationHandler {
// ALPN이 지원되지 않을 때 기본 프로토콜
public Http2OrHttp1Handler() {
super(ApplicationProtocolNames.HTTP_1_1);
}
@Override
protected void configurePipeline(ChannelHandlerContext ctx,
String protocol) {
ChannelPipeline pipeline = ctx.pipeline();
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
// HTTP/2로 협상됨
configureHttp2(pipeline);
} else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) {
// HTTP/1.1로 협상됨
configureHttp1(pipeline);
} else {
throw new IllegalStateException("알 수 없는 프로토콜: " + protocol);
}
}
private void configureHttp2(ChannelPipeline pipeline) {
pipeline.addLast("frameCodec",
Http2FrameCodecBuilder.forServer().build());
pipeline.addLast("multiplexHandler",
new Http2MultiplexHandler(
new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new Http2StreamHandler());
}
}
));
}
private void configureHttp1(ChannelPipeline pipeline) {
pipeline.addLast("httpCodec", new HttpServerCodec());
pipeline.addLast("aggregator",
new HttpObjectAggregator(1024 * 1024));
pipeline.addLast("handler", new Http1RequestHandler());
}
}
서버 부트스트랩에서의 사용
// SSL + 프로토콜 분기를 적용한 파이프라인
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 1. TLS 핸들러
pipeline.addLast("ssl", sslCtx.newHandler(ch.alloc()));
// 2. ALPN 결과에 따라 HTTP/2 또는 HTTP/1.1 파이프라인 구성
pipeline.addLast("protocolNegotiator", new Http2OrHttp1Handler());
}
});
이 구조의 핵심은 SslHandler 다음에 ApplicationProtocolNegotiationHandler를 배치 하는 것입니다. TLS 핸드셰이크가 완료되면 configurePipeline()이 호출되고, 이 메서드 안에서 프로토콜에 맞는 핸들러를 동적으로 추가합니다. 핸들러 자기 자신은 파이프라인에서 자동으로 제거됩니다.
HTTP/2 프레임 타입별 처리
실무에서 자주 다루는 프레임 타입을 좀 더 자세히 살펴봅니다.
SETTINGS 프레임 — 연결 초기 설정
HTTP/2 연결이 맺어지면 양쪽이 SETTINGS 프레임을 교환합니다.
// 서버가 보내는 초기 설정
Http2Settings settings = new Http2Settings()
.maxConcurrentStreams(100) // 동시 스트림 최대 100개
.initialWindowSize(1048576) // 흐름 제어 윈도우 1MB
.maxHeaderListSize(8192); // 헤더 최대 크기 8KB
Http2FrameCodec frameCodec = Http2FrameCodecBuilder.forServer()
.initialSettings(settings)
.build();
| 설정 | 기본값 | 설명 |
|---|---|---|
| HEADER_TABLE_SIZE | 4096 | HPACK 동적 테이블 크기 |
| MAX_CONCURRENT_STREAMS | 무제한 | 동시 열린 스트림 수 제한 |
| INITIAL_WINDOW_SIZE | 65535 | 스트림 수준 흐름 제어 윈도우 |
| MAX_FRAME_SIZE | 16384 | 단일 프레임의 최대 페이로드 크기 |
| MAX_HEADER_LIST_SIZE | 무제한 | 헤더 블록의 최대 크기 |
흐름 제어 — WINDOW_UPDATE
HTTP/2는 스트림 레벨과 연결 레벨 두 단계의 흐름 제어를 합니다. 수신 측이 처리할 수 있는 만큼만 데이터를 보내도록 WINDOW_UPDATE 프레임으로 윈도우를 갱신합니다.
네티의 Http2FrameCodec은 기본적으로 흐름 제어를 자동으로 처리합니다. DATA 프레임을 수신하면 윈도우를 소비하고, 처리 후 자동으로 WINDOW_UPDATE를 보냅니다.
HTTP/2의 한계 — TCP HOL Blocking
HTTP/2는 HTTP 레벨의 HOL Blocking을 해결했지만, TCP 레벨의 HOL Blocking 은 여전히 존재합니다.
TCP 패킷 흐름:
[패킷1: 스트림A] [패킷2: 스트림B] [패킷3: 스트림C] [패킷4: 스트림A]
↑
이 패킷이 유실되면?
→ 패킷3, 패킷4도 기다려야 함
→ 스트림 B, C, A 모두 영향
TCP는 순서 보장 프로토콜입니다. 패킷 하나가 유실되면 그 뒤의 모든 패킷이 재전송을 기다려야 합니다. HTTP/2에서는 서로 다른 스트림의 패킷이라도 같은 TCP 연결을 공유하므로, 한 패킷의 유실이 모든 스트림에 영향을 미칩니다.
이 문제를 근본적으로 해결하기 위해 등장한 것이 HTTP/3 (QUIC) 입니다.
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| 전송 프로토콜 | TCP | TCP | QUIC (UDP) |
| 멀티플렉싱 | 불가 | 가능 | 가능 |
| HOL Blocking | HTTP + TCP | TCP만 | 없음 |
| 헤더 압축 | 없음 | HPACK | QPACK |
| 연결 설정 | TCP + TLS (2~3 RTT) | TCP + TLS (2~3 RTT) | 0~1 RTT |
QUIC은 UDP 위에 신뢰성 있는 전송 을 구현하면서, 스트림별로 독립적인 패킷 순서를 보장합니다. 스트림 A의 패킷이 유실되어도 스트림 B, C는 영향을 받지 않습니다.
HTTP/2의 멀티플렉싱이 "한 연결에서 여러 스트림을 동시에 처리"하는 것이라면, QUIC은 "각 스트림이 전송 계층에서도 진짜 독립적"인 것입니다. HTTP/2의 멀티플렉싱은 애플리케이션 계층에서만 독립적이었던 셈이죠.
정리
HTTP/2의 핵심 내용을 요약합니다.
- **HTTP/1.1의 한계 **: 요청당 연결 또는 HOL Blocking. 텍스트 헤더의 중복 전송
- **HTTP/2의 해법 **: 바이너리 프레이밍, 멀티플렉싱, HPACK 헤더 압축, 서버 푸시
- ** 스트림 **: 하나의 TCP 연결 안에서 각 요청-응답에 고유 Stream ID를 부여하여 동시 처리
- Http2FrameCodec: HTTP/2 바이너리 프레임을 네티 객체로 인코딩/디코딩
- Http2MultiplexHandler: 스트림을 자식 Channel로 분리하여 각 스트림에 독립된 파이프라인 제공
- ALPN: TLS 핸드셰이크에서 프로토콜을 협상하여 추가 왕복 없이 HTTP/2 사용 결정
- ** 프로토콜 동시 지원 **:
ApplicationProtocolNegotiationHandler로 HTTP/1.1과 HTTP/2를 ALPN 결과에 따라 분기 - TCP HOL Blocking: HTTP/2에서도 TCP 패킷 유실 시 모든 스트림이 영향받는 한계가 있으며, HTTP/3(QUIC)이 이를 해결