,

[설계 판단] RPC를 언제 쓰는가 — 라이브러리 통합 시 7가지 결정 기준

RPC를 언제 쓰는가, 그리고 이 프로젝트는 왜 RPC를 쓰는가

이 글은 두 가지를 정리한다.

  1. RPC가 무엇이고 어떤 문제를 해결하기 위한 도구인가
  2. detection-mvp/image-extract가 PDFium 호출 방식으로 RPC(go-pdfiummulti_threaded 백엔드)를 선택한 정당성

“동료가 왜 이렇게 짰냐고 물었을 때, 막힘 없이 답할 수 있는 근거”를 목표로 한다.


1. RPC란 무엇인가

RPC(Remote Procedure Call)는 “함수 호출처럼 보이지만 실제로는 다른 실행 단위에서 수행되는 호출”이다. “다른 실행 단위”는 다음 중 하나다.

  • 같은 머신의 다른 프로세스 (IPC 기반 RPC, 본 프로젝트가 사용하는 형태)
  • 네트워크 너머의 다른 머신 (네트워크 RPC, 일반적인 마이크로서비스)
  • 같은 프로세스의 다른 언어 런타임 (드물지만 가능)

핵심은 “호출하는 쪽의 코드는 평범한 함수 호출처럼 작성하지만, 실제 실행은 다른 곳에서 일어난다”는 점이다.

go-pdfiummulti_threaded 백엔드에서 일어나는 흐름:

[메인 프로세스 image-extract]                  [자식 프로세스 pdfium-worker]
  pdfium.RenderToFile(...)
  └─ net/rpc 클라이언트
     └─ encoding/gob 직렬화
        └─ Unix Domain Socket 송신 ──►  수신
                                        └─ encoding/gob 역직렬화
                                           └─ CGO → libpdfium.so
                                              └─ PDF 렌더링 + PNG 인코딩
                                                 ◄── 응답 직렬화 송신
  ◄─ 응답 역직렬화
  return 값

호출자가 작성하는 코드는 단 한 줄(pdfium.RenderToFile(...))이지만, 그 한 줄 뒤에는 직렬화·소켓·프로세스 분리가 모두 들어있다. 이것이 RPC의 가치이자 비용이다.


2. RPC가 해결하려는 문제 — 언제 RPC를 써야 하는가

RPC는 도구 자체가 목적이 아니다. 다음 중 하나 이상의 문제를 해결하려는 수단이다.

2.1 격리(Isolation)

같은 프로세스 안에서 돌면 한쪽의 segfault, 메모리 손상, 무한 루프가 전체 프로세스를 죽인다. 별도 프로세스에 두면 워커만 죽고 메인은 살아남으며, 감지 즉시 새 워커를 spawn할 수 있다.

판별 질문: “이 라이브러리가 죽으면 내 메인 서비스도 함께 죽어도 되는가?” — 답이 No이면 격리가 필요하다.

2.2 동시성 제약 우회

thread-unsafe한 라이브러리는 한 프로세스 안에서 한 번에 한 호출만 받을 수 있다. 진정한 병렬성을 얻으려면 N개의 독립 프로세스를 띄워야 하고, 그 프로세스들과 통신하려면 RPC가 거의 강제된다.

대표적 예: PDFium, ImageMagick의 일부 연산, MATLAB 엔진, 일부 ML 추론 라이브러리.

2.3 영속 상태(State)

라이브러리 핸들/포인터가 여러 호출에 걸쳐 유효해야 하는 경우, 호출 단위가 아니라 세션 단위로 워커가 살아있어야 한다. 매 호출마다 새 프로세스를 띄우는 Command 패턴은 부적합하다.

2.4 다른 언어/런타임 호출

언어 간 직접 바인딩이 없거나 어려울 때 RPC가 다리 역할을 한다. Python ML 모델을 Go 서버에서 호출, C++ 엔진을 Java에서 호출 등.

2.5 OS 자원 제어와 샌드박싱

별도 프로세스에는 cgroup, ulimit, seccomp 등을 OS 단위로 적용할 수 있다. 같은 프로세스에서는 메인 전체에 적용되므로 부분 격리가 불가능하다.

2.6 독립 배포·확장

“PDF 처리 서버 10대, API 서버 100대”처럼 독립 스케일링이 필요하면 네트워크 RPC(마이크로서비스)가 된다.


3. RPC가 부적합한 경우

  • 호출 빈도가 매우 높고 한 번 호출이 매우 가벼운 경우: 호출당 ~수백 μs의 IPC 오버헤드가 처리 시간을 압도한다.
  • 신뢰되고 안전한 라이브러리: 죽지 않고 thread-safe하면 격리가 필요 없다.
  • 상태가 없는 1회성 작업: Command 패턴(fork+exec)이 더 단순하다.

4. 통합 방식 비교

항목 직접 호출 (FFI/CGO) Command (fork+exec) 같은 머신 RPC 네트워크 RPC
호출당 오버헤드 ~1~10 μs ~10~150 ms ~50~500 μs ~수 ms ~ 수십 ms
격리 없음 강 (매 호출) 강 (워커 분리) 강 (호스트 분리)
영속 상태 OK 불가 (매번 초기화) OK (워커 보유) OK (서버 보유)
병렬성 (thread-unsafe) 없음 있음 (N프로세스) 있음 (N워커) 있음 (N호스트)
배포 단위 1 바이너리 1 메인 + 외부 도구 1 메인 + 1 워커 N개 독립 서비스
개발 복잡도 낮음 중-상

Command 패턴이 적합한 경우

  • 호출 빈도가 매우 낮음 (배치, 정기 작업)
  • 호출 사이에 상태가 없어도 됨
  • 외부 도구가 CLI 형태로 존재 (예: ffmpeg, pdftoppm)

직접 호출이 적합한 경우

  • 라이브러리가 thread-safe하고 안정적
  • 같은 언어 또는 잘 만들어진 바인딩 존재
  • 호출 빈도 매우 높음, 호출당 작업량 작음

RPC가 적합한 경우

  • 위 조건 중 두 개 이상 해당
  • 호출당 작업량이 IPC 오버헤드보다 충분히 큼
  • 운영 안정성이 호출 효율보다 중요

5. 본 프로젝트에서 RPC를 선택한 정당성

detection-mvp/image-extractgo-pdfiummulti_threaded 백엔드를 사용한다. 격리, 동시성 제약 우회, 영속 상태가 동시에 적용되기 때문이다.

5.1 격리 — PDFium의 크래시 가능성

PDFium은 C++ 코드베이스로 작성된 PDF 파서·렌더러이며, 외부에서 받은 임의의 PDF를 처리한다. 손상된 PDF에 의한 segfault, 악의적 PDF에 의한 메모리 손상, 무한 루프/메모리 폭주가 위협이다.

“처리 중인 PDF 1건이 잘못되어 다른 PDF 99건의 처리를 중단시키지 않는다”는 보장이 격리에서 온다.

5.2 동시성 — PDFium의 thread-safety 부재

PDFium은 공식적으로 thread-safe하지 않다. single_threaded 백엔드는 글로벌 mutex로 호출을 직렬화한다. PDF 4개를 동시에 처리하려면 PDFium 인스턴스 4개, 즉 4개의 독립 프로세스가 필요하다.

5.3 영속 상태 — PDF 핸들의 수명

FPDF_LoadDocument(path)        → FPDF_DOCUMENT 핸들
FPDF_GetPageCount(doc)         → 핸들 사용
RenderToFile({Page: doc, idx}) → 핸들 사용
FPDF_CloseDocument(doc)        → 핸들 해제

Command 패턴이라면 매 호출마다 PDF를 다시 로드해야 한다. 11페이지 PDF에서 파싱이 11번 반복된다. RPC + 영속 워커는 PDF 1번 로드, 핸들 재사용으로 N번 렌더.

5.4 비용 — 무엇을 치르는가

항목 비용
호출당 IPC 오버헤드 ~50~500 μs
워커 시작 비용 ~50~200 ms (1회)
배포 단위 증가 image-extract + pdfium-worker 두 바이너리
데이터 전송 비용 큰 비트맵 전송 시 ~50~130 ms (RenderToFile로 회피 가능)

5.5 트레이드오프 정량

작업 단위 PDFium 처리 시간 RPC 오버헤드 오버헤드 비율
RenderToFile (페이지 PNG, 300 DPI) ~200~500 ms ~1 ms ~0.3%
FPDF_GetPageCount ~수 ms ~0.1~0.5 ms ~5~10%
FPDFBitmap_GetWidth (단순 getter) ~수 μs ~0.1~0.5 ms ~수십 배

페이지 단위 렌더처럼 호출 1회의 처리량이 무거운 경우 RPC 비용은 묻힌다. 이것이 현재 코드가 RenderToFile을 사용해 워커 안에서 PNG 인코딩과 디스크 쓰기까지 완료시키는 이유다.

5.6 “왜 이렇게 짰냐” — 1줄 답

“PDFium이 thread-unsafe하고 신뢰 못 할 PDF에 죽을 수 있어서, 격리와 병렬성을 동시에 얻으려고 별도 프로세스에 띄우고 RPC로 호출한다. PDF 핸들이 호출 사이에 살아있어야 해서 영속 워커이고, 호출당 ~수백 μs의 IPC 오버헤드는 페이지 렌더 ~수백 ms 앞에서는 무시할 수준이다.”


6. 결정 트리 — 새 라이브러리 통합 시 7가지 질문

  1. 이 라이브러리가 비정상 입력에 죽을 수 있는가?
    → Yes면 +RPC (격리 필요)
  2. thread-safe한가? 동시성이 필요한가?
    → thread-unsafe + 동시성 필요 → +RPC (병렬성 확보)
  3. 호출 사이에 상태(핸들/세션)가 살아있어야 하는가?
    → Yes면 RPC > Command
  4. 내 언어에서 직접 호출할 안정적 바인딩이 있는가?
    → No면 +RPC 또는 FFI
  5. OS 단위 자원 한도/샌드박싱이 필요한가?
    → Yes면 +RPC
  6. 호출 빈도가 매우 높고 호출당 일이 매우 가벼운가?
    → Yes면 -RPC (직접 호출이 유리)
  7. 독립 배포·확장이 필요한가?
    → Yes면 네트워크 RPC

본 프로젝트 적용: 1=Yes, 2=Yes, 3=Yes, 4=Yes(바인딩 있으나 CGO 필요), 5=No, 6=No(렌더는 무거움), 7=No → 격리 + 병렬성 + 영속 상태가 필요한 단일 머신 IPC RPC가 정답.


7. 의사결정 사후 점검 항목

  • 워커 크래시 빈도: 의미 있게 발생하면 격리가 가치를 입증하는 셈
  • PDF 동시 처리량 vs 워커 수: 비례하면 병렬성이 정당화됨
  • 호출당 RPC 오버헤드 측정: chatty 패턴이 늘어나면 API 설계를 묶는 방향으로 재정비할 신호
  • 메모리 누수 누적 여부: 워커 메모리 증가 시 워커 재시작 정책 추가 신호

부록. 사용 중인 통신 스택

구성요소 사용 기술
RPC 프로토콜 Go 표준 net/rpc
직렬화 포맷 encoding/gob (Go 전용 바이너리 포맷)
트랜스포트 Unix Domain Socket (Linux/macOS) / Named Pipe (Windows)
프로세스 관리 hashicorp/go-plugin

.proto 파일이나 *.pb.go 생성 코드는 없다 (Protobuf/gRPC 미사용). 사용자가 직접 작성해야 하는 RPC 코드는 0줄이다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다