05 — GraphQL vs RPC
한 줄 답: JSON-RPC, XML-RPC, OpenRPC는 모두 함수 호출 패러다임 — 클라이언트가 서버 함수의 이름과 인자를 보내면 결과가 온다. GraphQL은 그래프 쿼리 패러다임 — 클라이언트가 원하는 결과 모양을 보내면 엔진이 그래프를 traversal해서 결과를 만든다. 같은 “Remote” + “Procedure” 글자를 써도 모델이 다르다.
Why — 왜 “RPC와 GraphQL”이 자주 혼동되나
/graphql endpoint에 POST를 보낸다는 점에서 겉모양은 JSON-RPC와 닮았다. 그래서 종종 “GraphQL은 그냥 JSON-RPC의 변형 아닌가”라는 오해가 나온다. 또는 gRPC가 “RPC”라서 gRPC와 GraphQL의 비교(03)도 RPC 일반과의 비교인 줄로 착각한다.
이 챕터는 그 혼동을 정리한다 — RPC family(JSON-RPC, XML-RPC, OpenRPC, gRPC, SOAP)와 GraphQL의 패러다임 차이.
| RPC 계열 (function call) | GraphQL (graph query) |
|---|---|
| 클라이언트가 함수 이름과 인자를 보냄 | 클라이언트가 결과의 모양을 보냄 |
| 응답 type은 서버 함수의 return type | 응답 type은 쿼리가 정의한 type |
addOrder(item, qty) 처럼 동사 중심 | { order { items { qty } } } 처럼 명사 중심 |
| 그래프 traversal 없음 | 그래프 traversal 기본 |
| 한 호출 = 한 함수 = 평면 응답 | 한 쿼리 = 한 트리 = 중첩 응답 |
How — 각 RPC 변종이 어떻게 다른가
1) RPC family 한눈에
- XML-RPC (1998): 함수 이름 + 인자를 XML로 직렬화. 단순. 거의 사라짐.
- JSON-RPC 2.0 (2010): XML 대신 JSON. 가벼움. 블록체인 (Ethereum) 등에서 여전히 사용.
- SOAP: 무겁고 복잡한 XML + WSDL 스키마. 엔터프라이즈에서 잔존.
- gRPC: HTTP/2 + Protobuf 위의 RPC. 강타입, 빠름. (
03참고) - OpenRPC: JSON-RPC의 OpenAPI — 함수 시그니처 메타 명세.
2) JSON-RPC vs GraphQL — wire 비교
JSON-RPC 2.0 요청:
{
"jsonrpc": "2.0",
"method": "user.getById",
"params": { "id": 1 },
"id": 42
}JSON-RPC 2.0 응답:
{
"jsonrpc": "2.0",
"result": {
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"createdAt": "2024-01-01"
},
"id": 42
}→ method + params를 보내고, 함수의 전체 return을 받는다. 모양 결정은 서버.
GraphQL 요청:
{
"query": "{ user(id: 1) { name } }"
}GraphQL 응답:
{
"data": { "user": { "name": "Alice" } }
}→ 결과의 모양을 보내고, 그 모양 그대로 받는다. 모양 결정은 클라이언트.
겉으로는 비슷한 JSON POST지만, 의미 모델이 정반대다.
3) 그래프 traversal의 유무
// JSON-RPC — N개 호출 필요
const user = await call("user.getById", { id: 1 });
const posts = await call("post.byUserId", { userId: 1 });
const author = await Promise.all(
posts.map(p => call("user.getById", { id: p.authorId }))
);
// → 1 + 1 + N round trips# GraphQL — 한 호출
{
user(id: 1) {
posts {
title
author { name }
}
}
}
# → 1 round tripRPC는 함수 단위다. 관계를 따라가려면 클라이언트가 여러 호출을 엮어야. GraphQL은 관계를 따라가는 traversal이 쿼리 자체에 내장.
→ 이 차이가 GraphQL의 가장 큰 가치 제안이고, RPC가 줄 수 없는 것이다.
4) Batching — RPC의 회피책
JSON-RPC 2.0은 batch request를 지원한다.
[
{ "jsonrpc": "2.0", "method": "user.getById", "params": { "id": 1 }, "id": 1 },
{ "jsonrpc": "2.0", "method": "post.byUserId", "params": { "userId": 1 }, "id": 2 }
]→ N round trip은 1로 줄어들지만, 클라이언트가 여전히 모든 호출을 명시해야 함. 그리고 결과의 join도 클라이언트 책임. GraphQL의 관계 자동 traversal이 아니다.
5) OpenRPC — RPC family의 GraphQL Schema 시도
OpenRPC는 JSON-RPC에 메타 명세를 추가한다 — 함수 시그니처, 인자 type, return type을 적은 JSON 스펙. OpenAPI가 REST에 한 일을 JSON-RPC에 한다.
{
"openrpc": "1.2.6",
"methods": [
{
"name": "user.getById",
"params": [{ "name": "id", "schema": { "type": "integer" } }],
"result": { "schema": { "$ref": "#/components/schemas/User" } }
}
]
}→ type 안전과 자동 codegen은 가능해진다. 하지만 그래프 traversal은 여전히 없다. RPC family의 한계.
What — 패러다임 차이 요약
동사 중심 vs 명사 중심
RPC (동사 중심):
createUser(...)
getUser(...)
updateUser(...)
deleteUser(...)
getUserPosts(...)
getPostComments(...)
addComment(...)
...→ 함수가 무한히 늘어남. 화면이 늘면 함수도 늘어남.
GraphQL (명사 중심):
type User { posts: [Post] }
type Post { comments: [Comment] }
type Comment { author: User }
type Mutation {
createUser(...)
addComment(...)
}→ 명사 그래프를 한 번 정의하고, 쿼리는 그 그래프를 다양하게 traversal. 새 화면이 와도 스키마 변경 없음.
함수의 수 비교
| API | 함수/쿼리 가능한 수 |
|---|---|
| 잘 짠 REST | 리소스 수 × HTTP 메서드 수 |
| 잘 짠 JSON-RPC | 함수 수 |
| 잘 짠 GraphQL | 조합 폭발 — 같은 스키마로 수십억 가지 쿼리 가능 |
GraphQL은 스키마라는 조합 base가 있어서, 클라이언트가 명시적 함수를 정의하지 않은 쿼리도 받을 수 있다. RPC는 모든 호출이 미리 정의된 함수여야 한다.
Versioning 모델
| 도구 | versioning |
|---|---|
| XML-RPC | 함수 이름에 v2 (getUser_v2) |
| JSON-RPC | 함수 이름에 v2 |
| OpenRPC | API URL에 v2 (/v2/rpc) |
| gRPC | proto package 버전 |
| GraphQL | 버전 없음 — 필드 deprecate, 새 필드 추가 |
GraphQL의 no versioning은 명사 중심의 결과다 — 새 데이터 모양이 필요해도 그래프에 노드 추가면 됨, 새 함수 만들 필요 없음.
What-if — 잘못 선택하면
1) JSON-RPC로 모바일 News Feed 구현
→ 화면 한 장에 수십 호출 + 클라이언트 join — 모바일에서 수 초 대기. 대응: GraphQL의 그래프 traversal.
2) GraphQL로 단순 함수형 API (/sendEmail, /charge 등)
→ 스키마 overhead + codegen — 그냥 함수 하나면 되는 자리. 대응: JSON-RPC or REST.
3) “RPC와 GraphQL은 같다”고 보고 함수 묶음처럼 GraphQL 설계
→ type Mutation { getUser, getPost, getComment, ... }로 모든 query를 mutation으로 짜는 안티패턴.
대응: query는 명사 그래프, mutation은 동사 효과 분리.
4) OpenRPC vs GraphQL을 비교 시 문법 수준에서만 봄
→ “둘 다 JSON으로 method를 적는다”고 결론. 대응: 그래프 traversal 유무가 핵심 차이.
5) GraphQL을 batched JSON-RPC로만 봄
→ subscription, fragment, federation, codegen 같은 language-level 도구를 전혀 못 씀. 대응: GraphQL은 language다 — RPC family와 다른 가족.
Insight — 흥미로운 이야기
”Ethereum이 JSON-RPC를 고른 이유”
Ethereum 노드는 JSON-RPC를 표준 인터페이스로 쓴다 (eth_getBlockByNumber, eth_call 등). GraphQL은 EIP-1767로 대안 인터페이스로만 존재. 이유 — 블록체인 호출은 함수 호출이 본질 이다. 트랜잭션 보내기, 잔고 조회 — 동사 중심. 그래프 traversal이 필요 없는 영역. RPC가 맞는 자리의 전형.
”SOAP의 무덤”
1999년 SOAP은 *“웹 서비스의 미래”*로 추앙받았다. WSDL이라는 강타입 schema, namespace, security 표준 — 모든 게 있었다. 그러나 2010년대 너무 무겁다는 이유로 거의 죽었다. JSON-RPC와 REST가 자리를 차지하고, GraphQL이 마지막을 채웠다. schema가 있다고 살아남는 게 아니다 — 적합성이 맞아야 산다. SOAP의 교훈을 GraphQL도 새겨야 한다 — 잘못된 자리에 쓰면 무거움이 죽음이 된다.
”JSON-RPC의 끈질긴 생존”
JSON-RPC는 15년이 지나도 살아 있다 — Ethereum, Solana, Bitcoin Core, VS Code Language Server Protocol(LSP) 등. 이들의 공통점 — 함수 호출이 본질이고, 그래프 traversal이 필요 없는 자리. LSP는 “텍스트 에디터가 언어 서버에 goto_definition 호출” 같은 동사 중심 통신.
→ 각 도구는 자기 자리가 있다. GraphQL이 모든 자리를 차지하지 못하는 이유.
요약 + 다이어그램
RPC family와 GraphQL은 패러다임이 다르다. RPC는 함수 호출 — 동사 중심, 평면 응답, 호출이 늘면 함수도 늘어남. GraphQL은 그래프 쿼리 — 명사 중심, 중첩 응답, 한 스키마로 수많은 쿼리 표현. 함수 호출이 본질인 자리(블록체인·LSP·결제)에서는 RPC가 맞고, 데이터 그래프가 본질인 자리에서는 GraphQL이 맞다.
다음 문서:
06-bff-pattern.mdx— 그래서 GraphQL이 가장 자연스러운 자리가 BFF다. 왜 그런가?