JSON-RPC 2.0: 가볍고 단순한 원격 프로시저 호출 프로토콜
요즘 Model Context Protocol이나 A2A 프로토콜 관련 글을 보다 보면 “JSON-RPC 2.0 위에서 동작한다”는 표현을 자주 마주치게 되는데요. 이름만 들으면 거창해 보이지만 막상 스펙을 펼쳐보면 한 페이지로도 충분히 설명되는 작고 단순한 프로토콜입니다. 😅 JSON으로 메서드 이름과 인자를 보내고 결과를 JSON으로 받는다는, 정말 그게 전부거든요.
이번 포스팅에서는 JSON-RPC 2.0이 어떤 구조로 메시지를 주고받는지, REST와는 무엇이 다른지, 그리고 왜 최근 AI 도구 생태계가 다시 이 오래된 프로토콜을 꺼내 들었는지 차근차근 살펴보겠습니다.
JSON-RPC가 무엇인가요?
JSON-RPC는 이름 그대로 JSON을 데이터 포맷으로 사용하는 RPC(Remote Procedure Call) 프로토콜입니다. RPC는 다른 컴퓨터에 있는 함수를 마치 내 컴퓨터에 있는 함수처럼 호출하는 방식인데요. 클라이언트가 “이 메서드를 이 인자로 실행해 줘”라고 요청하면 서버가 결과를 돌려주는 단순한 모델입니다.
REST가 자원(Resource)을 중심으로 URL을 설계하고 HTTP 메서드(GET, POST, PUT, DELETE)로 행동을 표현한다면, JSON-RPC는 그냥 함수를 호출하는 느낌에 가깝습니다.
예를 들어 users/123/transfer처럼 리소스 경로를 고민할 필요 없이 transfer 메서드에 from과 to를 파라미터로 던지면 되는 거죠.
JSON-RPC 2.0은 2010년에 확정된 이후로 거의 변경되지 않을 만큼 안정적인 스펙이고, 트랜스포트(transport)에 대한 요구사항이 없다는 점이 큰 특징입니다. HTTP 위에서 써도 되고, WebSocket이나 표준 입출력(stdio), 심지어 TCP 소켓 위에서도 그대로 동작하거든요. 이 유연성 덕분에 비트코인 노드, Ethereum JSON-RPC API, 그리고 최근에는 MCP 서버 같은 서로 다른 환경에서 폭넓게 채택되어 왔습니다.
요청 객체의 구조
JSON-RPC 2.0의 요청은 네 개의 필드를 가진 JSON 객체입니다. 실제 메시지를 하나 보면 감이 빨리 옵니다.
{
"jsonrpc": "2.0",
"method": "subtract",
"params": [42, 23],
"id": 1
}
jsonrpc 필드는 반드시 문자열 "2.0"이어야 합니다.
이 값으로 1.0과 2.0을 구분하기 때문에 빼먹으면 서버가 요청을 거부할 수 있습니다.
method는 호출하려는 메서드 이름인데요.
스펙상 rpc.로 시작하는 이름은 내부 시스템 메서드용으로 예약되어 있어서 사용자 메서드 이름으로는 피해야 합니다.
params는 메서드에 넘길 인자입니다.
배열로 보내면 위치 기반(positional)으로 전달되고, 객체로 보내면 이름 기반(named)으로 전달됩니다.
인자가 없는 메서드라면 params 자체를 생략해도 됩니다.
{
"jsonrpc": "2.0",
"method": "subtract",
"params": { "minuend": 42, "subtrahend": 23 },
"id": 2
}
id는 요청을 식별하는 값으로, 문자열이나 숫자, 또는 null을 쓸 수 있습니다.
서버는 응답에 똑같은 id를 그대로 담아 보내기 때문에 클라이언트가 어떤 응답이 어떤 요청에 대한 것인지 매칭할 수 있습니다.
WebSocket처럼 응답이 비동기로 도착하는 환경에서 특히 중요한 역할을 합니다.
응답 객체의 구조
서버는 요청에 대해 두 가지 형태 중 하나로 응답합니다.
성공이면 result 필드를, 실패면 error 필드를 담습니다.
이 둘이 동시에 등장하는 일은 절대 없어야 합니다.
{
"jsonrpc": "2.0",
"result": 19,
"id": 1
}
{
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": "Method not found"
},
"id": 1
}
result는 메서드가 반환한 값이고, 타입은 자유롭습니다.
숫자, 문자열, 객체, 배열 무엇이든 JSON으로 표현될 수 있다면 그대로 담을 수 있습니다.
error는 code와 message를 필수로 가지고, 선택적으로 data 필드에 추가 정보를 담을 수 있습니다.
스택 트레이스나 검증 실패 항목을 디버깅용으로 넘길 때 유용한데요.
이때 JSON Schema로 데이터 구조 정의하기 글에서 다룬 검증 결과를 그대로 data에 담아 보내면 클라이언트 쪽에서 어떤 필드가 잘못됐는지 자세히 알려줄 수 있어 편리합니다.
미리 정의된 에러 코드
JSON-RPC 2.0은 자주 마주치는 상황에 대해 표준 에러 코드를 정의해 두었습니다. 규모가 작은 스펙치고는 꽤 친절한 부분인데요. 직접 에러 처리를 구현할 때 이 표를 참고하면 됩니다.
- -32700 Parse error — 서버가 받은 텍스트가 유효한 JSON이 아닐 때
- -32600 Invalid Request — JSON은 파싱했지만 요청 객체 형식이 잘못됐을 때
- -32601 Method not found — 존재하지 않는 메서드를 호출했을 때
- -32602 Invalid params — 메서드는 있지만 인자가 잘못됐을 때
- -32603 Internal error — 서버 내부 오류
- -32000 ~ -32099 — 서버 구현이 자유롭게 사용할 수 있는 영역
-32000부터 -32099까지의 범위는 애플리케이션이 자유롭게 정의할 수 있도록 비워둔 영역입니다.
“인증 실패”나 “잔액 부족” 같은 도메인 에러를 여기에 매핑하면 표준 에러와 깔끔하게 구분됩니다.
알림: 응답이 필요 없는 호출
id 필드를 빼고 보낸 요청을 알림(Notification)이라고 부릅니다.
서버는 알림에 대해서는 어떤 응답도 보내지 않아야 합니다.
실패하더라도 마찬가지로 침묵을 지킵니다.
{
"jsonrpc": "2.0",
"method": "log",
"params": ["user logged in", { "userId": 42 }]
}
로그를 남기거나 이벤트를 발행하는 것처럼 결과가 굳이 필요 없는 작업에 잘 어울립니다. 응답을 기다리지 않으니 지연(latency)도 줄어들고요. 다만 서버가 정말 받았는지 확인할 방법이 없으므로, 결과가 중요하다면 일반 요청을 사용하는 게 좋습니다.
배치 요청으로 묶어서 보내기
여러 요청을 한 번에 보내고 싶다면 요청 객체를 배열로 감싸기만 하면 됩니다.
서버는 각 요청을 독립적으로 처리한 뒤 결과를 배열로 묶어서 돌려주는데요.
이때 응답의 순서가 요청의 순서와 일치한다는 보장이 없으므로 id로 매칭해야 합니다.
[
{ "jsonrpc": "2.0", "method": "sum", "params": [1, 2, 4], "id": "1" },
{ "jsonrpc": "2.0", "method": "notify_hello", "params": [7] },
{ "jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2" }
]
[
{ "jsonrpc": "2.0", "result": 7, "id": "1" },
{ "jsonrpc": "2.0", "result": 19, "id": "2" }
]
배치 안에 알림이 섞여 있으면 그 알림에 대한 응답은 결과 배열에서 빠집니다. 모든 요청이 알림이라면 서버는 빈 배열이 아닌 아무 응답도 보내지 말아야 합니다. HTTP 라운드트립을 줄이고 싶을 때 유용한 기능이지만, 한 번에 너무 많은 작업을 묶으면 타임아웃 위험도 커지니 적당히 끊어서 보내는 게 좋습니다.
간단한 서버 만들어 보기
이론은 충분히 살펴봤으니 이제 직접 작은 서버를 만들어 보겠습니다. Bun 표준 라이브러리만 사용해서 HTTP 위에서 JSON-RPC 2.0을 처리해 볼게요.
const methods = {
add: ({ a, b }) => a + b,
subtract: ({ a, b }) => a - b,
};
function handle(request) {
if (request.jsonrpc !== "2.0" || typeof request.method !== "string") {
return {
jsonrpc: "2.0",
error: { code: -32600, message: "Invalid Request" },
id: request.id ?? null,
};
}
const fn = methods[request.method];
if (!fn) {
return {
jsonrpc: "2.0",
error: { code: -32601, message: "Method not found" },
id: request.id ?? null,
};
}
try {
const result = fn(request.params ?? {});
if (request.id === undefined) return null; // 알림은 응답 없음
return { jsonrpc: "2.0", result, id: request.id };
} catch (error) {
return {
jsonrpc: "2.0",
error: { code: -32603, message: error.message },
id: request.id ?? null,
};
}
}
Bun.serve({
port: 3000,
async fetch(req) {
const body = await req.json();
const response = Array.isArray(body)
? body.map(handle).filter(Boolean)
: handle(body);
return Response.json(response);
},
});
서버를 실행한 뒤 curl로 호출해 보면 동작을 바로 확인할 수 있습니다.
curl -X POST http://localhost:3000 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"add","params":{"a":3,"b":4},"id":1}'
{ "jsonrpc": "2.0", "result": 7, "id": 1 }
100줄도 안 되는 코드로 RPC 서버가 동작하는 모습을 보면, 왜 이 프로토콜이 가벼운 통신 채널이 필요한 곳마다 자주 등장하는지 이해가 됩니다.
자바스크립트의 JSON 객체에서 다룬 JSON.parse와 JSON.stringify만 있으면 어떤 언어에서든 비슷하게 구현할 수 있고요.
REST와 비교하면 어떤가요?
JSON-RPC와 REST 중에 뭐가 더 좋냐는 질문은 사실 답하기 어렵습니다. 둘은 서로 다른 문제를 푸는 도구라서요.
REST는 자원(Resource)이 명확하게 모델링되는 도메인에서 빛을 발합니다. 사용자, 게시글, 댓글처럼 CRUD가 중심인 시스템에서는 URL 구조와 HTTP 메서드만 봐도 의미가 드러나거든요. HTTP 캐싱, 콘텐츠 협상, 상태 코드 같은 웹 인프라를 자연스럽게 활용할 수 있다는 것도 큰 장점입니다.
반대로 동작(action)이 중심인 도메인에서는 REST가 어색해질 때가 많습니다.
“송금하기”, “재시도하기”, “노트북을 깨우기” 같은 메서드를 굳이 자원으로 비틀어 표현하다 보면 POST /accounts/123/transfers 같은 경로가 늘어나거든요.
이런 경우엔 JSON-RPC가 훨씬 직관적입니다. 그냥 transfer 메서드를 부르면 끝이니까요.
또한 JSON-RPC는 트랜스포트에 묶여 있지 않다는 점이 중요합니다. 같은 프로토콜을 HTTP로도, WebSocket으로도, 자식 프로세스의 stdio로도 그대로 쓸 수 있거든요. 이 특성 덕분에 MCP 서버는 로컬에서 돌아가는 stdio 기반 서버와 원격 HTTP 서버를 같은 메시지 포맷으로 다룰 수 있습니다.
실제로 어디에서 쓰이고 있나요?
가장 유명한 사례는 블록체인 노드입니다.
Ethereum의 JSON-RPC API는 eth_blockNumber, eth_getBalance 같은 메서드를 그대로 호출해 노드와 통신하고요.
비트코인 코어도 마찬가지로 JSON-RPC 인터페이스를 제공합니다.
최근에 다시 주목받는 이유는 AI 도구 생태계입니다. Model Context Protocol은 메시지 포맷으로 JSON-RPC 2.0을 채택했고, A2A 프로토콜도 같은 선택을 했습니다. 새로운 프로토콜을 처음부터 설계하는 것보다, 이미 검증된 단순한 메시지 포맷 위에 의미만 얹는 편이 훨씬 빠르고 안전하니까요. LSP(Language Server Protocol)도 JSON-RPC 위에서 돌아가는데요. VS Code 같은 에디터가 다양한 언어 서버와 표준화된 방식으로 대화할 수 있는 것도 이 덕분입니다.
마치며
JSON-RPC 2.0은 한 번 익혀두면 두고두고 도움이 되는 프로토콜입니다. 스펙이 짧아서 한나절이면 다 읽을 수 있고, 트랜스포트에 묶이지 않아서 다양한 환경에서 재사용할 수 있고, 무엇보다 메시지 포맷이 단순해서 디버깅이 편하거든요.
다음 단계로는 직접 만든 서버에 JSON Schema 검증을 붙여서 잘못된 파라미터가 들어왔을 때 -32602 에러를 자동으로 응답하도록 만들어 보는 걸 추천드립니다.
스펙을 직접 확인하고 싶다면 JSON-RPC 2.0 공식 사이트를 참고하세요.
This work is licensed under
CC BY 4.0