gRPC
고친 사람 github-actions[bot]
gRPC 는 다른 서버에 있는 함수를 내 코드의 함수처럼 부르게 해 주는 프로토콜입니다. 주고받을 함수와 데이터 모양을 파일 하나에 먼저 적어 둡니다. 그 파일에서 양쪽 코드가 만들어지므로 서버와 클라이언트가 같은 정의를 들고 시작합니다. 서비스끼리 서로 부르는 백엔드 내부 통신에서 흔히 씁니다.
쉽고 빠른 이해
gRPC 는 다른 서버의 함수를 내 함수처럼 부르게 해 줍니다. 주문 서비스가 결제 서비스의 결제 함수를 부르면, 코드에서는 메서드 한 번 부르는 것으로 보입니다.
이게 없으면 부를 때마다 이런 코드를 손으로 씁니다. 주소를 짜고 데이터를 글자로 바꾸는 코드, 받은 글자를 다시 객체로 푸는 코드입니다. 양쪽이 필드 이름 하나만 달리 알아도 실행해 보기 전까지 모릅니다.
어떻게 도나:
- 함수 이름과 주고받을 데이터 모양을 정의 파일 하나에 적습니다
- 도구가 그 파일에서 서버 쪽 뼈대와 클라이언트 쪽 호출 코드를 만들어 줍니다
- 호출하면 데이터가 작은 바이트열로 바뀌어 연결 하나 위를 오갑니다. 끝에는 성공인지 실패인지가 함께 돌아옵니다
대가도 있습니다. 오가는 데이터를 사람이 눈으로 못 읽습니다. 브라우저에서는 바로 부를 수 없어 중간 변환기를 둡니다.
그래서 서비스끼리 부르는 내부 통신에 주로 쓰고, 브라우저가 부르는 외부 API 는 REST 로 둡니다.
상세
gRPC 는 RPC(Remote Procedure Call, 원격 프로시저 호출)를 하는 프로토콜입니다. RPC 는 네트워크 너머에 있는 함수를 부르는 방식입니다. 부르는 코드에서는 보통 함수 호출처럼 보이게 합니다. 호출이 네트워크를 건넌다는 사실을 코드 한 줄 뒤로 숨기는 것이 목적입니다.
전화 주문에 비유할 수 있습니다. 가게마다 메뉴판이 미리 정해져 있습니다. 손님은 메뉴 이름과 수량만 말합니다. 주문서를 어떻게 적고 주방에 어떻게 넘기는지는 손님이 신경 쓰지 않습니다.
이 절은 gRPC 한 번의 호출을 따라갑니다. 순서는 .proto 파일 작성 → 코드 생성 → 바이트 전송 → 결과 반환입니다.
.proto 파일
gRPC 에서는 부를 함수와 주고받을 데이터를 먼저 파일에 적습니다. 이런 파일을 쓰는 언어를 IDL(Interface Definition Language, 인터페이스 정의 언어)이라고 부릅니다. 특정 프로그래밍 언어에 매이지 않는 중립적인 정의라, 자바 서버와 파이썬 클라이언트가 같은 파일을 나눠 씁니다.
gRPC 가 기본으로 쓰는 IDL 은 Protocol Buffers 입니다. 줄여서 Protobuf 라고 부릅니다. 파일 확장자는 .proto 입니다. Protobuf 는 인터페이스를 적는 언어입니다. 데이터를 작은 바이트열로 바꾸는 직렬화 방식이기도 합니다.
아래는 인사를 주고받는 서비스 하나를 적은 .proto 파일입니다. service 는 부를 수 있는 함수 묶음입니다. message 는 주고받을 데이터의 모양입니다. 맨 위의 package 는 이름이 겹치지 않게 붙이는 묶음 이름입니다.
package greet;
service Greeter {
rpc SayHello (HelloRequest)
returns (HelloReply);
}
message HelloRequest {
string name = 1; // 필드 번호 1
}
message HelloReply {
string message = 1;
}
필드 옆의 = 1 은 값이 아니라 필드 번호입니다. 직렬화된 바이트에는 필드 이름 대신 이 번호가 들어갑니다. 이름을 안 실으니 메시지가 작아집니다. 이름을 바꿔도 번호가 같으면 옛 코드와 계속 통합니다.
코드 생성과 스텁
.proto 파일을 코드 생성기에 넣으면 언어별 코드가 나옵니다. 클라이언트 쪽에는 스텁이 생깁니다. 스텁은 원격 함수와 똑같은 이름의 메서드를 가진 대리 객체입니다.
메서드를 부르면 스텁이 요청을 바이트로 바꿔 서버로 보냅니다. 답이 오면 객체로 풀어서 돌려줍니다.
서버 쪽에는 구현해야 할 메서드 목록을 담은 뼈대가 생깁니다. 개발자는 그 뼈대를 상속하거나 구현해 본문만 채웁니다. 요청을 풀고 답을 싸는 일은 생성된 코드가 맡습니다.
flowchart TD
P["greet.proto"] --> G["코드 생성기"]
G --> C["클라이언트 스텁"]
G --> S["서버 뼈대"]
C -->|요청을 바이트로 바꿔 보냄| N["네트워크"]
N -->|바이트를 객체로 풀어 부름| S
이 구조에서는 인터페이스 정의가 한 곳에만 있습니다. 한쪽이 필드를 잘못 알고 있으면 대개 컴파일할 때 드러납니다. 실행 중에 JSON(JavaScript Object Notation) 키 이름이 틀려 빈 값이 들어오는 식의 사고가 줄어듭니다.
HTTP/2 위에서 오가는 한 번의 호출
gRPC 는 바이트를 나를 때 HTTP/2(HyperText Transfer Protocol version 2)를 씁니다. HTTP/2 는 연결 하나를 여러 요청이 함께 나눠 쓰는 HTTP 판입니다. 요청마다 연결을 새로 맺지 않아도 되니 연결을 맺는 비용이 줄어듭니다.
연결 안에서 요청 하나와 그 응답이 오가는 따로 떨어진 통로를 스트림이라고 부릅니다. 연결 하나 안에 스트림을 여러 개 열 수 있습니다. gRPC 호출 하나가 스트림 하나를 씁니다. 그래서 호출 수백 개가 한 연결 위에서 동시에 오갑니다.
HTTP/2 에서 데이터는 프레임이라는 조각으로 오갑니다. 헤더를 싣는 HEADERS 프레임과 본문을 싣는 DATA 프레임이 대표적입니다. gRPC 호출 하나는 이 두 종류 프레임의 정해진 순서로 이루어집니다.
sequenceDiagram
participant 클라이언트
participant 서버
클라이언트->>서버: HEADERS · POST /greet.Greeter/SayHello
클라이언트->>서버: DATA · 요청 메시지 하나
서버->>클라이언트: HEADERS · status 200
서버->>클라이언트: DATA · 응답 메시지 하나
서버->>클라이언트: HEADERS · grpc-status 0
클라이언트는 먼저 헤더를 보냅니다. 메서드는 언제나 POST 입니다. 경로는 /[[패키지]] 이름.서비스 이름/메서드 이름 꼴입니다. 그림의 greet. 이 앞의 .proto 파일에 적은 패키지 이름입니다. content-type 은 application/grpc 로 시작합니다. 서버는 이 경로만 보고 어느 함수를 부를지 고릅니다.
그다음 요청 메시지가 DATA 프레임에 실려 갑니다. 서버는 응답 헤더와 응답 메시지를 보냅니다. 마지막에는 헤더를 한 번 더 보냅니다. 본문 뒤에 붙는 이 마지막 헤더를 트레일러라고 부릅니다.
응답 헤더의 200 은 HTTP 전송이 됐다는 뜻일 뿐입니다. 호출이 성공했는지는 트레일러의 grpc-status 가 알려 줍니다. 0 이 성공입니다. 자세한 것은 아래 상태 코드 절에서 봅니다.
메시지 하나의 모양
HTTP/2 의 DATA 프레임은 크기에 따라 쪼개지기도 하고 붙기도 합니다. 그래서 받는 쪽은 어디서 메시지 하나가 끝나는지 따로 알아야 합니다. gRPC 는 메시지마다 앞에 길이를 붙여 이 문제를 풉니다.
메시지 앞에 5바이트 접두부가 붙습니다. 첫 1바이트는 이 메시지를 압축했는지 표시합니다. 다음 4바이트는 뒤따르는 메시지의 길이입니다. 길이는 큰 자리 바이트를 먼저 적는 빅 엔디언 순서로 씁니다.
앞의 HelloRequest 에 이름 hello 를 담아 보내면 스트림에 이렇게 실립니다.
00 // 압축 안 함
00 00 00 07 // 뒤에 7바이트
0a // 필드 1 · 길이 붙는 값
05 // 값 길이 5
68 65 6c 6c 6f // "hello"
접두부 5바이트 뒤에 Protobuf 로 직렬화한 7바이트가 이어집니다. 필드 이름 name 은 어디에도 없고 필드 번호 1 만 들어 있습니다. 같은 내용을 JSON 으로 적은 {"name":"hello"} 는 16바이트입니다.
0a 가 1 이 아닌 까닭은 이 한 바이트에 두 정보가 함께 들어 있기 때문입니다. 필드 번호에 8 을 곱하고 값의 종류를 더합니다. 문자열처럼 길이가 뒤따르는 값은 종류가 2 라서 1×8+2 = 10, 16진수로 0a 입니다.
네 가지 호출 방식
한 스트림 안에서 메시지를 몇 개 보내느냐에 따라 호출 방식이 넷으로 갈립니다. 앞의 5바이트 접두부 덕분에 한 스트림에 메시지를 여러 개 이어 보낼 수 있습니다. 이것이 스트리밍입니다.
| 방식 | 클라이언트가 보내는 메시지 | 서버가 보내는 메시지 | 어울리는 일 |
|---|---|---|---|
| 단항 | 하나 | 하나 | 보통의 조회·저장 |
| 서버 스트리밍 | 하나 | 여럿 | 긴 목록을 조각조각 받기 · 변경 알림 구독 |
| 클라이언트 스트리밍 | 여럿 | 하나 | 파일 조각 올리기 · 측정값 모아 보내기 |
| 양방향 스트리밍 | 여럿 | 여럿 | 채팅 · 서로 계속 주고받는 제어 신호 |
단항은 영어로 unary 라고 부릅니다. 일반 함수 호출과 모양이 같습니다. 나머지 셋에서는 메서드가 값 하나 대신 메시지를 계속 읽고 쓰는 통로를 돌려줍니다. .proto 파일에서는 요청이나 응답 타입 앞에 stream 을 붙여 적습니다.
결과는 상태 코드로 돌아온다
gRPC 호출이 실패해도 HTTP 응답 코드는 대개 200 입니다. HTTP 입장에서는 스트림이 멀쩡히 오갔기 때문입니다. 호출의 진짜 결과는 트레일러의 grpc-status 에 숫자로 담깁니다.
결과를 맨 끝에 싣는 이유는 스트리밍에 있습니다. 서버가 메시지를 열 개 보내다가 열한 번째에서 실패할 수 있습니다. 결과는 마지막에야 정해지므로 본문 뒤의 트레일러가 싣습니다.
자주 보는 상태 코드는 이렇습니다.
| 이름 | 숫자 | 뜻 |
|---|---|---|
| OK | 0 | 성공 |
| DEADLINE_EXCEEDED | 4 | 정해 둔 시간 안에 답이 안 왔다 |
| NOT_FOUND | 5 | 찾는 대상이 없다 |
| UNIMPLEMENTED | 12 | 서버에 그 메서드가 없다 |
| UNAVAILABLE | 14 | 서버에 지금 닿을 수 없다. 대개 다시 시도해 볼 만하다 |
기한
gRPC 클라이언트는 호출마다 데드라인을 붙일 수 있습니다. 데드라인은 「이 시각까지 답이 없으면 포기한다」는 기한입니다. 이 값은 grpc-timeout 헤더에 남은 시간으로 실려 서버에도 전달됩니다.
서버도 기한을 알기 때문에, 이미 기다리는 사람이 없는 요청을 계속 처리하지 않고 멈출 수 있습니다. 서버가 다른 서비스를 다시 부를 때 남은 시간을 넘겨주면 호출 사슬 전체가 같은 기한을 따릅니다. 기한을 넘기면 호출은 DEADLINE_EXCEEDED 로 끝납니다.
쓰면 생기는 대가
오가는 데이터가 바이트열이라 사람이 바로 읽지 못합니다. 브라우저 개발자 도구나 curl 로 요청을 찍어 보던 습관이 통하지 않습니다. .proto 파일을 읽을 줄 아는 전용 도구가 있어야 요청을 보내고 답을 읽을 수 있습니다.
브라우저는 gRPC 를 직접 부르지 못합니다. 브라우저의 요청 API(Application Programming Interface, 응용 프로그래밍 인터페이스)로는 응답의 트레일러를 읽을 수 없기 때문입니다. 그래서 웹 화면에서 쓰려면 gRPC-Web 같은 중간 변환기를 둡니다.
연결 하나를 오래 붙들고 호출을 여러 개 싣는 성질은 부하 분산을 까다롭게 합니다. 연결 단위로 서버를 고르는 로드 밸런서는 연결이 처음 맺힐 때 한 번만 서버를 고릅니다. 그 뒤 그 연결에 실리는 호출은 전부 같은 서버로 갑니다.
대책은 두 가지입니다. 하나는 연결이 아니라 호출마다 서버를 고르는 로드 밸런서를 두는 것입니다. 다른 하나는 클라이언트가 서버 목록을 직접 들고 호출마다 서버를 골라 나눠 보내는 것입니다.
이런 이유로 gRPC 는 서비스끼리 부르는 내부 통신에 자주 쓰입니다. 외부에 여는 API 는 사람이 읽기 쉽고 브라우저가 바로 부르는 REST(Representational State Transfer) 방식으로 두는 경우가 많습니다.
관련 항목
gRPC 가 한 갈래로 속하는 통신 방식
RPC · API 설계 · 프로토콜 · 마이크로서비스 · 서비스 간 통신
gRPC 와 같은 역할을 두고 겨루는 API 방식
REST · GraphQL · SOAP · Thrift · JSON-RPC · WebSocket
gRPC 호출을 이루는 구성 요소
Protocol Buffers · IDL · 스텁 · 코드 생성 · 메시지 · 필드 번호 · 상태 코드 · 메타데이터 · 데드라인
gRPC 가 올라타는 전송 프로토콜
HTTP/2 · 스트림 · 프레임 · 트레일러 · 멀티플렉싱 · TCP · TLS · HTTP
gRPC 메시지를 바이트로 바꾸는 직렬화 방식
직렬화 · 역직렬화 · 이진 인코딩 · JSON · 빅 엔디언 · 스키마 · 하위 호환
gRPC 의 네 가지 호출 방식
단항 RPC · 서버 스트리밍 · 클라이언트 스트리밍 · 양방향 스트리밍 · 스트리밍
gRPC 호출이 겪는 오류와 그 대비책
DEADLINE_EXCEEDED · UNAVAILABLE · 타임아웃 · 재시도 · 취소 전파 · 서킷 브레이커
gRPC 트래픽을 나누고 중계하는 장치
로드 밸런서 · 부하 분산 · L7 로드 밸런싱 · 클라이언트 측 로드 밸런싱 · 서비스 메시 · Envoy · gRPC-Web · gRPC-Gateway
gRPC 서버를 두드리고 재는 도구
다른 이름: gRPC Remote Procedure Calls · 지알피씨