04 — 전송 계층 (Transport)
질문: GraphQL은 어떤 네트워크 프로토콜 위에서 동작하며, 각 전송이 풀려는 문제는 무엇인가? 한 줄 답 (Pyramid Top): “GraphQL은 전송과 무관한 사양이다 — HTTP가 가장 흔하지만, WebSocket(subscription)·SSE(stream)·multipart(upload)가 모두 표준이다.”
이전 챕터(03 — N+1 & DataLoader)까지는 서버 안에서 일어나는 일만 다뤘다. 이번 챕터는 그 쿼리/응답이 어떤 와이어 포맷으로 클라이언트와 서버 사이를 오가는지 — 즉 transport layer 위의 GraphQL을 다룬다.
핵심은 한 가지다. GraphQL spec 자체는 transport를 정의하지 않는다. spec은 “쿼리를 어떻게 평가하고 응답이 어떻게 생겨야 하는가”만 기술한다. 실제 wire format은 별도의 서브 사양들 — graphql-over-http, graphql-ws, graphql-multipart-request-spec, graphql-sse — 이 채운다.
한 문장 답
클라이언트가 query/mutation을 보낼 때는 HTTP POST + JSON (또는 GET + URL)을 쓰고, 서버가 여러 번 응답을 흘려보내야 할 때는 WebSocket(subscription)이나 SSE/multipart(@defer/@stream)를 쓰며, 파일을 같이 올릴 때는 multipart/form-data spec을 따로 쓴다. 즉, GraphQL 위에는 전송별로 분리된 4개의 사양이 동시에 존재한다.
챕터 지도 (Mermaid)
읽는 순서
| # | 파일 | 읽는 데 | 핵심 키워드 |
|---|---|---|---|
| 01 | 01-graphql-over-http | 10분 | spec, application/graphql-response+json, Accept negotiation, 200 + errors |
| 02 | 02-get-vs-post | 8분 | URL 캐시, persisted query, mutation = POST 강제 |
| 03 | 03-multipart-file-upload | 10분 | jaydenseric spec, operations + map + files, Upload scalar |
| 04 | 04-graphql-over-ws | 12분 | graphql-ws, connection_init → subscribe → next → complete |
| 05 | 05-subscriptions-and-pubsub | 12분 | PubSub 백엔드, sticky session, scaling 비용 |
| 06 | 06-sse-and-defer-stream | 10분 | multipart/mixed, @defer/@stream, React Suspense |
추천 동선: 1→2를 먼저 읽으면 95%의 GraphQL 트래픽이 이해된다. 3·4·5·6은 필요한 순간(파일 업로드·실시간·점진 응답)에 돌아오는 reference로 쓰면 된다.
의존성: 02는 01의 HTTP 기반 위에 얹힌다. 04→05는 한 쌍 (WS는 기제, subscription은 그 위의 의미론). 06은 01의 HTTP를 재활용해서 streaming을 한다.
6개 문서 한 줄 요약
| # | 문서 | 한 줄 답 |
|---|---|---|
| 01 | GraphQL over HTTP | spec.graphql.org의 graphql-over-http가 표준이다 — POST + JSON body, 200 + errors[]가 기본, 새 Content-Type은 application/graphql-response+json. |
| 02 | GET vs POST | GET은 CDN 캐시 가능하지만 URL 길이/보안 한계, POST는 body 자유. mutation은 spec상 POST만 허용된다. |
| 03 | Multipart Upload | jaydenseric의 de facto 표준 — operations(JSON) + map(파일↔변수) + 실제 파일 파트를 한 multipart에 묶는다. Upload scalar는 spec이 아니다. |
| 04 | GraphQL over WS | graphql-ws(enisdenjo)가 표준이다 — subscriptions-transport-ws는 deprecated. 인증은 connection_init.payload에. |
| 05 | Subscriptions & PubSub | subscription의 비용은 transport가 아니라 백엔드다 — long-lived connection + pub/sub broker + sticky routing이 모두 필요. |
| 06 | SSE & @defer/@stream | 한 쿼리의 응답을 여러 청크로 흘려보낸다. multipart/mixed로 HTTP 위에서, SSE로 1방향 스트림으로 — Apollo는 production에 쓰지만 spec은 working draft. |
전송별 비교 — 한 표로
| 전송 | Use case | Content-Type | 캐시 | 양방향 | 다중 응답 | 표준 상태 |
|---|---|---|---|---|---|---|
| HTTP POST + JSON | query/mutation 99% | application/graphql-response+json | ✗ (body) | ✗ | ✗ | ✅ graphql-over-http spec |
| HTTP GET | 캐시 가능한 쿼리, persisted query | application/graphql-response+json | ✅ CDN | ✗ | ✗ | ✅ graphql-over-http spec |
| multipart/form-data | 파일 업로드 | multipart/form-data | ✗ | ✗ | ✗ | ⚠️ de facto (jaydenseric) |
| WebSocket (graphql-ws) | subscription, 실시간 | graphql-transport-ws subprotocol | ✗ | ✅ | ✅ | ✅ graphql-ws |
| SSE / multipart/mixed | @defer, @stream | text/event-stream 또는 multipart/mixed | ✗ | ✗ (서버→클) | ✅ | ⚠️ working draft |
Why — 왜 이 챕터가 필요한가
GraphQL을 처음 도입할 때 가장 많이 깨지는 것은 비즈니스 로직이 아니라 transport 가정이다.
- “왜 우리 CDN이 GraphQL 응답을 캐시 못 하지?” → POST는 캐시 키가 없다 (
01,02) - “REST는 4xx로 에러가 오는데 GraphQL은 왜 다 200이지?” →
data + errors패러다임 (01) - “Apollo Client에서 파일 업로드가 왜 안 되지?” → multipart spec은 별도 패키지가 필요 (
03) - “subscription을 production에 켰는데 서버 메모리가 터졌다” → long-lived connection의 scaling (
04,05) - “subscription이 다른 서버 인스턴스의 데이터를 못 받는다” → PubSub broker가 빠졌다 (
05) - “React Suspense랑 GraphQL을 같이 쓰고 싶다” →
@defer/@stream+ multipart (06)
이 챕터는 전송별로 분리된 사양에 이름을 붙이고, 각 사양이 어떤 문제를 풀려고 만들어졌는가를 명시한다.
How — 어떻게 읽나
- 백엔드 엔지니어: 1 → 2 → 5 → 4. transport보다 pub/sub 백엔드 선택이 더 어렵다.
- 프론트엔드 엔지니어: 1 → 2 → 3 → 4 → 6. 클라이언트 라이브러리(Apollo, urql, Relay)가 어떤 transport을 어떻게 골라 쓰는지.
- 인프라/DevOps: 1 → 2 → 5. 캐시(
02)와 sticky session(05)이 가장 직접적.
What-if — 이 챕터를 건너뛰면
- HTTP만 쓰면: subscription을 polling으로 흉내내다가 backend가 죽는다.
- 200 + errors를 모르면: 모든 GraphQL 응답을
response.ok로 검사하면서 서버는 에러를 잘 돌려보내는데 클라이언트만 못 본다. - graphql-ws를 모르면: 검색하면 가장 먼저 나오는
subscriptions-transport-ws를 설치한 뒤 5년째 deprecated인 라이브러리를 production에 올린다. - PubSub broker를 모르면: subscription을 단일 노드에서만 테스트하고, scale-out 순간 다른 인스턴스의 이벤트가 안 흐른다는 사실에 당황한다.
- @defer/@stream을 모르면: Apollo 최신 가이드가 왜 streaming response를 가정하는지 이해 못 한다.
Insight — 한 단락 이야기
“GraphQL은 transport를 정의하지 않기로 한 그날부터, 4개의 사양이 분기했다”
2015년 Facebook이 GraphQL을 오픈소스로 풀었을 때 spec에 의도적으로 빠진 게 transport였다. 이유는 단순했다 — Facebook 안에서는 모바일 앱과 서버 사이에 자체 RPC 프로토콜을 썼고, 외부에 풀 때 HTTP를 강요할 이유가 없었다. 결과는 두 갈래로 나타났다. 첫째, 어떤 transport에도 GraphQL이 얹힐 수 있다는 자유 — Netflix는 한때 gRPC 위에 GraphQL을 얹었다. 둘째, transport 표준이 사양 밖에서 자라났다는 분기 —
graphql-over-http는 2018년부터 working group이 작업해서 2025년에야 안정화됐고,graphql-multipart-request-spec은 jaydenseric이라는 한 사람의 GitHub 레포가 사실상 표준이 됐으며, subscription은subscriptions-transport-ws가 죽고graphql-ws가 그 자리를 가져갔다. 사양의 공백이 생태계에 더 많은 사양을 만들어냈다. — 이 챕터는 그 사양들의 지도다.
한 단락 요약
GraphQL의 transport은 HTTP(
01,02)·multipart(03)·WebSocket(04,05)·SSE/multipart-mixed(06) 네 개의 분리된 사양 위에 동시에 존재한다. 이 챕터를 끝내면 “GraphQL을 어떻게 보내지”라는 질문 대신 *“이 응답은 1번인가 N번인가, 캐시 가능한가, 파일을 끼고 있나”*라는 질문을 던지게 된다. 다음 챕터(05-cache-performance)는 이 transport들 위에서 캐시와 성능이 어떻게 작동하는지를 다룬다.