HTTP/1.1은 요청과 응답이 순차적이다. 하나의 연결에서 한 번에 하나의 요청만 처리한다. 동시에 여러 리소스가 필요하면 연결을 여러 개 열어야 한다. HTTP/2는 하나의 연결에서 여러 요청을 병렬로 처리하는 구조로 이 한계를 해결했다.
HTTP/1.1의 한계
HOL 블로킹
HTTP/1.1은 하나의 TCP 연결에서 요청-응답을 순차 처리한다. 첫 번째 요청의 응답이 도착하기 전까지 두 번째 요청은 대기한다. 이를 HOL 블로킹, Head-of-Line Blocking이라 한다.
sequenceDiagram
participant C as 클라이언트
participant S as 서버
Note over C,S: HTTP/1.1 — 순차 처리
C->>S: GET /style.css
S-->>C: style.css 응답
C->>S: GET /script.js
S-->>C: script.js 응답
C->>S: GET /image.png
S-->>C: image.png 응답
웹 페이지 하나를 렌더링하는 데 수십 개의 리소스가 필요하다. 순차 처리로는 느리다.
HTTP/1.1은 이를 우회하기 위해 파이프라이닝을 지원한다. 응답을 기다리지 않고 여러 요청을 연속으로 보내는 방식이다. 하지만 응답은 여전히 요청 순서대로 도착해야 한다. 앞선 응답이 느리면 뒤의 응답도 밀린다. 구현상의 문제도 많아서 대부분의 브라우저가 파이프라이닝을 비활성화했다.
실제로는 동시 TCP 연결로 대응한다. 브라우저는 같은 도메인에 최대 6개의 TCP 연결을 동시에 열어서 병렬 요청을 처리한다. 하지만 각 연결마다 TCP handshake와 TLS handshake 비용이 발생한다.
헤더 중복
HTTP/1.1은 헤더를 텍스트로 전송한다. 매 요청마다 User-Agent, Cookie, Accept 같은 헤더가 반복된다. 쿠키가 포함되면 요청 헤더만 수 KB에 달한다. 압축 메커니즘이 없어서 동일한 정보가 매번 전송된다.
HTTP/2
HTTP/2는 2015년에 표준화되었다. Google의 SPDY 프로토콜을 기반으로 한다. HTTP/1.1과 동일한 의미 체계를 유지하면서 전송 방식을 근본적으로 변경했다.
바이너리 프레이밍
HTTP/1.1은 텍스트 기반이다. 요청과 응답을 사람이 읽을 수 있는 문자열로 주고받는다. HTTP/2는 모든 메시지를 바이너리 프레임으로 인코딩한다.
block-beta
columns 3
block:http1["HTTP/1.1"]:3
h1["GET /index.html HTTP/1.1\nHost: example.com\nAccept: text/html"]
end
space:3
block:http2["HTTP/2"]:3
h2a["HEADERS 프레임\n(스트림 1)"]
h2b["DATA 프레임\n(스트림 1)"]
h2c["HEADERS 프레임\n(스트림 3)"]
end
style http1 fill:#FFCDD2
style http2 fill:#C8E6C9
하나의 HTTP 메시지가 HEADERS 프레임과 DATA 프레임으로 분리된다. 각 프레임에는 스트림 ID가 태그되어 있어서 수신 측이 프레임을 올바른 메시지로 재조립할 수 있다.
멀티플렉싱
HTTP/2의 핵심 개선이다. 하나의 TCP 연결에서 여러 스트림이 동시에 동작한다. 각 스트림은 독립적인 요청-응답 쌍이다. 프레임 단위로 인터리빙되므로 한 스트림의 응답이 느려도 다른 스트림은 영향을 받지 않는다.
sequenceDiagram
participant C as 클라이언트
participant S as 서버
Note over C,S: HTTP/2 — 멀티플렉싱
C->>S: 스트림 1: GET /style.css
C->>S: 스트림 3: GET /script.js
C->>S: 스트림 5: GET /image.png
S-->>C: 스트림 3: script.js (일부)
S-->>C: 스트림 1: style.css (완료)
S-->>C: 스트림 5: image.png (일부)
S-->>C: 스트림 3: script.js (완료)
S-->>C: 스트림 5: image.png (완료)
HTTP/1.1처럼 여러 TCP 연결을 열 필요가 없다. 하나의 연결이 모든 요청을 처리한다. TCP handshake, TLS handshake 비용이 한 번으로 줄어든다.
HPACK 헤더 압축
HTTP/2는 HPACK이라는 전용 압축 알고리즘으로 헤더를 압축한다. 두 가지 기법을 결합한다.
정적/동적 테이블: 자주 사용되는 헤더 필드를 인덱스 번호로 대체한다. :method: GET이 정적 테이블에서 인덱스 2로 매핑되면 2바이트면 충분하다. 연결 중에 새로 등장한 헤더는 동적 테이블에 추가되어 이후 요청에서 인덱스로 참조된다.
허프만 인코딩: 테이블에 없는 값은 허프만 코딩으로 압축한다. 빈도가 높은 문자에 짧은 비트를 할당하여 전체 크기를 줄인다.
HTTP/1.1에서 매 요청마다 반복되던 수 KB의 헤더가 수십 바이트로 줄어든다.
서버 푸시
클라이언트가 HTML을 요청하면 서버가 해당 HTML에 필요한 CSS, JavaScript를 클라이언트의 추가 요청 없이 선제적으로 전송할 수 있다.
sequenceDiagram
participant C as 클라이언트
participant S as 서버
C->>S: GET /index.html
S-->>C: PUSH_PROMISE: /style.css
S-->>C: PUSH_PROMISE: /script.js
S-->>C: /index.html 응답
S-->>C: /style.css 응답 (푸시)
S-->>C: /script.js 응답 (푸시)
클라이언트가 HTML을 파싱하고 추가 리소스를 요청하는 왕복 시간을 제거한다. 다만 실제로는 캐시된 리소스를 불필요하게 전송하는 문제가 있어서 활용이 제한적이다.
스트림 우선순위
클라이언트가 각 스트림에 가중치와 의존성을 설정할 수 있다. CSS 파일의 우선순위를 이미지보다 높게 설정하면 서버가 CSS를 먼저 전송한다. 렌더링에 필수적인 리소스를 우선 처리하여 초기 로딩 속도를 개선한다.
HTTP/2의 한계
HTTP/2는 HTTP 수준의 HOL 블로킹을 해결했지만 TCP 수준의 HOL 블로킹은 남아 있다. 하나의 TCP 연결 위에서 모든 스트림이 동작하므로, TCP 패킷이 유실되면 해당 패킷이 재전송될 때까지 모든 스트림이 대기한다.
HTTP/3은 TCP 대신 UDP 기반의 QUIC 프로토콜을 사용하여 이 문제를 해결한다. 각 스트림이 독립적인 전송 단위가 되어 한 스트림의 패킷 유실이 다른 스트림에 영향을 주지 않는다.
gRPC
gRPC는 Google이 개발한 RPC 프레임워크다. HTTP/2를 전송 프로토콜로 사용하고, Protocol Buffers를 직렬화 형식으로 사용한다.
HTTP/2 활용
gRPC는 HTTP/2의 멀티플렉싱을 활용하여 하나의 연결에서 여러 RPC 호출을 병렬로 처리한다. HPACK 헤더 압축도 그대로 적용된다. HTTP/2의 스트리밍 기능을 확장하여 네 가지 통신 패턴을 지원한다.
block-beta
columns 2
block:unary["단방향"]:1
columns 3
u1["클라이언트"]
space
u2["서버"]
u1 -- "요청 1 / 응답 1" --> u2
end
block:server["서버 스트리밍"]:1
columns 3
s1["클라이언트"]
space
s2["서버"]
s1 -- "요청 1 / 응답 N" --> s2
end
block:client["클라이언트 스트리밍"]:1
columns 3
c1["클라이언트"]
space
c2["서버"]
c1 -- "요청 N / 응답 1" --> c2
end
block:bidi["양방향 스트리밍"]:1
columns 3
b1["클라이언트"]
space
b2["서버"]
b1 -- "요청 N / 응답 N" --> b2
end
style unary fill:#E3F2FD
style server fill:#E8F5E9
style client fill:#FFF3E0
style bidi fill:#F3E5F5
Protocol Buffers
REST가 JSON을 사용하는 것과 달리 gRPC는 Protocol Buffers, Protobuf를 사용한다. .proto 파일에 메시지 구조와 서비스 인터페이스를 정의하면 각 언어의 코드가 자동 생성된다.
service ChatService {
rpc SendMessage (MessageRequest) returns (MessageResponse);
rpc StreamMessages (RoomRequest) returns (stream Message);
}
message MessageRequest {
string room_id = 1;
string user_id = 2;
string content = 3;
}
Protobuf는 바이너리 직렬화를 사용한다. JSON 대비 크기가 35배 작고, 직렬화/역직렬화 속도가 510배 빠르다. 스키마가 .proto 파일에 명시되므로 클라이언트와 서버 간 인터페이스 계약이 코드 수준에서 강제된다.
REST vs gRPC
gRPC가 적합한 경우:
- 마이크로서비스 간 내부 통신: 낮은 지연, 높은 처리량이 필요할 때
- 양방향 스트리밍: 실시간 채팅, 이벤트 구독
- 다중 언어 환경:
.proto파일 하나로 여러 언어의 클라이언트/서버 코드 생성
REST가 적합한 경우:
- 외부 API: 브라우저가 gRPC를 직접 지원하지 않는다. gRPC-Web이나 프록시가 필요하다
- 디버깅 편의성: JSON은 사람이 읽을 수 있고, curl이나 브라우저 개발자 도구로 확인 가능하다
- 단순 CRUD: RPC 스타일이 필요 없는 리소스 기반 API
실무에서는 내부 서비스 간 통신에 gRPC, 외부 API에 REST를 조합하는 패턴이 일반적이다.
HTTP/1.1은 요청과 응답이 순차적이다. HTTP/2는 멀티플렉싱으로 이 제약을 제거했다. gRPC는 HTTP/2의 이점 위에 바이너리 직렬화와 스트리밍을 더했다. 프로토콜이 해결하려는 문제가 각각 다르고, 선택은 워크로드가 결정한다.