도입
원격에 있는 프로시저(함수, 메서드)를 호출합니다.
표면적으로는 로컬 메서드를 호출하는 것처럼 보이지만, 실제로는 네트워크 전송, 직렬화, 타임아웃, 재시도, 버전 호환성 같은 분산 시스템 문제가 뒤에 숨어 있습니다. 그래서 RPC를 이해한다는 것은 단순한 통신 방식 하나를 외우는 것이 아니라, 원격 호출을 로컬 호출처럼 보이게 만드는 추상화와 그 한계를 함께 이해하는 일에 가깝습니다.
필요성
마이크로서비스나 분산 시스템에서는 한 서비스가 다른 서비스의 기능을 계속 호출합니다. 이때 매번 직접 HTTP 요청을 만들고 JSON 파싱을 반복하면, 비즈니스 로직과 통신 코드가 뒤섞이기 쉽습니다.
RPC는 이 통신을 “메서드 호출”처럼 표현하게 해 줍니다. 인터페이스를 먼저 정의하고, 클라이언트는 마치 로컬 객체를 부르듯 사용하며, 실제 네트워크 왕복과 직렬화는 프레임워크가 숨깁니다. 그래서 팀이 커질수록 RPC는 단순 편의 기능이 아니라 서비스 간 계약 모델로서의 의미가 커집니다.
- 서비스 간 인터페이스를 명확하게 정의하고 싶을 때
- 클라이언트 스텁으로 통신 코드를 단순화하고 싶을 때
- 직렬화 방식, 오류 코드, 타임아웃 정책을 일관되게 통제해야 할 때
- gRPC, JSON-RPC, RMI 같은 계열의 차이를 정리해야 할 때
- 네트워크 호출을 로컬 호출처럼 쓰는 것이 왜 편리하면서도 위험한지 이해해야 할 때
정의
RPC는 하나의 단일 제품 이름이 아니라 통신 모델 또는 설계 방식입니다. 어떤 시스템은 바이너리 프로토콜을 쓰고, 어떤 시스템은 JSON을 쓰고, 어떤 시스템은 같은 언어 런타임끼리만 통신합니다. 하지만 모두 공통으로 “원격 기능을 호출한다”는 구조를 가집니다.
즉, RPC를 이해할 때는 특정 프레임워크 문법보다 클라이언트가 원격 기능을 로컬 호출처럼 보이게 사용한다는 구조를 먼저 잡는 것이 중요합니다.
핵심 원리
RPC 시스템은 보통 클라이언트 쪽에 stub 또는 client proxy를 제공합니다. 개발자는 그 객체의 메서드를 호출하고, 실제로는 스텁이 파라미터를 메시지로 감싸 네트워크로 보냅니다.
서버 쪽에서는 요청을 받아 적절한 구현체에 전달하고, 실행 결과를 다시 응답 메시지로 돌려보냅니다. 결국 RPC는 “원격 호출을 쉽게 만들어 준다”는 장점이 있지만, 동시에 네트워크를 숨기기 때문에 로컬 호출과 원격 호출의 차이를 과소평가하게 만들 위험도 있습니다.
기본 구조
| 구성 요소 | 역할 | 실무 포인트 |
|---|---|---|
| Interface / IDL | 호출 가능한 메서드와 타입 정의 | 계약이 먼저 있어야 클라이언트/서버가 독립적으로 개발 가능 |
| Serialization | 파라미터와 반환값을 바이트 또는 텍스트로 변환 | 성능, 호환성, 디버깅 난이도와 직결됨 |
| Transport | 메시지를 실제로 전달 | TCP, HTTP/2, 소켓 등 방식이 다양함 |
| Client Stub | 로컬 메서드처럼 보이는 호출 인터페이스 제공 | 개발자 입장에서 RPC의 체감 UX를 결정함 |
| Server Handler | 들어온 호출을 실제 구현으로 연결 | 인증, 검증, 오류 매핑도 이 지점에서 자주 처리됨 |
| Error Model | 실패를 어떻게 표현할지 정의 | 네트워크 실패와 비즈니스 실패를 구분해야 함 |
일반적인 RPC 호출 흐름
클라이언트 코드
↓
클라이언트 스텁
↓
직렬화 / 전송
↓
서버 수신
↓
서버 핸들러 / 실제 구현
↓
결과 생성
↓
역직렬화 후 클라이언트 반환
기본 구현
syntax = "proto3";
service OrderService {
rpc GetOrder (GetOrderRequest) returns (GetOrderResponse);
}
message GetOrderRequest {
string order_id = 1;
}
message GetOrderResponse {
string order_id = 1;
string status = 2;
}
이런 식으로 인터페이스를 먼저 정의하고, 클라이언트는 생성된 스텁을 호출하며, 서버는 대응 구현을 작성하는 방식이 현대 RPC 프레임워크에서 매우 흔합니다.
{
"jsonrpc": "2.0",
"method": "getOrder",
"params": { "orderId": "A-1001" },
"id": 1
}
{
"jsonrpc": "2.0",
"result": { "orderId": "A-1001", "status": "PAID" },
"id": 1
}
패턴 1. 고전적인 RPC 호출 흐름
전통적인 RPC 설명에서는 호출자가 인자를 보내고, 서버가 처리 후 결과를 돌려주며, 호출자는 그 결과를 받아 계속 실행합니다. 이 흐름은 로컬 함수 호출의 정신 모델과 비슷합니다.
하지만 원격 호출은 네트워크 위에서 일어나기 때문에 로컬 호출과 완전히 같을 수는 없습니다. 호출 메시지와 응답 메시지를 주고받아야 하고, 전송이 불안정한 경우 타임아웃, 재전송, 중복 처리 같은 정책도 필요해질 수 있습니다. 또한 반드시 블로킹 방식일 필요는 없어서, 구현에 따라 비동기식 호출도 가능합니다.
패턴 2. 대표 계열: gRPC, JSON-RPC, Java RMI
| 계열 | 특징 | 강점 | 주의점 |
|---|---|---|---|
| gRPC | IDL과 스텁 기반, 기본적으로 Protocol Buffers 사용 | 강한 계약, 다국어 지원, 스트리밍 지원 | 바이너리 기반이라 사람이 바로 읽기 어렵고 생태계 학습 필요 |
| JSON-RPC | JSON 기반의 경량 RPC 프로토콜 | 단순하고 사람이 읽기 쉬움 | 구조가 단순한 만큼 별도 운영 규칙을 직접 정해야 할 수 있음 |
| Java RMI | Java 객체 메서드를 다른 JVM에서 호출 | Java 생태계 안에서는 자연스러운 모델 | 언어 종속성이 강하고 범용 웹 인터페이스와는 결이 다름 |
gRPC는 서비스 인터페이스를 먼저 정의하고, 생성된 클라이언트/서버 코드를 중심으로 통신합니다. 기본적으로 Protocol Buffers를 IDL과 메시지 형식으로 사용하고, 단일 요청-응답뿐 아니라 서버 스트리밍, 클라이언트 스트리밍, 양방향 스트리밍도 지원합니다.
JSON-RPC는 더 단순합니다. method, params, id 같은 구조를 가진 요청/응답 객체를 JSON으로 교환하며, transport agnostic이라 HTTP뿐 아니라 소켓이나 다른 메시징 환경에도 올릴 수 있습니다. 응답을 기대하지 않는 notification 개념도 있습니다.
Java RMI는 Java 객체의 원격 메서드 호출을 모델링합니다. 다른 JVM에서 실행 중인 Java 객체 메서드를 호출할 수 있고, 직렬화 기반으로 파라미터와 반환값을 주고받습니다. 따라서 “원격 객체 호출”에 매우 직접적인 느낌을 주지만, 범용 API보다는 Java 생태계 내부 분산 시스템에 더 가깝습니다.
패턴 3. 타임아웃, 재시도, 멱등성
원격 호출은 언제든 느려질 수 있고, 응답이 오기 전에 연결이 끊길 수 있으며, 서버는 실제로 처리했는데 응답만 유실될 수도 있습니다. 이때 클라이언트가 재시도하면 중복 실행이 생길 수 있습니다.
그래서 RPC 설계에서는 얼마나 기다릴 것인가, 실패 시 다시 보낼 것인가, 다시 보내도 안전한가를 같이 봐야 합니다. gRPC도 클라이언트가 deadline/timeout을 지정할 수 있도록 하고, 전통적 RPC 문서도 전송 계층이 신뢰성을 주지 않으면 타임아웃과 재전송 정책을 구현 쪽에서 다뤄야 한다고 설명합니다.
- Timeout / Deadline : 무한 대기 방지
- Retry : 일시적 실패 대응
- Idempotency : 재시도 시 중복 처리 방지
- Error Mapping : 네트워크 실패와 비즈니스 오류 구분
- Cancellation : 더 이상 의미 없는 호출 중단
RPC 와 REST 관점 차이
RPC 스타일에서는 보통 메서드 이름이 중심입니다. 예를 들어 createOrder, cancelOrder, getBalance처럼 “행위”가 API 표면에 드러납니다.
반면 REST 스타일에서는 자원과 HTTP 의미를 더 강조합니다. 같은 기능도 /orders, /orders/{id} 같은 자원을 기준으로 나누고, 조회·생성·수정·삭제를 HTTP 메서드 의미에 맞춰 표현하려는 경향이 있습니다.
둘 중 하나가 절대적으로 우월한 것은 아닙니다. 강한 인터페이스 계약, 다국어 스텁, 내부 서비스 간 고성능 호출에는 RPC가 매우 잘 맞고, 공개 웹 API나 HTTP 의미 활용에는 자원 중심 스타일이 더 잘 맞을 수 있습니다.
한계와 주의점
로컬 함수 호출은 보통 빠르고, 실패 유형도 상대적으로 단순합니다. 하지만 RPC는 네트워크 지연, 패킷 손실, 서버 과부하, 역직렬화 오류, 버전 불일치 같은 문제를 항상 안고 있습니다.
그래서 RPC를 “그냥 함수 호출”처럼 다루면 서비스 간 채팅 같은 과도한 결합이 생기고, 호출 수가 폭증하며, 장애 전파도 빠르게 확산됩니다. RPC는 편리하지만, 그 편리함 때문에 분산 시스템 비용이 가려진다는 점이 가장 큰 함정입니다.
- 원격 호출 비용을 로컬 함수 호출처럼 과소평가하기 쉬움
- 서비스 간 결합도가 높아질 수 있음
- 직렬화와 버전 호환성 관리가 필요함
- 네트워크 실패가 호출 모델 전체를 흔들 수 있음
- 재시도와 멱등성 설계를 빼먹으면 데이터 중복 문제가 생김
자주 하는 실수
- 네트워크 호출 비용을 무시하고 RPC를 과도하게 세분화함
- 타임아웃 없이 호출을 날려 장애 시 무한 대기함
- 재시도는 넣었지만 멱등성을 고려하지 않음
- 네트워크 오류와 비즈니스 오류를 한 종류 예외처럼 처리함
- 직렬화 포맷과 버전 호환성 문제를 너무 늦게 고민함
- 동기식 호출만 쌓아 서비스 간 대기 시간을 과도하게 만듦
- Pub/Sub나 이벤트 기반 처리가 더 맞는 곳까지 RPC로만 해결하려 함
실무 루틴
- 먼저 이 통신이 정말 메서드 호출 모델에 잘 맞는지 본다.
- 인터페이스와 타입 계약을 먼저 정의한다.
- 직렬화 포맷과 버전 관리 전략을 정한다.
- 타임아웃, 재시도, 오류 코드, 멱등성을 함께 설계한다.
- 동기 호출이 맞는지, 비동기/스트리밍이 더 맞는지 판단한다.
- 로그, 추적, 메트릭을 포함한 관측 가능성까지 함께 설계한다.
- 마지막으로 gRPC, JSON-RPC, RMI 등 구체 기술을 선택한다.
디버깅
RPC 장애를 볼 때 먼저 나눌 질문
- 계약이 서로 맞는가?
- 직렬화/역직렬화가 정상인가?
- 네트워크가 느린가, 끊겼는가?
- 서버가 실제로 실행했는가?
- 응답만 유실된 것은 아닌가?
- 재시도로 중복 실행이 생기지 않았는가?
요약
- ✅ RPC는 Remote Procedure Call, 즉 원격 기능 호출 모델이다.
- ✅ 로컬 메서드처럼 보이지만 실제로는 요청/응답 메시지를 주고받는다.
- ✅ 좋은 RPC 설계는 인터페이스, 직렬화, 전송, 오류 모델을 함께 정의한다.
- ✅ gRPC는 강한 계약과 스텁, 스트리밍에 강하고, JSON-RPC는 단순하고 가볍다.
- ✅ Java RMI는 Java 객체 메서드를 다른 JVM에서 호출하는 방식이다.
- ✅ RPC는 동기 호출처럼 보이기 쉽지만 네트워크는 본질적으로 비동기적이다.
- ✅ 타임아웃, 재시도, 멱등성을 설계하지 않으면 원격 호출은 쉽게 위험해진다.
- ✅ RPC는 강력하지만, 네트워크 비용을 로컬 호출처럼 과소평가하면 구조가 쉽게 무너진다.