포스트

IPC와 RPC 이해하기

프로세스 간 통신인 IPC와 원격 함수 호출을 추상화하는 RPC의 관계를 Socket, REST, gRPC와 함께 정리한다.

IPC와 RPC 이해하기

서로 다른 프로세스는 각각 독립된 주소 공간을 가지므로 다른 프로세스의 함수를 직접 호출하거나 객체를 그대로 참조할 수 없다.

따라서 프로세스 경계를 넘어 데이터를 전달하기 위한 통신 메커니즘이 필요하다.

여기서 IPC와 RPC가 등장한다.

핵심부터 정리하면 다음과 같다.

IPC는 프로세스 사이에 데이터를 전달하는 메커니즘이고, RPC는 그 통신을 원격 함수 호출처럼 사용할 수 있도록 만든 추상화다.

같은 프로세스에서는 직접 호출할 수 있다

같은 프로세스 안에 있는 함수라면 일반적인 함수 호출이 가능하다.

1
2
3
4
5
6
7
Process A

getUser(10)
    ↓
getUser() 실행
    ↓
User 반환

함수와 호출자가 같은 주소 공간에 있기 때문이다.

플러그인도 Host와 같은 프로세스와 런타임에 로드된다면 비슷하다.

1
2
3
4
5
[ Host Process ]

Core
  ↓ 직접 함수 호출
Plugin

Python 애플리케이션이 Python Plugin을 직접 import해서 사용하는 구조가 대표적이다.

프로세스가 다르면 직접 호출할 수 없다

Plugin이나 Service를 별도 프로세스로 분리하면 상황이 달라진다.

1
2
3
4
5
[ Host Process ]        [ Plugin Process ]

      Core                    Plugin
        │                       ↑
        └──── 직접 호출 불가 ────┘

두 프로세스의 메모리 공간이 다르기 때문에 함수나 객체를 그대로 참조할 수 없다.

따라서 별도의 통신수단이 필요하다.

IPC

IPC(Inter-Process Communication)는 프로세스 사이에서 데이터를 주고받기 위한 메커니즘의 총칭이다.

대표적인 IPC 수단은 다음과 같다.

1
2
3
4
5
6
IPC
├─ Pipe
├─ Unix Domain Socket
├─ Message Queue
├─ Shared Memory
└─ 기타 OS 제공 메커니즘

예를 들어 Unix Domain Socket을 사용하면 같은 머신의 두 프로세스가 Socket 추상화를 이용해 통신할 수 있다.

1
2
3
4
5
6
7
Process A
    ↓
Unix Domain Socket
    ↓
Kernel
    ↓
Process B

이 경우 TCP/IP 네트워크를 사용하지 않아도 된다.

Socket은 IPC 전용 개념은 아니다. TCP/UDP Socket을 사용하면 다른 머신의 프로세스와도 통신할 수 있다.

1
2
3
4
5
6
7
8
9
10
                 Socket
                /      \
               /        \
       같은 머신          다른 머신
          ↓                  ↓
   Unix Domain Socket    TCP/UDP Socket
          ↓                  ↓
      Process B            Network
                              ↓
                           Process B

따라서 모든 IPC가 Socket인 것도 아니고, 모든 Socket이 로컬 IPC인 것도 아니다.

IPC만 사용하면 무엇을 해야 하는가

IPC는 기본적으로 데이터를 전달하는 수단이다.

예를 들어 Process A가 Process B에게 사용자 조회를 요청한다고 하자.

1
2
3
4
5
6
7
8
9
10
11
Process A

{
  "requestId": 17,
  "type": "GET_USER",
  "id": 10
}
       ↓
      IPC
       ↓
Process B

Process B는 메시지를 읽고 어떤 기능을 실행할지 직접 판단해야 한다.

1
2
if type == "GET_USER":
    getUser(id)

규모가 커지면 개발자가 직접 처리해야 하는 것이 많아진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
요청 메시지 형식 정의
      ↓
직렬화
      ↓
전송
      ↓
요청 종류 판별
      ↓
기능 실행
      ↓
응답 메시지 생성
      ↓
요청 ID와 응답 매칭
      ↓
역직렬화
      ↓
오류 / timeout 처리

이 복잡성을 더 높은 수준에서 추상화하는 방법 중 하나가 RPC다.

RPC

RPC(Remote Procedure Call)는 다른 프로세스 또는 원격 시스템의 기능을 로컬 함수 호출처럼 사용할 수 있도록 만드는 통신 모델이다.

개발자는 다음처럼 사용하고 싶다.

1
user = client.getUser(10)

하지만 실제로 다른 프로세스의 getUser() 함수를 직접 호출하는 것은 아니다.

내부적으로는 다음과 같은 과정이 일어난다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Process A                         Process B

client.getUser(10)
       │
       ↓
   Client Stub
       │
       │  method = getUser
       │  args   = 10
       │
       ├──── IPC / Network ─────→ RPC Server
                                  │
                                  ↓
                              getUser(10)
                                  │
                                  ↓
                                User
       ←────── Response ──────────┘
       │
       ↓
   return User

즉 RPC는 다른 프로세스의 메모리에 들어가 함수를 직접 호출하는 기술이 아니다.

함수 호출을 메시지로 변환하고, 상대가 그 메시지를 해석해 대응하는 기능을 실행한 뒤 결과를 돌려주는 과정을 함수 호출처럼 보이게 만드는 추상화다.

Client Stub / Proxy

RPC 호출자 쪽에는 일반적으로 Client Stub 또는 Client Proxy가 존재한다.

1
2
3
4
5
6
7
8
9
10
11
Application
    │
    │ client.getUser(10)
    ▼
Client Stub / Proxy
    │
    ├─ 요청 생성
    ├─ 직렬화
    ├─ 전송
    ├─ 응답 대기
    └─ 역직렬화

개발자는 통신 과정을 직접 처리하지 않고 일반 메서드를 호출하는 것과 비슷한 방식으로 사용할 수 있다.

Server 쪽에도 요청을 실제 구현 코드에 연결하는 Handler 또는 Stub이 존재할 수 있다.

1
2
3
4
5
6
7
Client                           Server

Application                     Service
    ↓                              ↑
Client Stub                    Server Stub
    │                              ↑
    └──────── Communication ────────┘

RPC Server도 요청을 기다린다

RPC Server도 웹 서버와 마찬가지로 호출 요청을 받을 수 있도록 endpoint를 열고 대기한다.

1
2
3
4
5
6
7
8
9
10
11
12
RPC Client                         RPC Server

client.add(10, 20)
      │
      │ Request
      ├──────────────────────────→ 수신
      │                              ↓
      │                          add(10,20)
      │                              ↓
      │                             30
      │ Response                     │
      ←──────────────────────────────┘

네트워크 RPC라면 TCP Socket이나 HTTP 같은 네트워크 통신 계층을 사용할 수 있고, 같은 머신이라면 Unix Domain Socket 같은 IPC 메커니즘을 사용할 수도 있다.

IPC와 RPC는 경쟁 관계가 아니다

IPC와 RPC 중 하나를 선택하는 관계로 생각하면 혼란이 생긴다.

RPC는 IPC를 대체하는 것이 아니라 IPC 또는 네트워크 통신 위에 올라갈 수 있는 더 높은 수준의 추상화다.

1
2
3
4
5
6
7
8
9
RPC
 ↓
메시지 / 직렬화 / 요청-응답 처리
 ↓
IPC 또는 Network Transport
 ↓
Unix Socket / TCP Socket / Pipe / ...
 ↓
OS

예를 들어 단순 메시지 전달만 필요하다면 IPC를 직접 사용할 수 있다.

1
2
3
4
5
Process A
   ↓ "hello"
Unix Socket
   ↓
Process B

반면 상대 프로세스의 기능을 API처럼 호출하고 싶다면 RPC를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
Process A

plugin.format("a.md")
       ↓
      RPC
       ↓
 Unix Domain Socket
       ↓
Process B

RPC를 쓰는 이유는 IPC를 사용하지 않기 위해서가 아니라 IPC 또는 네트워크 통신의 복잡성을 함수 호출이라는 프로그래밍 모델로 숨기기 위해서다.

RPC의 주요 이점

IPC를 직접 사용하면 메시지 규격과 요청-응답 처리를 애플리케이션에서 직접 설계해야 한다.

RPC Framework를 사용하면 이러한 공통 문제를 대신 처리할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
명확한 Interface Contract
        ↓
Client / Server Stub
        ↓
Serialization / Deserialization
        ↓
Request / Response Matching
        ↓
Timeout / Error / Cancellation
        ↓
실제 IPC / Network Transport

따라서 RPC는 특히 상대방의 기능을 호출한다는 모델이 자연스러운 시스템에서 유용하다.

REST와 RPC

REST와 RPC는 밑바닥 통신 구조만 보면 상당히 비슷하다.

둘 다 결국 다른 프로세스나 서버에 요청을 보내고 결과를 돌려받는다.

1
2
3
4
5
6
7
8
9
10
11
Client
  ↓
Request
  ↓
Communication
  ↓
Server
  ↓
Logic 실행
  ↓
Response

차이는 외부에 보여주는 모델에 있다.

REST는 Resource 중심이다.

1
2
3
GET    /users/10
POST   /users
DELETE /users/10

RPC는 Procedure / Method 중심이다.

1
2
3
getUser(10)
createUser(...)
deleteUser(10)

즉 다음과 같이 볼 수 있다.

1
2
3
4
5
REST
→ 원격 Resource를 HTTP 방식으로 조작

RPC
→ 원격 Function / Method를 호출하는 방식으로 추상화

둘 다 Client-Server Request/Response 통신이라는 더 큰 구조 안에 존재한다.

gRPC

gRPC는 RPC라는 개념을 실제로 사용할 수 있도록 만든 대표적인 RPC Framework다.

1
2
3
4
5
6
7
RPC                     개념 / 모델
 ↓
gRPC                    구체적인 구현 기술
 ├─ Protocol Buffers
 ├─ Interface Contract
 ├─ Client / Server Stub
 └─ HTTP/2 기반 통신

일반적으로 .proto 파일로 서비스 계약을 정의한다.

1
2
3
4
service UserService {
  rpc GetUser(GetUserRequest)
      returns (GetUserResponse);
}

이 계약을 기반으로 여러 언어의 Client / Server 코드를 생성할 수 있다.

1
2
3
4
5
6
7
8
9
10
              user.proto
                  │
          ┌───────┴────────┐
          ↓                ↓
     Client Stub       Server Stub
          │                │
getUser(10) ───────────────→│
          │            실제 로직 실행
          │←─────────────── │
        User

개념적으로 통신 계층은 다음처럼 내려갈 수 있다.

1
2
3
4
5
6
7
8
9
10
11
Application
    ↓
gRPC
    ↓
HTTP/2
    ↓
TCP
    ↓
Socket API
    ↓
OS Network Stack

따라서 gRPC와 HTTP가 완전히 대체 관계인 것은 아니다. gRPC 자체가 HTTP/2를 이용한다.

REST 대신 gRPC를 사용하는 이유

gRPC는 특히 서비스 간 통신에서 다음과 같은 장점이 있다.

1
2
3
4
5
6
7
8
9
.proto 기반의 강한 계약
        ↓
타입이 있는 Client / Server 코드 생성
        ↓
Protocol Buffers 기반 직렬화
        ↓
효율적인 서비스 간 호출
        ↓
Streaming 지원

REST는 HTTP와 JSON 생태계를 그대로 활용할 수 있고 사람이 읽거나 curl 등으로 테스트하기 쉽다는 장점이 있다.

따라서 흔히 다음과 같은 선택이 가능하다.

1
2
3
4
5
6
7
8
9
Browser / External Client
          ↓
       REST API
          ↓
      Application
          ↓
       gRPC
          ↓
   Internal Service

절대적인 규칙은 아니며 시스템의 요구사항에 따라 선택한다.

RPC와 보안

RPC Server를 네트워크에 공개하면 REST API를 공개하는 것과 마찬가지로 공격 표면이 생긴다.

1
2
3
4
5
6
7
8
9
Untrusted Network
       ↓
   RPC Server
       ↓
Authentication
       ↓
Authorization
       ↓
RPC Method

일반적으로 다음과 같은 요소를 고려해야 한다.

1
2
3
4
5
6
7
TLS
Authentication
Authorization
Input Validation
Rate Limiting
Firewall / Network Policy
Audit Logging

RPC가 코드상으로는 평범한 함수 호출처럼 보인다는 점 때문에 실제로는 네트워크와 보안 경계를 넘는 호출이라는 사실을 놓치지 않는 것이 중요하다.

Plugin Architecture와 RPC

RPC를 살펴보게 된 출발점은 Plugin Architecture였다.

Host와 Plugin이 같은 프로세스에 있으면 직접 호출이 가능하다.

1
2
3
4
5
6
7
[ Same Process ]

Host
 ↓
Plugin API
 ↓
Plugin

하지만 Plugin을 별도 프로세스로 격리하면 직접 함수 호출이 불가능하다.

1
2
3
4
5
[ Host Process ]           [ Plugin Process ]

Host                            Plugin
 │                                ↑
 └──────── Process Boundary ───────┘

이때 IPC를 이용해 메시지를 직접 주고받을 수도 있고, 그 위에 RPC를 구성해 Plugin API를 함수 호출처럼 제공할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Host
 │
 │ plugin.doSomething()
 ▼
RPC Client
 │
 ▼
IPC / Socket
 │
 ▼
Plugin Process
 │
 ▼
doSomething()

이 방식은 Host와 Plugin의 런타임이나 언어를 분리하고 장애를 격리하는 데 유리할 수 있다.

대신 직렬화, 통신 비용, 프로세스 lifecycle 등의 복잡성이 추가된다.

1
2
3
4
5
6
7
8
9
10
In-process Plugin

장점: 빠름, 단순함, 객체 직접 공유
단점: Host와 강하게 결합, 장애 격리 어려움


Out-of-process Plugin + RPC

장점: 격리, 언어 선택 자유도, 명확한 경계
단점: IPC/RPC와 직렬화 필요, 구조 복잡

정리

IPC와 RPC의 관계를 가장 단순하게 정리하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
RPC
= "상대 기능을 함수처럼 호출하고 싶다"
        ↓
Request / Response 추상화
        ↓
IPC / Network
= "프로세스 사이에 데이터를 전달한다"
        ↓
Socket / Pipe / Message Queue / ...
= 실제 통신 메커니즘

따라서 다음 세 문장을 기억하면 된다.

IPC는 프로세스 사이에 데이터를 전달한다.

Socket은 IPC와 네트워크 통신에 사용할 수 있는 통신 수단 중 하나다.

RPC는 그 통신을 원격 함수 호출처럼 사용할 수 있도록 추상화한다.

RPC의 핵심 가치는 단순히 데이터를 전달하는 데 있지 않다.

프로세스나 네트워크 경계를 넘는 통신의 복잡성을 익숙한 함수 호출 모델로 감추고, 명확한 Interface Contract를 제공하는 것이 RPC의 핵심이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.