Search

gRPC에 대한 모든 것

주제
Backend
날짜
2024/04/14

RPC란?

Remote Procedure Call(원격 프로시저 호출) 통신이나 call 방식에 신경쓰지 않고 원격지의 자원을 내 것처럼 사용할 수 있다.
별도의 원격 제어를 위한 코딩 없이 다른 주소 공간의 함수나 프로시저를 실행할 수 있게 하는 프로세스 간 통신 기술이다.
RPC에서 클라이언트-서버 통신은 클라이언트의 API 요청이 로컬 작업이거나 요청이 내부 서버 코드인 것처럼 작동한다.
즉, Client와 Server는 각각 일반 메소드를 호출하는 것처럼 원격지의 프로시저를 호출할 수 있다.
RPC에서 클라이언트는 서버의 프로세스로 요청을 전송한다. 이 프로세스는 항상 원격 호출을 수신 대기 중인 상태로 유지된다. 요청에는 직접적으로 호출할 서버 함수와 전달할 파라미터가 포함된다.
일반적으로 프로세스는 자신의 주소공간 안에 존재하는 함수만 호출하여 실행 가능하다.
그러나, RPC의 경우 자신과 다른 주소공간에서 동작하는 프로세스의 함수를 실행할 수 있게 해주는데, 이는 네트워크를 통한 메시징을 수행하기 때문이다.
RPC API는 HTTP, TCP 또는 UDP와 같은 프로토콜을 기본 데이터 교환 메커니즘으로 사용한다.
IDL(Interface Definication Language) 기반으로 다양한 언어를 가진 환경에서도 쉽게 확장이 가능하며, 인터페이스 협업에도 용이하다는 장점이 있다.
즉, 언어나 환경에 구애를 받지 않는다.
함수 vs 프로시저
함수 (Function)
Input에 따른 Ouput의 발생을 목적으로 한다.
따라서 Return 값을 필수로 가져야하며, 클라이언트측에서 처리되기 때문에 주로 간단한 계산 및 수치 등을 도출할 때 사용한다.
프로시저 (Procedure)
Output 값 자체에 집중하기보단, 명령 단위가 수행하는 절차에 집중한 개념이다.
따라서 Return 값이 필수가 아니며, 서버단에서 처리되기 때문에 함수보다 큰 단위의 실행, 프로세싱 등을 할 때 사용한다.
RDBMS인 MS-SQL에도 Procedure라는 개념이 있어 금융권에서 많이 사용하는 것으로 알고 있다.

등장배경

Server-Client Model

네트워크 통신이 중요해지며 발전하게된 이유 → 소형 컴퓨터의 발전으로 인한 컴퓨터 분산
PC(Personal Computer)의 개념이 없던 시절, 프로그램은 하나의 메인 프레임에서 동작하는 Monolothic 구조로 설계되었다.
그렇기에 모든 기능이 한 공간에서 구동되나 보니 네트워크 통신이 중요하지 않았다.
기술 발전에 따라 소형 컴퓨터(PC, 워크스테이션 서버 등)이 등장하게 되고, 기업 입장에선 매우 고가인 메인 프레임워크를 비교적 저가의 워크스테이션 서버로 대체하고 싶어했다.
이 때문에 메인 프레임워크의 기능을 워크스테이션 서버로 분산시키고, 네트워크 연결로 서비스하는 방식을 채택하게 된다. 흔히 말하는 Server-Client Model이다.
이처럼 서버 간 혹은 서버와 개인 PC 간 네트워크 연결/통신이 중요해지면서 OSI 7 layer, TCP/IP 등 네트워크 계층 구조가 정의되고 발전하기 시작한다.

IPC

프로세스간 통신을 위한 방법론 → Socket, HTTP, RPC 모두 IPC의 일종
프로세스는 기본적으로 상호독립적이다. 메모리를 공유하지 않기 때문에 각자 할 일만 하며 서로 간섭하지 않는다.
하지만 필요에 따라 프로세스간 정보를 교환해야하는 경우에 별도 수단을 이용하여 프로세스 통신하는 방법론을 통칭하여 IPC(Inter Process Communication) 라고 한다.
Socket
OSI 7 layer 구조의 Application Layer(L7)에서 Transport Port(L4)의 TCP 또는 UDP를 이용하기 위한 수단이다.
다만 서비스가 고도화될 수록 수백 수천가지 데이터가 돌아다니게 되며, 이에 따라 data formatting 을 하는 것이 점점 어려워진다.
RPC
이런 소켓의 한계에서 RPC(Remote Procedure Call)라는 기술이 등장한다.
이름 그대로 네트워크로 연결된 서버 상의 프로시저(함수, 메서드 등)를 원격으로 호출할 수 있는 기능이다.
REST
REST는 HTTP/1.1 기반으로 URI를 통해 모든 자원(Resource)을 명시하고 HTTP Method를 통해 처리하는 아키텍쳐이다.
하지만 REST에도 한계는 존재한다. REST는 일종의 스타일이지 표준이 아니기 때문에 파라미터와 응답 값이 명시적이지 않다. 또한 HTTP 메소드의 형태가 제한적이기 때문에 세부 기능 구현에는 제약이 있다.
왜 RPC는 자주 쓰이지 않는가?
RPC 출시 방시 상당히 획기적인 방법론이였으며, 분산 환경의 등장에 따라 함께 발전해 온 오래된 기술이다. 따라서 구현체도 CORBA, RMI 등 여러가지가 있었다.
이들 모두 로컬에서 제공하는 빠른 속도, 가용성 등을 분산 프로그래밍에서도 제공하고 있다고 홍보를 했지만, 정작 구현의 어려움/지원 기능의 한계 등으로 제대로 활용되지 못했다.
그렇게 RPC 프로젝트도 점차 뒷길로 가게되며 데이터 통신을 우리에게 익숙한 Web을 활용해보려는 시도로 이어졌고, 이 자리를 REST가 차지하게된다.

RPC 동작 방식

RPC의 핵심 개념은 Stub
서버와 클라이언트는 서로 다른 주소 공간을 사용하므로, 함수 호출에 사용된 매개 변수를 꼭 변환해줘야 한다. 안그러면 메모리 매개 변수에 대한 포인터가 다른 데이터를 가리키게 된다. 이 변환을 담당하는게 Stub이다.
Client Stub
함수 호출에 사용된 파라미터의 변환(Marshalling)함수 실행 후 서버에서 전달된 결과의 변환을 담당한다.
Server Stub
클라이언트가 전달한 파라미터의 역변환(Unmarshalling)함수 실행 결과 변환을 담당한다.

상세 동작 방식

IDL 기반의 RPC 과정
1. IDL (Interface Definition Language)
IDL을 사용하여 호출 규약을 정의한다.
함수명, 인자, 반환값에 대한 데이터 타입이 정의된 IDL 파일을 rpcgen으로 컴파일하면 Stub Code가 자동으로 생성된다.
2. Stub Code 빌드
Stub Code에 명시된 함수는 원시소스코드(C, Java 등)의 형태로 만들어지며, 상세 기능은 서버에서 구현된다.
생성된 Stub Code는 클라이언트/서버에 각각 포함하여 함께 빌드한다.
3. Client Stub에 정의된 함수를 호출
클라이언트 입장에서는 자신의 프로세스 주소 공간의 함수를 호출하는 것처럼 보이는데, Client Stub에 정의된 함수를 호출하는 것이다.
4. XDR 형식으로 변환 후 RPC 호출
Stub Code는 데이터 타입을 XDR(External Data Representation) 형식으로 변환하여 RPC 호출을 실행한다.
XDR로 변환하는 이유는 기본 데이터 타입(정수형, 부동소수점 등)에 대한 메모리 저장방식(Little-Endian / Big-Endian)이 CPU 아키텍처별로 다르며, 네트워크 전송과정에서 바이트 전송 순서를 보장하기 위함이다.
5. Server는 수신된 Procedure 호출에 대한 처리 후 결과 값 반환
서버는 수신된 함수/프로시저 호출에 대한 처리를 Server Stub을 통해 처리 완료 후, 결과값을 XDR 변환하여 Return.
6. Server의 결과 값을 Client가 수신
최종적으로 클라이언트는 서버가 Return한 결과값을 전송 받고, 함수를 Local에 있는 것 처럼 사용할 수 있다.
IDL(Interface Definition Language) 인터페이스 정의 언어. 어느 한 언어에 국한되지 않는 언어 중립적인 방법으로 인터페이스를 표현함으로써, 구현 언어(C, C++, Java 등)가 아닌 정의 언어로, 구현 언어로의 매핑(mapping)을 지원.

gRPC란?

gRPC는 모든 환경에서 실행할 수 있는 최신 오픈 소스 고성능 원격 프로시저 호출(RPC) 프레임워크입니다. 로드 밸런싱, 추적, 상태 확인 및 인증을 위한 플러그 지원으로 데이터 센터 안팎의 서비스를 효율적으로 연결할 수 있습니다. 장치, 모바일 애플리케이션 및 브라우저를 백엔드 서비스에 연결하는 분산 컴퓨팅의 마지막 단계에서도 적용할 수 있습니다. 출처: https://grpc.io/about/
Google Remote Procedure Call의 약자가 아니다 gRPC Remote Procedure Call의 약자이다
gRPC는 구글에서 개발한 오픈소스 RPC(Remote Procedure Call) 프레임워크이다.
이전까지는 RPC 기능은 지원하지 않고, 메세지(JSON 등)을 Serialize할 수 있는 프레임워크인 프로토콜 버퍼(Protocol Buffer)만을 제공해왔는데, 프로토콜 버퍼 기반 Serizlaizer에 HTTP/2를 결합하여 RPC 프레임워크를 탄생시켰다.
gRPC는 HTTP/2에 대한 전이중(full duplex) 연결을 제공한다.
REST와 비교했을 때 기반 기술이 다르기에 특징도 많이 다르지만, 가장 큰 차이점은 HTTP/2를 사용한다는 것과 프로토콜 버퍼로 데이터를 전달한다는 점이다.
그렇기에 .proto File만 배포하면 환경과 프로그램 언어에 구애받지 않고 서로 간의 데이터 통신이 가능하다.
현재 브라우저에서 사용할 때는 어댑터가 필요하며, 백엔드 서비스로 가장 인기가 높다.
gRPC는 특히 강력한 API 계약과 메서드 추상화가 복잡성을 낮추는 데 도움이 되는 마이크로서비스(MSA)에서 많이 사용된다.
gRPC는 일반적인 REST 스택에 비해 사전 작업이 더 많이 필요하지만 더 구조적인 접근 방식이 유리한 조직과 규모가 큰 프로젝트에 매력적일 수 있다.

gRPC와 RPC의 차이점

gRPC는 몇 가지 최적화와 함께 기존 RPC를 구현하는 시스템이다.
예를 들어 gRPC는 데이터 전송에 Protocol Buffer와 HTTP/2를 사용한다.
gRPC는 개발자의 데이터 교환 메커니즘을 추상화한다.
예를 들어 널리 사용되는 또 다른 RPC API 구현인 OpenAPI를 사용할 때는 개발자가 RPC 개념을 HTTP 프로토콜에 매핑해야 한다. 하지만 gRPC는 기본 HTTP 통신을 추상화한다. 이러한 최적화 덕에 gRPC는 다른 RPC 구현보다 더 빠르고 쉽게 구현할 수 있으며 웹 친화적이다.

HTTP/2

HTTP/1.1
기본적으로 클라이언트의 요청이 올때만 서버가 응답을 하는 구조로 매 요청마다 connection을 생성해야만 한다. 또한 Cookie 등 많은 메타 정보들을 저장하는 무거운 header가 요청마다 중복 전달되어 비효율적이고 느린 속도를 보여준다.
HTTP/2
하나의 connection으로 동시에 여러 개 메시지를 주고 받으며, header를 압축하여 중복 제거 후 전달하기에 HTTP/1에 비해 훨씬 효율적이다.
또한, 필요 시 클라이언트 요청 없이도 서버가 리소스를 전달할 수도 있기 때문에 클라이언트 요청을 최소화 할 수 있다.

ProtoBuf

ProtoBuf(Protocol Buffer)는 구글에서 개발한 구조화된 데이터를 직렬화(Serialization)하는 기법이다.
Protocol Buffer는 작성된 Proto File을 기반으로 직렬화된다.
직렬화(Serialization)란?
데이터 표현을 바이트 단위로 변환하는 작업을 의미한다.
아래 예제처럼 같은 정보를 저장해도 Text 기반인 JSON의 경우 82byte가 소요되는데 반해, 직렬화 된 Protocol Buffer는 필드 번호, 필드 유형 등을 1byte로 받아서 식별하고, 주어진 length 만큼만 읽도록 하여 단지 33byte만 필요하게 된다.
JSON vs Protocol Buffer 데이터 비교

Proto File의 구성 요소

Proto File이란 Protocol Buffer의 기본 정보를 명세하는데 필요한 파일이다.
Proto File Style Guide
파일 이름은 lower_snake_case.proto, message 이름은 PascalCase 형태, field 이름은 lower_snake_case 형태로 사용할 것을 권장하고 있다.
추가로 field 이름은 일반적인 변수와 마찬가지로 숫자로 시작할 수 없다. Style Guide: Protocol Buffers Documentation

Message and Field

.proto File에서는 주고 받는 data들을 message 라는 것으로 정의한다. 이 메시지는 여러가지 타입의 필드로 구성된다.
아래 예시로 name, age, gender 라는 필드를 가지는 UserRequest 라는 메시지를 정의했다.
Field Tag (= Field number)
메시지에 정의된 필드들은 각각 고유한 번호를 가지게되고, 이는 Enconding 이후 binary data에서 필드를 식별하는데 사용된다.
Field Tag는 최소 1, 최대 536,870,911(=229–1) 로 지정 가능하며, 19000 ~ 19999는 프로토콜 버퍼 구현을 위해 예약된 값이므로 사용할 수 없다.
string name = 1; ← 여기서 1은 Value가 아니라 Field Tag(Field number)이다.
필드 번호가 1~15일 때는 1byte, 16~2047일 때는 2byte를 Tag로 가져가게 된다.
그 때문에 자주 호출되는 필드에 대해선 1~15로 지정해두는 것이 좋다.

Proto2 vs Proto3

user.proto 에서는 syntax = "proto3";을 통해 proto version 3을 사용한다.
이를 명시하지 않으면 기본적으로 proto version 2를 사용하게 된다.
proto3과 proto2는 서로 지원하는 언어도 다르지만, message 작성 시 field rule 지정 등 문법에도 차이가 있다.
Proto2 지원 언어 : C++, Java, Python, Go
Proto3 지원 언어 : C++, Java, Python, Go, Ruby, Objectice-C, C#, JavaScript, PHP, Dart

Proto File Filed Rule

required
필수로 가져야 할 필드 (Proto2에서만 사용)
optional
해당 필드를 가지지 않거나 하나만 가짐 (Proto2에서만 사용)
repeated
임의 반복 가능한 필드 (번호 및 값의 순서는 보존)
[packed=true] 옵션
key-value 쌍 형태에서 value만 반복
Proto2의 경우 required, optional를 필드 별로 꼭 명시해야 한다.
Proto3에선 required, optional은 사라지고, repeated만 사용한다.
optional은 ProtoBuf 3.15 버전에서 다시 추가되었다.
Proto2도 계속 기술지원이 되고 있으나, 지원 언어 및 새로운 기능 지원을 위해 Proto3을 사용할 것을 권장한다.
해당 글도 Proto3에 맞추어 기술하겠다.
requiredoptional이 사라진 이유
“깨지지 말아야 할 상황에서도 required 필드 때문에 backward/forward compatibility가 맞지 않게 되고 안전하게 required 필드를 제거/추가할 수 없었다. 구글 내부에서도 아무도 required 레이블을 쉽게 추가/제거할 수 없었다.” ”그래서 required를 제거하고 보니 optional도 필요 없어져서 같이 제거했다.” 출처 : github.com/protocolbuffers
구글 내에서는 실제로 best practice로 optional을 가급적 전부 붙이게 한다고 한다.
optional만 다시 추가된 이유 (추측)
optionalProtobuf 3.15에서 다시 공식적으로 추가됐다.
그래서 그런지 위 UserRequestProto3을 보면 optional에 빨간줄이 없는걸 확인할 수 있다.
WKT(WellKnownTypes)에 있는 주석을 보면 optional이 다시 추가된 이유에 대해 추측을 해볼 수 있다.
Primitive Type 필드에서 값의 부재와 default-value 사이의 차이를 알 수 있으니 유용하다. 출처 : protocolbuffers/protobuf/src/google/protobuf/wrappers.proto
대부분의 경우에는 아니겠지만, 분명히 각 타입에서 null과 default-value("", 0, false 등)의 차이를 주어야 할 때가 있는데, 그럴 때 처리할 수 있는 wrapper들이다.
그렇기에 optional 또한 비슷한 이유로 인해 추가되지 않았을까 추측된다.
위와 같이 repeated rule을 주게되면 필드를 배열의 형태로도 사용할 수 있게 된다.
필드는 Key-Value 구조로 저장되어 repeated 필드를 사용할 때도 key가 계속 붙게되는데, reqeated 뒤에 [packed = true] 옵션을 주면 value만 반복하게끔 할 수 있다.
필드 번호는 바뀌지 않으니 되도록이면 [packed = true]를 주면 보다 효율적으로 Enconding된다.
repeated int32 age = 2; // 2: {3 270}, 2: {86942} repeated int32 age = 2 [packed = true]; // 2: {3 270 86942} // https://protobuf.dev/programming-guides/encoding/#packed
Protobuf
복사

Package

Service

gRPC vs HTTP(JSON)

기능
gRPC
JSON을 사용하는 HTTP API
계약
필수(.proto)
선택 사항(OpenAPI)
프로토콜
HTTP/2
HTTP
페이로드
Protobuf(소형, 이진)
JSON(대형, 사람이 읽을 수 있음)
규범
느슨함. 모든 HTTP가 유효합니다.
스트리밍
클라이언트, 서버
브라우저 지원
보안
전송(TLS)
전송(TLS)
클라이언트 코드 생성
OpenAPI + 타사 도구