🔷 GraphQL4. 전송 계층📖 개요

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)


읽는 순서

#파일읽는 데핵심 키워드
0101-graphql-over-http10분spec, application/graphql-response+json, Accept negotiation, 200 + errors
0202-get-vs-post8분URL 캐시, persisted query, mutation = POST 강제
0303-multipart-file-upload10분jaydenseric spec, operations + map + files, Upload scalar
0404-graphql-over-ws12분graphql-ws, connection_initsubscribenextcomplete
0505-subscriptions-and-pubsub12분PubSub 백엔드, sticky session, scaling 비용
0606-sse-and-defer-stream10분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개 문서 한 줄 요약

#문서한 줄 답
01GraphQL over HTTPspec.graphql.org의 graphql-over-http가 표준이다 — POST + JSON body, 200 + errors[]가 기본, 새 Content-Type은 application/graphql-response+json.
02GET vs POSTGET은 CDN 캐시 가능하지만 URL 길이/보안 한계, POST는 body 자유. mutation은 spec상 POST만 허용된다.
03Multipart Uploadjaydenseric의 de facto 표준operations(JSON) + map(파일↔변수) + 실제 파일 파트를 한 multipart에 묶는다. Upload scalar는 spec이 아니다.
04GraphQL over WSgraphql-ws(enisdenjo)가 표준이다 — subscriptions-transport-wsdeprecated. 인증은 connection_init.payload에.
05Subscriptions & PubSubsubscription의 비용은 transport가 아니라 백엔드다 — long-lived connection + pub/sub broker + sticky routing이 모두 필요.
06SSE & @defer/@stream한 쿼리의 응답을 여러 청크로 흘려보낸다. multipart/mixed로 HTTP 위에서, SSE로 1방향 스트림으로 — Apollo는 production에 쓰지만 spec은 working draft.

전송별 비교 — 한 표로

전송Use caseContent-Type캐시양방향다중 응답표준 상태
HTTP POST + JSONquery/mutation 99%application/graphql-response+json✗ (body)✅ graphql-over-http spec
HTTP GET캐시 가능한 쿼리, persisted queryapplication/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, @streamtext/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, 02multipart(03WebSocket(04, 05SSE/multipart-mixed(06) 네 개의 분리된 사양 위에 동시에 존재한다. 이 챕터를 끝내면 “GraphQL을 어떻게 보내지”라는 질문 대신 *“이 응답은 1번인가 N번인가, 캐시 가능한가, 파일을 끼고 있나”*라는 질문을 던지게 된다. 다음 챕터(05-cache-performance)는 이 transport들 위에서 캐시와 성능이 어떻게 작동하는지를 다룬다.