ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

RPC (Remote Procedure Call)

도입

네트워크를 없애는 기술이 아니라, 다른 프로세스나 다른 머신에 있는 기능을 로컬 함수 호출처럼 다루게 만들어 분산 시스템 프로그래밍을 단순화하려는 데 있다

원격에 있는 프로시저(함수, 메서드)를 호출합니다.

표면적으로는 로컬 메서드를 호출하는 것처럼 보이지만, 실제로는 네트워크 전송, 직렬화, 타임아웃, 재시도, 버전 호환성 같은 분산 시스템 문제가 뒤에 숨어 있습니다. 그래서 RPC를 이해한다는 것은 단순한 통신 방식 하나를 외우는 것이 아니라, 원격 호출을 로컬 호출처럼 보이게 만드는 추상화와 그 한계를 함께 이해하는 일에 가깝습니다.

필요성

RPC를 이해하면 서비스 간 통신을 단순 HTTP 호출 코드 묶음이 아니라, 인터페이스·직렬화·오류 모델·호출 의미까지 포함한 계약으로 다룰 수 있다

마이크로서비스나 분산 시스템에서는 한 서비스가 다른 서비스의 기능을 계속 호출합니다. 이때 매번 직접 HTTP 요청을 만들고 JSON 파싱을 반복하면, 비즈니스 로직과 통신 코드가 뒤섞이기 쉽습니다.

RPC는 이 통신을 “메서드 호출”처럼 표현하게 해 줍니다. 인터페이스를 먼저 정의하고, 클라이언트는 마치 로컬 객체를 부르듯 사용하며, 실제 네트워크 왕복과 직렬화는 프레임워크가 숨깁니다. 그래서 팀이 커질수록 RPC는 단순 편의 기능이 아니라 서비스 간 계약 모델로서의 의미가 커집니다.

RPC를 꼭 이해해야 하는 상황
  • 서비스 간 인터페이스를 명확하게 정의하고 싶을 때
  • 클라이언트 스텁으로 통신 코드를 단순화하고 싶을 때
  • 직렬화 방식, 오류 코드, 타임아웃 정책을 일관되게 통제해야 할 때
  • gRPC, JSON-RPC, RMI 같은 계열의 차이를 정리해야 할 때
  • 네트워크 호출을 로컬 호출처럼 쓰는 것이 왜 편리하면서도 위험한지 이해해야 할 때

정의

RPC는 다른 프로세스나 다른 컴퓨터에 있는 함수 또는 메서드를 호출하는 모델이며, 호출자는 로컬 함수처럼 보지만 실제로는 요청 메시지와 응답 메시지를 교환한다

RPC는 하나의 단일 제품 이름이 아니라 통신 모델 또는 설계 방식입니다. 어떤 시스템은 바이너리 프로토콜을 쓰고, 어떤 시스템은 JSON을 쓰고, 어떤 시스템은 같은 언어 런타임끼리만 통신합니다. 하지만 모두 공통으로 “원격 기능을 호출한다”는 구조를 가집니다.

즉, RPC를 이해할 때는 특정 프레임워크 문법보다 클라이언트가 원격 기능을 로컬 호출처럼 보이게 사용한다는 구조를 먼저 잡는 것이 중요합니다.

핵심 문장
RPC는 “원격”과 “호출”을 하나로 묶는 추상화입니다. 사용자는 메서드를 부른다고 느끼지만, 시스템은 사실 메시지를 주고받고 있습니다.

핵심 원리

RPC의 핵심은 로컬 함수 호출 인터페이스를 유지하되, 그 뒤에서 파라미터 직렬화·네트워크 전송·원격 실행·결과 역직렬화 과정을 자동화하는 데 있다

RPC 시스템은 보통 클라이언트 쪽에 stub 또는 client proxy를 제공합니다. 개발자는 그 객체의 메서드를 호출하고, 실제로는 스텁이 파라미터를 메시지로 감싸 네트워크로 보냅니다.

서버 쪽에서는 요청을 받아 적절한 구현체에 전달하고, 실행 결과를 다시 응답 메시지로 돌려보냅니다. 결국 RPC는 “원격 호출을 쉽게 만들어 준다”는 장점이 있지만, 동시에 네트워크를 숨기기 때문에 로컬 호출과 원격 호출의 차이를 과소평가하게 만들 위험도 있습니다.

기본 구조

RPC를 실무에서 이해하려면 인터페이스 정의, 직렬화, 전송, 클라이언트 스텁, 서버 구현, 오류 모델을 각각 다른 책임으로 나눠 보는 것이 좋다
구성 요소 역할 실무 포인트
Interface / IDL 호출 가능한 메서드와 타입 정의 계약이 먼저 있어야 클라이언트/서버가 독립적으로 개발 가능
Serialization 파라미터와 반환값을 바이트 또는 텍스트로 변환 성능, 호환성, 디버깅 난이도와 직결됨
Transport 메시지를 실제로 전달 TCP, HTTP/2, 소켓 등 방식이 다양함
Client Stub 로컬 메서드처럼 보이는 호출 인터페이스 제공 개발자 입장에서 RPC의 체감 UX를 결정함
Server Handler 들어온 호출을 실제 구현으로 연결 인증, 검증, 오류 매핑도 이 지점에서 자주 처리됨
Error Model 실패를 어떻게 표현할지 정의 네트워크 실패와 비즈니스 실패를 구분해야 함
일반적인 RPC 호출 흐름
클라이언트 코드
  ↓
클라이언트 스텁
  ↓
직렬화 / 전송
  ↓
서버 수신
  ↓
서버 핸들러 / 실제 구현
  ↓
결과 생성
  ↓
역직렬화 후 클라이언트 반환

기본 구현

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 모델은 로컬 호출 모델과 닮아 있지만, 실제로는 호출 메시지와 응답 메시지를 주고받는 구조이며 필요에 따라 동기식 또는 비동기식으로 구현될 수 있다

전통적인 RPC 설명에서는 호출자가 인자를 보내고, 서버가 처리 후 결과를 돌려주며, 호출자는 그 결과를 받아 계속 실행합니다. 이 흐름은 로컬 함수 호출의 정신 모델과 비슷합니다.

하지만 원격 호출은 네트워크 위에서 일어나기 때문에 로컬 호출과 완전히 같을 수는 없습니다. 호출 메시지와 응답 메시지를 주고받아야 하고, 전송이 불안정한 경우 타임아웃, 재전송, 중복 처리 같은 정책도 필요해질 수 있습니다. 또한 반드시 블로킹 방식일 필요는 없어서, 구현에 따라 비동기식 호출도 가능합니다.

중요한 감각
RPC는 “로컬 호출처럼 보이게 한다”가 목표이지, “네트워크가 사라진다”가 목표는 아닙니다. 지연, 실패, 재시도는 여전히 분산 시스템의 본질적인 문제입니다.

패턴 2. 대표 계열: gRPC, JSON-RPC, Java RMI

RPC는 하나의 단일 구현이 아니라 여러 계열로 발전해 왔으며, 같은 RPC라도 인터페이스 정의, 직렬화 방식, 언어 범위, 스트리밍 지원 수준이 다르다
계열 특징 강점 주의점
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를 실전에서 어렵게 만드는 가장 큰 이유는 로컬 호출과 달리 네트워크 실패가 항상 가능하다는 점이며, 그래서 타임아웃, 재시도, 멱등성을 함께 설계해야 한다

원격 호출은 언제든 느려질 수 있고, 응답이 오기 전에 연결이 끊길 수 있으며, 서버는 실제로 처리했는데 응답만 유실될 수도 있습니다. 이때 클라이언트가 재시도하면 중복 실행이 생길 수 있습니다.

그래서 RPC 설계에서는 얼마나 기다릴 것인가, 실패 시 다시 보낼 것인가, 다시 보내도 안전한가를 같이 봐야 합니다. gRPC도 클라이언트가 deadline/timeout을 지정할 수 있도록 하고, 전통적 RPC 문서도 전송 계층이 신뢰성을 주지 않으면 타임아웃과 재전송 정책을 구현 쪽에서 다뤄야 한다고 설명합니다.

실무에서 꼭 같이 설계해야 할 것
  • Timeout / Deadline : 무한 대기 방지
  • Retry : 일시적 실패 대응
  • Idempotency : 재시도 시 중복 처리 방지
  • Error Mapping : 네트워크 실패와 비즈니스 오류 구분
  • Cancellation : 더 이상 의미 없는 호출 중단
실전 감각
RPC에서 “호출이 실패했다”는 말은 단순하지 않습니다. 서버가 실행을 안 했는지, 했는데 응답만 안 왔는지, 중간에 일부만 처리됐는지를 항상 따져야 합니다.

RPC 와 REST 관점 차이

RPC가 “무슨 동작을 호출할 것인가”에 더 가깝다면, REST 스타일은 “어떤 자원에 대해 어떤 HTTP 의미를 적용할 것인가”에 더 가깝다고 볼 수 있다

RPC 스타일에서는 보통 메서드 이름이 중심입니다. 예를 들어 createOrder, cancelOrder, getBalance처럼 “행위”가 API 표면에 드러납니다.

반면 REST 스타일에서는 자원과 HTTP 의미를 더 강조합니다. 같은 기능도 /orders, /orders/{id} 같은 자원을 기준으로 나누고, 조회·생성·수정·삭제를 HTTP 메서드 의미에 맞춰 표현하려는 경향이 있습니다.

둘 중 하나가 절대적으로 우월한 것은 아닙니다. 강한 인터페이스 계약, 다국어 스텁, 내부 서비스 간 고성능 호출에는 RPC가 매우 잘 맞고, 공개 웹 API나 HTTP 의미 활용에는 자원 중심 스타일이 더 잘 맞을 수 있습니다.

한계와 주의점

RPC는 개발 경험을 단순하게 만들어 주지만, 네트워크를 지나간다는 사실까지 감춰 버리면 오히려 호출 비용과 실패 모델을 과소평가하게 만드는 부작용이 있다

로컬 함수 호출은 보통 빠르고, 실패 유형도 상대적으로 단순합니다. 하지만 RPC는 네트워크 지연, 패킷 손실, 서버 과부하, 역직렬화 오류, 버전 불일치 같은 문제를 항상 안고 있습니다.

그래서 RPC를 “그냥 함수 호출”처럼 다루면 서비스 간 채팅 같은 과도한 결합이 생기고, 호출 수가 폭증하며, 장애 전파도 빠르게 확산됩니다. RPC는 편리하지만, 그 편리함 때문에 분산 시스템 비용이 가려진다는 점이 가장 큰 함정입니다.

RPC의 대표적인 리스크
  • 원격 호출 비용을 로컬 함수 호출처럼 과소평가하기 쉬움
  • 서비스 간 결합도가 높아질 수 있음
  • 직렬화와 버전 호환성 관리가 필요함
  • 네트워크 실패가 호출 모델 전체를 흔들 수 있음
  • 재시도와 멱등성 설계를 빼먹으면 데이터 중복 문제가 생김

자주 하는 실수

RPC를 잘못 쓰는 가장 흔한 이유는 원격 호출을 정말 로컬 호출처럼 취급하고, 타임아웃·재시도·오류 모델을 뒤늦게 붙이려는 데 있다
  • 네트워크 호출 비용을 무시하고 RPC를 과도하게 세분화함
  • 타임아웃 없이 호출을 날려 장애 시 무한 대기함
  • 재시도는 넣었지만 멱등성을 고려하지 않음
  • 네트워크 오류와 비즈니스 오류를 한 종류 예외처럼 처리함
  • 직렬화 포맷과 버전 호환성 문제를 너무 늦게 고민함
  • 동기식 호출만 쌓아 서비스 간 대기 시간을 과도하게 만듦
  • Pub/Sub나 이벤트 기반 처리가 더 맞는 곳까지 RPC로만 해결하려 함

실무 루틴

RPC를 설계할 때는 프레임워크부터 고르기보다, 계약을 어떻게 정의할지, 어떤 실패 모델을 가질지, 어떤 호출이 정말 원격 메서드처럼 표현돼야 하는지부터 정리하는 순서가 맞다
  1. 먼저 이 통신이 정말 메서드 호출 모델에 잘 맞는지 본다.
  2. 인터페이스와 타입 계약을 먼저 정의한다.
  3. 직렬화 포맷과 버전 관리 전략을 정한다.
  4. 타임아웃, 재시도, 오류 코드, 멱등성을 함께 설계한다.
  5. 동기 호출이 맞는지, 비동기/스트리밍이 더 맞는지 판단한다.
  6. 로그, 추적, 메트릭을 포함한 관측 가능성까지 함께 설계한다.
  7. 마지막으로 gRPC, JSON-RPC, RMI 등 구체 기술을 선택한다.

디버깅

RPC 문제를 디버깅할 때는 “호출이 실패했다”라고 뭉뚱그리지 말고, 계약 문제인지·직렬화 문제인지·네트워크 문제인지·서버 실행 문제인지·재시도 문제인지 먼저 나눠야 한다
1
문제가 클라이언트 스텁, 직렬화, 전송, 서버 구현 중 어디에서 나는지 먼저 나눈다.
2
계약(IDL, 요청/응답 객체, 버전)이 서로 맞는지 확인한다.
3
타임아웃인지, 서버 오류인지, 네트워크 단절인지 구분한다.
4
재시도가 들어갔다면 중복 실행이 생겼는지도 확인한다.
5
동기 호출 체인이 길다면 어디서 대기가 누적되는지 추적한다.
RPC 장애를 볼 때 먼저 나눌 질문
- 계약이 서로 맞는가?
- 직렬화/역직렬화가 정상인가?
- 네트워크가 느린가, 끊겼는가?
- 서버가 실제로 실행했는가?
- 응답만 유실된 것은 아닌가?
- 재시도로 중복 실행이 생기지 않았는가?

요약

RPC의 본질은 원격 기능을 로컬 메서드처럼 다루게 만드는 추상화에 있으며, 실제 실무에서는 인터페이스 정의, 직렬화, 전송, 오류 모델, 타임아웃, 멱등성을 함께 설계해야 비로소 안전한 서비스 간 통신 모델이 된다
  • ✅ RPC는 Remote Procedure Call, 즉 원격 기능 호출 모델이다.
  • ✅ 로컬 메서드처럼 보이지만 실제로는 요청/응답 메시지를 주고받는다.
  • ✅ 좋은 RPC 설계는 인터페이스, 직렬화, 전송, 오류 모델을 함께 정의한다.
  • ✅ gRPC는 강한 계약과 스텁, 스트리밍에 강하고, JSON-RPC는 단순하고 가볍다.
  • ✅ Java RMI는 Java 객체 메서드를 다른 JVM에서 호출하는 방식이다.
  • ✅ RPC는 동기 호출처럼 보이기 쉽지만 네트워크는 본질적으로 비동기적이다.
  • ✅ 타임아웃, 재시도, 멱등성을 설계하지 않으면 원격 호출은 쉽게 위험해진다.
  • ✅ RPC는 강력하지만, 네트워크 비용을 로컬 호출처럼 과소평가하면 구조가 쉽게 무너진다.
728x90