원격 호출 가능한 메서드
고친 사람 github-actions[bot]
원격 호출 가능한 메서드는 서버의 기능 하나를 네트워크 너머의 프로그램이 부를 수 있게 열어 줍니다. 서버는 이런 메서드를 서비스 정의에 적어 밖에 알립니다. 클라이언트는 거기 적힌 메서드만 부를 수 있습니다. 적히지 않은 메서드는 밖에서 보이지 않습니다.
쉽고 빠른 이해
원격 호출 가능한 메서드는 다른 컴퓨터가 부를 수 있게 열어 둔 서버의 기능입니다. 주문 서버라면 「주문 만들기」와 「주문 찾기」가 이런 메서드입니다.
서버 안에는 메서드가 훨씬 많습니다. 전부 밖에 열면 아무나 내부 계산까지 부를 수 있습니다. 서버는 어느 메서드도 마음 놓고 고치지 못하게 됩니다. 그래서 밖에서 부를 것만 골라 약속으로 적어 둡니다.
- 서버가 열 메서드의 이름과 주고받을 값을 약속 파일(서비스 정의)에 적습니다
- 클라이언트는 그 메서드를 내 메서드처럼 부릅니다. 도구가 만든 코드가 호출을 서버로 실어 보냅니다
- 서버는 이름을 보고 맞는 구현을 돌립니다. 결과는 같은 길로 돌아옵니다
대가는 호출이 네트워크를 건넌다는 것입니다. 내 메서드보다 느립니다. 응답이 아예 안 올 수도 있습니다. 그래서 부르는 쪽은 얼마나 기다릴지 정해 둡니다. 실패를 다루는 코드도 따로 짭니다.
이 방식은 서버끼리 정해진 동작을 주고받을 때 씁니다. 부르는 쪽이 결과를 바로 기다리는 경우입니다. 브라우저 같은 바깥 클라이언트에 열 때는 웹 주소로 부르는 방식을 고릅니다. 결과를 기다릴 필요가 없으면 메시지를 줄 세워 두는 큐를 씁니다.
상세
은행 창구를 떠올려 봅시다. 은행 안에서 직원들이 하는 일은 많습니다. 손님은 그중 창구에 걸린 업무 목록에 있는 것만 부탁할 수 있습니다.
원격 호출 가능한 메서드가 이 창구 업무입니다. 서버 프로그램 안에는 메서드가 많습니다. 그중 다른 컴퓨터가 부르라고 골라서 내놓은 것이 원격 호출 가능한 메서드입니다. 주문 서버라면 「주문 만들기」와 「주문 찾기」는 열어 둡니다. 재고를 셈하는 내부 메서드는 열지 않습니다.
이 절은 먼저 원격 호출이 무엇인지 짚습니다. 다음으로 메서드를 골라서 여는 이유를 봅니다. 이어서 메서드 한 줄의 생김새와 호출이 서버까지 가는 길을 따라갑니다. 그다음은 내 프로그램 안의 메서드와 무엇이 다른지, 그 차이 때문에 생기는 설계 고민입니다. 끝으로 이 방식을 쓰는 때와 다른 방식을 고르는 때를 가릅니다.
원격 프로시저 호출
원격 프로시저 호출(RPC, Remote Procedure Call)은 다른 컴퓨터에 있는 함수를 내 코드의 함수처럼 부르는 방식입니다. 프로시저는 함수의 옛 이름입니다. 객체 지향 언어에서는 같은 것을 메서드라고 부릅니다.
부르는 쪽 코드에는 평범한 메서드 호출 한 줄만 보입니다. 메서드 이름과 인자는 네트워크를 건너 서버로 갑니다. 서버가 그 메서드를 돌린 결과가 되돌아옵니다. 원격 호출 가능한 메서드는 이 방식으로 부를 수 있는 메서드를 말합니다.
메서드를 골라서 여는 이유
서버의 메서드를 전부 밖에 열면 세 가지가 곤란해집니다. 첫째, 밖에서 내부 계산이나 관리용 기능까지 부를 수 있습니다. 둘째, 서버는 어느 메서드도 마음 놓고 고칠 수 없습니다. 누가 부르고 있을지 모르기 때문입니다.
셋째, 클라이언트가 무엇을 부를 수 있는지 알 길이 없습니다. 서버와 클라이언트는 다른 팀이 다른 언어로 짜는 일이 흔합니다. 함께 볼 목록이 없으면 이름 하나가 어긋나도 돌려 봐야 압니다.
그래서 서버는 밖에 열 메서드만 골라 서비스 정의에 적습니다. 적힌 메서드는 바깥과의 약속이 됩니다. 서버는 그 약속만 지키면 됩니다. 적히지 않은 메서드는 안에서 마음대로 고칠 수 있습니다.
서비스 정의에 적힌 한 줄
서비스 정의는 대개 특정 프로그래밍 언어가 아닌 전용 문법으로 적습니다. 이 문법을 인터페이스 정의 언어(IDL, Interface Definition Language)라고 부릅니다. 서버가 Go 로, 클라이언트가 자바로 짜여 있어도 둘은 같은 IDL 파일 하나를 봅니다.
아래는 프로토콜 버퍼라는 IDL 로 적은 주문 서비스입니다. 메서드 하나가 한 줄입니다.
service OrderService { // 서비스 이름
rpc GetOrder(GetOrderRequest) // 받는 값
returns (Order); // 돌려주는 값
}
service 는 메서드들을 한 묶음으로 엮는 낱말입니다. rpc 로 시작하는 줄이 원격 호출 가능한 메서드
하나입니다. 이 줄은 세 가지를 정합니다. 메서드 이름, 받는 값의 모양, 돌려주는 값의 모양입니다.
받는 값과 돌려주는 값은 따로 정의한 메시지 타입입니다. 메시지는 필드 몇 개를 묶은 데이터
꾸러미입니다. 주문 번호 하나를 담은 GetOrderRequest 를 보내면 주문 내용을 담은 Order 가 옵니다.
호출이 서버까지 가는 길
클라이언트가 원격 메서드를 내 메서드처럼 부를 수 있는 것은 스텁 덕분입니다. 스텁은 원격 메서드와 똑같은 이름과 인자를 가진 클라이언트 쪽 객체입니다. 서비스 정의에서 도구가 만들어 줍니다. 겉으로는 메서드를 가진 평범한 객체입니다. 안에서는 호출을 서버로 실어 보냅니다.
호출을 네트워크로 보내려면 인자 객체를 바이트로 바꿔야 합니다. 이 변환을 직렬화라고 부릅니다. 받은 쪽은 바이트를 다시 객체로 되돌립니다. 이 반대 방향을 역직렬화라고 부릅니다.
서버 쪽에도 도구가 만든 코드가 있습니다. 이 코드는 도착한 메서드 이름을 보고 그 이름에 맞는 구현을 찾아 부릅니다. 어떤 원격 호출 방식은 서버 쪽의 이 코드를 스켈레톤이라고 부릅니다. 한 번의 호출은 이렇게 흘러갑니다.
sequenceDiagram
participant C as 클라이언트 코드
participant S as 스텁
participant V as 서버 쪽 코드
participant I as 구현
C->>S: GetOrder 를 부른다
Note over S: 인자를 직렬화한다
S->>V: 메서드 이름과 바이트를 보낸다
V->>I: 이름에 맞는 구현을 부른다
I-->>V: Order 를 돌려준다
V-->>S: 결과를 직렬화해 보낸다
S-->>C: 역직렬화한 Order 를 돌려준다
클라이언트 코드가 보는 것은 맨 위와 맨 아래 화살표뿐입니다. 가운데에서 일어나는 직렬화와 네트워크 전송은 스텁과 서버 쪽 코드가 맡습니다.
로컬 메서드와 다른 점
내 프로세스 안에서 부르는 메서드를 로컬 메서드라고 합니다. 프로세스는 실행 중인 프로그램 하나를 말합니다. 원격 메서드도 로컬 메서드처럼 부릅니다. 하지만 네트워크를 건너기 때문에 성질이 여럿 다릅니다.
| 축 | 로컬 메서드 | 원격 호출 가능한 메서드 |
|---|---|---|
| 걸리는 시간 | 같은 프로세스 안이라 짧다 | 네트워크 왕복만큼 길다 |
| 실패 | 메서드가 던진 예외뿐 | 예외에 더해 연결 끊김 · 응답 없음 |
| 인자 | 같은 객체를 가리킨다 | 사본이 건너간다 |
| 바뀌는 때 | 부르는 쪽과 한 프로그램으로 함께 배포된다 | 서버와 클라이언트가 따로 배포된다 |
이 가운데 실패가 제일 까다롭습니다. 응답이 안 오면 클라이언트는 둘 중 무엇인지 모릅니다. 요청이 서버에 닿지 못했을 수도 있습니다. 아니면 서버가 일을 마친 뒤 응답만 사라졌을 수도 있습니다. 이렇게 일부만 실패해 결과를 알 수 없는 상태를 부분 실패라고 부릅니다.
인자가 사본으로 건너간다는 점도 로컬 메서드와 다릅니다. 서버가 받은 인자를 고쳐도 클라이언트의 객체는
안 바뀝니다. 아래 코드에서 stub 은 앞에서 본 스텁입니다.
order.setMemo("문 앞"); // 내 객체
stub.save(order); // 사본이 서버로
// 서버 구현이 memo 를 바꿈
order.getMemo(); // "문 앞"
로컬 메서드였다면 마지막 줄은 메서드가 바꾼 값을 돌려줬을 겁니다. 원격 메서드에서 바뀐 값을 받으려면 그 값을 돌려주는 값으로 받아야 합니다.
실패를 다루는 장치
응답을 끝없이 기다릴 수는 없습니다. 부르는 쪽은 얼마나 기다릴지 미리 정해 둡니다. 이 한도를 타임아웃이라고 부릅니다. 한도를 넘기면 호출을 실패로 치고 다음 일로 넘어갑니다.
실패한 호출을 한 번 더 보내는 것을 재시도라고 합니다. 재시도에는 함정이 있습니다. 앞의 요청이 서버에서 이미 처리됐다면, 「주문 만들기」를 다시 보낸 순간 주문이 두 개가 됩니다.
그래서 원격 메서드는 멱등성을 따져 설계합니다. 멱등성은 같은 요청을 여러 번 해도 한 번 한 것과 결과가 같은 성질입니다. 「주문 찾기」는 원래 멱등입니다. 「주문 만들기」는 요청마다 고유 번호를 실어 보냅니다. 서버는 같은 번호를 한 번만 처리합니다. 이렇게 해서 멱등으로 만듭니다.
Java RMI(Java Remote Method Invocation, 자바 원격 메서드 호출)는 네트워크 실패를 메서드 선언에
드러냅니다. 원격 메서드는 모두 RemoteException 을 던질 수 있다고 선언해야 합니다.
자바에서는 이렇게 선언된 예외를 부르는 쪽이 잡거나 다시 던져야 합니다. 그러지 않으면 컴파일이 되지 않습니다. 원격 메서드를 부르는 코드는 네트워크 실패를 다루는 처리를 빠뜨릴 수 없게 됩니다.
메서드 하나에 담는 일의 크기
로컬 메서드는 잘게 쪼개도 부르는 비용이 거의 없습니다. 원격 메서드는 부를 때마다 네트워크 왕복을 기다립니다. 메서드를 잘게 쪼갤수록 기다림이 쌓입니다.
주문 화면 하나에 주문 · 배송 · 결제 정보가 필요하다고 해 봅시다. 셋을 따로 여는 서버라면 클라이언트는 왕복을 세 번 합니다. 이렇게 잦은 왕복을 부르는 설계를 수다스러운 인터페이스(chatty interface)라고 부릅니다.
그래서 원격 메서드는 한 번에 넉넉한 일을 하도록 짓는 경우가 많습니다. 「주문 상세 가져오기」 하나가 셋을 함께 돌려주는 식입니다. 대가는 메서드가 특정 화면에 맞춰진다는 것입니다. 다른 화면을 가진 클라이언트는 필요 없는 정보까지 받게 됩니다.
요청과 응답의 개수
기본 모양은 요청 하나를 받고 응답 하나를 돌려주는 메서드입니다. 이 모양을 단항 RPC(unary RPC)라고
부릅니다. 앞에서 본 GetOrder 가 단항입니다.
메시지를 여러 개 이어 보내는 메서드도 있습니다. 이렇게 메시지를 하나씩 흘려보내는 방식을 스트리밍이라고 부릅니다. 긴 목록을 한 번에 담지 않고 조금씩 나눠 받을 수 있습니다.
gRPC는 프로토콜 버퍼로 서비스 정의를 적는 원격 호출 도구입니다. 앞에서 본 OrderService 같은
정의에서 스텁과 서버 쪽 코드를 만들어 줍니다. gRPC 는 메서드를 요청과 응답의 개수에 따라 네 모양으로
가릅니다.
| 모양 | 요청 | 응답 | 쓰임 |
|---|---|---|---|
| 단항 | 하나 | 하나 | 주문 하나 찾기 |
| 서버 스트리밍 | 하나 | 여러 개 | 긴 주문 목록을 조금씩 받기 |
| 클라이언트 스트리밍 | 여러 개 | 하나 | 센서 값을 잇달아 올리고 합계 받기 |
| 양방향 스트리밍 | 여러 개 | 여러 개 | 채팅처럼 양쪽이 번갈아 보내기 |
어느 모양인지는 서비스 정의의 메서드 줄에 적습니다. 여러 개가 오가는 쪽의 타입 앞에 stream 을
붙입니다. 아래는 주문 목록을 조금씩 돌려주는 서버 스트리밍 메서드입니다.
rpc ListOrders(ListOrdersRequest)
returns (stream Order); // 여러 개
returns 쪽에만 stream 이 붙었으니 요청은 하나, 응답은 여러 개입니다. 모양이 바뀌면 클라이언트
코드의 부르는 방식도 달라집니다. 모양 역시 약속의 일부입니다.
메서드를 고칠 때 깨지는 호환성
로컬 메서드는 부르는 쪽과 한 프로그램으로 함께 빌드되고 함께 배포됩니다. 배포는 고친 프로그램을 실제로 돌아가는 곳에 올리는 일입니다. 원격 메서드는 다릅니다. 서버와 클라이언트는 따로 배포됩니다.
서버가 메서드를 고친 뒤에도 옛 클라이언트는 한동안 옛 약속대로 부릅니다. 옛 클라이언트가 계속 동작하게 지키는 성질을 하위 호환성이라고 부릅니다.
메서드를 새로 더하는 것은 옛 클라이언트에 영향이 없습니다. 옛 클라이언트는 새 메서드를 모르니 부르지도 않습니다. 메서드 이름을 바꾸거나 지우면 다릅니다. 옛 클라이언트의 호출은 「그런 메서드가 없다」는 오류로 실패합니다.
메서드를 바꿀 때는 새 이름으로 하나를 더 엽니다. 클라이언트가 모두 새 메서드로 옮겨 간 뒤에 옛 메서드를 닫습니다.
쓰는 때와 다른 방식을 고르는 때
원격 호출 가능한 메서드는 서버끼리 정해진 동작을 주고받을 때 잘 맞습니다. 한 회사 안의 서비스들처럼 양쪽이 같은 서비스 정의를 받아 코드를 만들 수 있는 환경입니다. 이름 붙은 동작이 몇 개로 정해져 있습니다. 부르는 쪽은 결과를 바로 기다립니다.
REST(Representational State Transfer)는 다른 길을 갑니다. 동작마다 메서드를 여는 대신, 주문
하나하나를 자원으로 보고 웹 주소를 붙입니다. 42번 주문이라면 /orders/42 같은 주소입니다.
그 주소에 HTTP(HyperText Transfer Protocol)의 정해진 요청 메서드를 보내 자원을 읽고 바꿉니다.
조회는 GET, 생성은 POST, 삭제는 DELETE 입니다. 브라우저나 서비스 정의를 받지 않은 바깥
클라이언트도 HTTP 만 알면 부를 수 있습니다.
HTTP 의 요청 메서드도 이름에 「메서드」가 들어갑니다. 그쪽은 조회 · 생성처럼 요청의 종류를 가리킵니다. 서버가 이름을 붙여 연 동작을 가리키는 이 표제어와는 이름만 겹칩니다.
부르는 쪽이 결과를 기다릴 필요가 없으면 메시지 큐를 쓰기도 합니다. 메시지를 큐에 넣고 바로 다음 일로 넘어갑니다. 받는 쪽이 잠시 꺼져 있어도 메시지는 큐에서 기다립니다. 대가는 응답이 바로 오지 않는다는 것입니다.
관련 항목
원격 호출 가능한 메서드가 속하는 상위 분류
RPC · API · API 설계 · 분산 시스템 · 서비스 지향 아키텍처 · 마이크로서비스
이것을 선언하는 정의 언어와 파일
서비스 정의 · 인터페이스 정의 언어 · proto 파일 · 프로토콜 버퍼 · Apache Thrift · WSDL · OpenAPI
원격 호출 가능한 메서드를 이루는 구성 요소
rpc 메서드 · 메시지 · 필드 · 단항 RPC · 스트리밍 · 양방향 스트리밍
호출을 실어 나르는 코드와 변환 단계
스텁 · 스켈레톤 · 코드 생성 · 직렬화 · 역직렬화 · 마샬링
원격 호출에서 나는 실패와 대처 장치
부분 실패 · 타임아웃 · 데드라인 · 재시도 · 멱등성 · 서킷 브레이커 · 왕복 시간
원격 메서드를 설계할 때 따지는 성질
하위 호환성 · API 버전 관리 · API 계약 · 수다스러운 인터페이스
이것을 구현한 원격 호출 방식
gRPC · Java RMI · CORBA · JSON-RPC · XML-RPC · ONC RPC · DCOM
원격 메서드 대신 쓸 수 있는 통신 수단
REST · GraphQL · 메시지 큐 · 이벤트 기반 아키텍처 · 웹소켓
이름이 겹치는 헷갈리는 이웃
다른 이름: remote method · remotely callable method · 원격 메서드