본문 바로가기

엔지니어링(TA, AA, SA)/성능과 튜닝

[gRPC] gRPC 깊게 파고들기

https://blog.naver.com/n_cloudplatform/221751268831

https://blog.naver.com/n_cloudplatform/221751405158

 

더보기

1. Server-Client Model

PC(Personal Computer)의 개념이 없던 시절, 프로그램은 하나의 메인 프레임에서 동작하는 Monolithic 구조로 설계되었다. 이때까지만 해도 모든 기능들이 한 공간에서 구동되다 보니 지금처럼 네트워크 통신이 크게 중요하지 않았다. 기술 발전에 따라 소형 컴퓨터 장비들이 등장하게 되고, 기업 입장에선 매우 고가인 메인 프레임워크를 비교적 저가의 워크스테이션 서버로 대체하고 싶어했다. 하지만 메인 프레임워크의 초고사양 서비스를 워크스테이션 서버에서 그대로 제공하기엔 한계가 있다.

 

이때문에서 메인 프레임워크의 기능을 워크스테이션 서버로 분산시키고, 네트워크 연결로 서비스하는 방식을 채택하게된다. 흔히 말하는 Server-Client Model이다. 이처럼 서버간 혹은 서버와 개인 PC 간 네트워크 연결/통신이 중요해지면서 OSI 7 layer, TCP/IP 등 네트워크 계층 구조가 정의되고 발전하기 시작한다.

2. IPC

프로세스들은 기본적으로 상호독립적이다. 메모리를 공유하지 않기 때문에 각자 자신의 일만 하며 서로 간섭하지 않는다. 하지만 필요에 따라 프로세스간 정보를 교환해야 하는 경우가 있다. 이때 별도 수단을 이용하여 프로세스 통신하는 방법론을 통칭하여 IPC(Inter Process Communication) 라고 한다.

 

1) Socket

IPC 기법에는 공유 메모리, PIPE, 메시지 큐 등 여러가지가 있지만 이중 소켓(socket)을 살펴본다. socket이란, 앞서 언급한 OSI 7 layer 구조의 Application Layer(L7)에서 Transport Port(L4)의 TCP 또는 UDP를 이용하기 위한 수단이다. 일종의 창구 역할을 하는 것이다. 목적지와의 통신이 컴퓨터 내부가 아니라 온라인 범위에서 이루어지기 때문에 네트워크 간 통신이라고 구분하기도 하지만, 실질적으로는 로컬 컴퓨터의 프로세스와 원격지 컴퓨터의 프로세스가 IPC 통신을 하는 것이다.

 

소켓은 대부분의 언어에서 API 형태로 제공하는 편리함 때문에 지금도 많이 사용되고 있지만, 일련의 통신 과정을 직접 구현하므로 통신 관련 장애를 처리하는 것은 고스란히 개발자의 몫이 된다. 서비스가 고도화될 수록 수백 수천가지 데이터가 돌아다니게 될텐데, 이에 따라 data formatting을 하는것도 점점 어려워지게 된다.

 

2) RPC (Remote Procedure Call)

이런 소켓의 한계에서 RPC(Remote Procedure Call)라는 기술이 등장한다. 이름 그대로 네트워크로 연결된 서버 상의 프로시저(함수, 메서드 등)을 원격으로 호출할 수 있는 기능이다. 네트워크 통신을 위한 작업 하나하나 챙기기 귀찮으니. 통신이나 call 방식에 신경쓰지 않고 원격지의 자원을 내것처럼 사용할 수 있다. IDL(Interface Definition Language) 기반으로 다양한 언어를 가진 환경에서도 쉽게 확장이 가능하며, 인터페이스 협업에도 용이하다는 장점이 있다. (지원 언어: C++, Java, Python, Ruby, Node.js, C#, Go, PHP, Objective-C...)

 

RPC의 핵심 개념은 'Stub(스텁)'이라는 것이다. 서버와 클라이언트는 서로 다른 주소 공간을 사용하므로, 함수 호출에 사용된 매개 변수를 꼭 변환해줘야 한다. 안그러면 메모리 매개 변수에 대한 포인터가 다른 데이터를 가리키게 될것이다. 이 변환을 담당하는게 스텁이다.

 

Client Stub은 함수 호출에 사용된 파라미터의 변환(Marshalling, 마샬링) 및 함수 실행 후 서버에서 전달된 결과의 변환을, Server Stub은 클라이언트가 전달한 매개 변수의 역변환(Unmarshalling, 언마샬링) 및 함수 실행 결과 변환을 담당하게 된다. 이런 Stub을 이용한 기본적인 RPC 통신 과정을 살펴본다.

1. IDL(Interface Definition Language)을 사용하여 호출 규약을 정의한다.

    - 함수명, 인자, 반환값에 대한 데이터형이 정의된 IDL 파일을 rpcgen으로 컴파일하면 stub code가 자동으로 생성된다.

2. Stub Code에 명시된 함수는 원시코드의 형태로, 상세 기능은 server에서 구현된다.

    - 만들어진 stub 코드는 클라이언트/서버에 함께 빌드한다.

3. client에서 stub에 정의된 함수를 사용할때,

4. client stub은 RPC runtime을 통해 함수 호출하고

5. server는 수신된 procedure 호출에 대한 처리 후 결과 값을 반환한다.

6. 최종적으로 Client는 Server의 결과 값을 반환받고, 함수를 Local에 있는 것처럼 사용할 수 있다.

 

RPC는 상당히 획기적인 방법론이었으며, 분산 환경의 등장에 따라 함께 발전해 온 오래된 기술이다. 따라서 구현체도 CORBA, RMI 등 여러가지가 있었다. 이들 모두 로컬에서 제공하는 빠른 속도, 가용성 등을 분산 프로그래밍에서도 제공하고 있다고 홍보를 했지만, 정작 구현의 어려움/지원 기능의 한계 등으로 제대로 활용되지 못했다. 그렇게 RPC 프로젝트도 점차 뒷길로 가게되며 데이터 통신을 우리에게 익숙한 Web을 활용해보려는 시도로 이어졌다.

 

3) REST (REpresentational State Transfer)

REST는 HTTP/1.1 기반으로 URI를 통해 모든 자원(Resource)를 명시하고 HTTP Method를 통해 처리하는 아키텍쳐이다. 자원 그 자체를 표현하기에 직관적이고, HTTP를 그대로 계승하였기에 별도 작업없이도 쉽게 사용할 수 있다는 장점으로 현대에 매우 보편화되어있다. 하지만 REST에도 한계는 존재한다. REST는 일종의 스타일이지 표준이 아니기때문에 parameter와 응답 값이 명시적이지 않다. 또한 HTTP 메소드의 형태가 제한적이기 때문에 세부 기능 구현에는 제약이 있다.

 

덧붙여, 웹 데이터 전달 format으로 xml, json을 많이 사용한다. XML은 html과 같이 tag 기반이지만 미리 정의된 태그가 없어(no pre-defined tags) 높은 확장성을 인정받아 이기종간 데이터 전송의 표준이었으나, 다소 복잡하고 비효율적인 데이터 구조탓에 속도가 느리다는 단점이 있었다. 이런 효율 문제를 JSON이 간결한 Key-Value 구조 기반으로 해결하는 듯 하였으나, 제공되는 자료형의 한계로 파싱 후 추가 형변환이 필요한 경우가 많아졌다. 또한 두 타입 모두 String 기반이라 사람이 읽기 편하다는 장점은 있으나, 바꿔 말하면 데이터 전송 및 처리를 위해선 별도의 Serialization이 필요하다는 것을 의미한다.


더보기

gRPC는 google에서 개발한 오픈소스 RPC(Remote Procedure Call) 프레임워크이다. 이전까지는 RPC 기능은 지원하지 않고, 메시지(JSON 등)을 Serialize할 수 있는 프레임워크인 PB(Protocol Buffer, 프로토콜 버퍼)만을 제공해왔는데, PB 기반 Serializer에 HTTP/2를 결합하여 RPC 프레임워크를 탄생시켰다.

 

REST와 비교했을때 기반 기술이 다르기에 특징도 많이 다르지만, 가장 두드러진 차이점은 HTTP/2를 사용한다는 것과 프로토콜 버퍼로 데이터를 전달한다는 점이다. 그렇기에 Proto File만 배포하면 환경과 프로그램 언어에 구애받지 않고 서로 간의 데이터 통신이 가능하다.

1. HTTP/2

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

2. ProtoBuf (Protocol Buffer, 프로토콜 버퍼)

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

3. Proto File

자세한 Encoding/Decoding 원리는 Protocol Buffer의 기본 정보를 명세하는 Proto File의 구성 요소를 살펴본다. 

 

1) Message and Field

Proto File에서는 주고 받는 data들을 message라는 것으로 정의한다. 이 메시지는 여러가지 타입의 필드로 구성되는데, 아래 예시로 query, page_number, result_per_page라는 필드를 가지는 SearchRequest라는 메시지를 정의한 proto file이다.

>> Naming

message 이름은 CamelCase 형태, field 이름은 under_bar 형태로 사용할 것을 권장하고 있다(필수는 아니다). 유의할 것은 field 이름은 숫자로 시작할수 없다는 점이다. 숫자로 표기해야할 경우 꼭 문자 뒤에 표기해주어야 한다.

 

>> Field Tag (= Field number)

메시지에 정의된 필드들은 각각 고유한 번호를 가지게되고 이는 Encoding 이후 binary data에서 필드를 식별하는데 사용된다. Field Tag는 최소 1, 최대 536,840,911(=229-1)로 지정 가능하며, 19000 ~ 19999는 프로토콜 버퍼 구현을 위해 reserved 된 값이므로 사용할 수 없다. 필드 번호가 1~15일때는 1byte, 16~2047은 2byte를 Tag로 가져가게 된다. 때문에 자주 호출되는 필드에 대해선 1~15로 지정해두는 것이 좋다.

 

>> proto2 VS proto3

위 예제에서는 첫줄에 syntax = "proto3"을 지정해줌으로써 proto version 3의 규약을 따르겠다고 선언했다. 이를 명시하지 않으면 default로 version2 문법을 따르게 된다. 아래와 같이 지원 언어도 다르지만, message 작성시 field rule 지정 등 문법에서도 차이가 나타난다.

  - Proto2 지원 언어: C++, Java, Python, Go

  - Proto3 지원 언어: C++, Java, Python, Go, Ruby, Objective-C, C#, JavaScript, PHP, Dart

 

>> Proto File Field Rule

  - required: 필수로 가져야 할 필드 (only use proto2)

  - optional: 해당 필드를 가지지 않거나 하나만 가짐 (only use proto2)

  - repeated: 임의 반복 가능한 필드 (번호 및 값의 순서는 보존)

   * [packed=true] 옵션: key-value 쌍 형태에서 value만 반복

위 예시처럼 proto2의 경우 required, optional를 필드 별로 꼭 명시해주어야 한다. proto3에선 required, optional은 사라지고, repeated만 사용된다. proto2도 계속 기술지원이 되고 있으나, 지원 언어 및 새로운 기능 지원을 위해 proto3을 사용할 것을 권장한다.

위와 같이 repeated rule을 주게되면 Field를 배열의 형태로도 사용할 수 있게 된다. 필드는 Key-Value 구조로 저장되어 repeated field를 사용할때도 key가 계속 붙게되는데, repeated 뒤에 packed 옵션을 주면 value만 반복하게 하는것도 가능하다. 필드 번호는 바뀌지 않으니 되도록 이 옵션을 주면 보다 효율적인 Encoding이 될 수 있다.

 

2) Package

package는 message type 이름을 중첩없이 구분할때 사용한다. 메시지 사용시 package를 명시함으로써 필드와 명확히 구분해준다. 아래 예제에서는 Open이라는 message를 타입으로 하는 field 이름을 open으로 주어 모호한 정의를 package로 구분하였다. foo.bar라는 package를 굳이 쓰지 않는다고 사용이 불가한 것은 아니지만, 구성 메시지가 많다면 명확하게 구분될 수 있게 명시해주는 것이 좋다.

 

3) Service

Service는 RPC를 통해 서버가 클라이언트에게 제공할 함수의 형태를 정의한다. 서비스명과 RPC 메소드명 모두 CamelCase 형태를 권장한다. 옵션을 주지 않으면 단일 요청/응답으로 동작하지만, stream 옵션을 주면 양방향 RPC를 구현할 수 있다.

4. Protocol Buffer Encoding

기본적인 proto file 작성법 및 message 구조를 알았으니, 실제로 Serialize 되는 방식을 살펴본다.

 

1) Message (Key-Value) Encoding

앞서 정의한 message 들은 일련의 Key-Value 쌍으로 이루어진 binary data로 인코딩 된다. key는 field Number 뿐만 아니라, 해당 Field의 data type을 지시하는 Wire Type을 표현한다.

 

Key는 field_number<<3)|wire_type의 형태로, 일반적인 1byte인 경우 key는 Field Number(5bit) + Wire Type(3bit)로 이루어진다. Field Number는 proto file에 명시된대로 들어가지만, WireType은 선언한 data type 별로 지정된다.

protocol buffer wire type

앞선 proto3 message 예제를 다시 살펴보겠다. query의 Field number는 1이며, string이므로 wire type은 2가 된다. 때문에 Key는 00001|010=0x12가 될것이다. result_per_page 필드의 경우 Field number = 3 이며, int32이므로 wire type이 0 즉, 00011|000 = 0x24를 Key로 가지게 된다.

 

2) Varints

>> wire type 0

wire type에 따른 자료형은 여러가지가 있지만 정수를 Serializing하는 Varints Encoding부터 알아본다. Varints에 포함되는 정수형 타입들은 첫 byte의 1bit를 무조건 뒷 byte에 대한 지시 역할을 하는 msb(most significant bit)로 가지게 된다. 이 값이 1이면 뒷 데이터가 더 있다는 것이고, 0이라면 이어지는 byte stream과 분리된다는 의미이다. "least significant group first" 룰을 따르기 때문에 하위 byte부터 저장된다.

 

1byte로는 Field number를 1~15까지만 표현 가능한 이유가 여기에 있다. Field number는 정수형이므로 Varints Encoding 법칙을 따르게 된다. 따라서 1byte key에서 wire type 부분을 제외한 5bit 중 1bit가 msb로 사용된다. 즉 key가 1byte라도 실제 필드 번호 값을 담는 크기는 4bit이기 때문에 1~15까지만 표현가능한 것이다. 그이상의 값이라도 Varints Encoding 법칙을 벗어나지는 않는다. 

 

>> Encoding

300 이라는 정수를 Encoding 해본다.

1) 300은 이진수로 256 + 32 + 8 + 4 = 100101100 로 표기된다.

2) Varints 직렬화를 위해선 byte 당 msb가 포함돼야 하므로 7bit 단위로 구분해준다.

  -> □000 0010□010 1100

3) least significant group first 룰에 맞게 byte를 역순으로 나열한다.

  -> □010 1100□0000010

4) msb를 설정해준다.

  -> 1010 11000000 0010

 

>> Decoding

만약 1010 11000000 0010라는 직렬화 데이터를 받았고 이를 Decoding 한다면, 위 절차를 거꾸로 따라가면 된다.

1) msb를 제외한다.

  -> □010 1100□000 0010

2) byte는 역순으로 나열되어 있으므로 다시 역순으로 정렬한다.

  -> □000 0010□010 1100

3) msb 제외 후 data를 연접한다.

  -> 10010 1100= 300

 

>> Signed Integers

만약 protocol buffer에서 음의 정수를 표기한다면 signed int(sint) 형을 사용하는게 효율적이다. 일반 int형에서는 음수로 사용시 절댓값에 관계없이 항상 고정된 byte 크기를 잡는데, signed int형은 ZigZag Encoding으로 이미 부호있는 정수를 부호없는 정수로 매핑시켜 두었기 때문이다.

 

3) Non-Varint number

>> wire type 1,5

정수형이 아닌 실수형 타입은 예외 없이 간단하다. wire type 1인 64bit double 등은 고정된 64bit 데이터를 지시하며, wire type 5인 32bit형 float 등은 고정된 32bit 데이터를 지시한다.

 

>> wire type 2

string 같이 wire type이 2인 경우, varints 형태에서 길이를 지시하는 byte가 추가된다. 즉 key를 읽어서 wire type이 2라면 그 다음 바이트는 길이를 지정하는 것이다. 실제로 지정된 value는 UTF-8로 인코딩된다.

 

예를 들어 query 필드의 type은 string이었고 key는 0x12라고 했다. 이 query 필드에 "testing"이라는 데이터가 저장되었다면 Encoding시 12 07 74 65 73 74 69 6e 67로 표기될 것이다.

- 0x12 : 0001 0010  field num = 2 & wire type = 2 (string)
- 0x07 : 7 byte
- UTF-8 encoding : "testing"

길이 byte는 1byte로 지정된 것은 아니며, 만약 wire type2 데이터의 길이가 0xFF = 255 bytes 이상이라면 아래와 같이 2byte 이상으로도 표기된다. 참고로 default message limit는 4*1024*1024 = 4Mbyte이다.

 

4) Embedded Message

package 설명에서 잠깐 예를 들었지만 message 정의시 field type을 다른 메시지로도 지정할 수 있다. 아래와 같은 Test1 이라는 message에 a라는 field가 있고 여기에 150이 설정됐다고 가정하면 a 필드는 08 96 01으로 encoding된다. 이 Test1이라는 message가 Test3 message의 field type으로 사용된다면, wire type = 2 이므로 길이 byte가 추가되고 결과적으로 c 필드는 1a 03 08 96 01로 encoding 된다.

>> a=150
08 96 01
0x08 : 0000 1000 -> field num = 1 & wire type = 0 (int32)
0x96 0x01 : 1001 0110 0000 0001
-> □001 0110 □000 0001
-> 0000001 0010110 → 10010110

>> c
0x1a : 0001 1010 → filed num = 3 & wire type = 2 (embedded Mesgage)
0x03 : 3byte
08 96 01: a=150

5. Value Type

1) Scalar Type

아래 표는 일반적으로 사용되는 Scalar 자료형들이 각 언어로 generate 되었을때 어떤 type으로 변환되는지 정리한 표이다. 유의 사항이 언어별로 좀 상이한데 특히 python의 경우 예외가 많다.

.proto Type

C++

Java

Python[2]

Go

Ruby

C#

PHP

Dart

double

double

double

float

float64

Float

double

float

double

float

float

float

float

float32

Float

float

float

double

int32

int32

int

int

int32

Fixnum or Bignum (as required)

int

integer

int

int64

int64

long

int/long[3]

int64

Bignum

long

integer/string[5]

Int64

uint32

uint32

int[1]

int/long[3]

uint32

Fixnum or Bignum (as required)

uint

integer

int

uint64

uint64

long[1]

int/long[3]

uint64

Bignum

ulong

integer/string[5]

Int64

sint32

int32

int

int

int32

Fixnum or Bignum (as required)

int

integer

int

sint64

int64

long

int/long[3]

int64

Bignum

long

integer/string[5]

Int64

fixed32

uint32

int[1]

int/long[3]

uint32

Fixnum or Bignum (as required)

uint

integer

int

fixed64

uint64

long[1]

int/long[3]

uint64

Bignum

ulong

integer/string[5]

Int64

sfixed32

int32

int

int

int32

Fixnum or Bignum (as required)

int

integer

int

sfixed64

int64

long

int/long[3]

int64

Bignum

long

integer/string[5]

Int64

bool

bool

boolean

bool

bool

TrueClass/FalseClass

bool

boolean

bool

string

string

String

str/unicode[4]

string

String (UTF-8)

string

string

String

bytes

string

ByteString

str

[]byte

String (ASCII-8BIT)

ByteString

string

List<int>

 

2) Default Value

Protocol buffer에서 지정하는 디폴트 값들이다. 특별히 유의해야 할 점은 enum이라는 열거형 데이터는 첫번째 값을 default value로 지정한다는 점이다. message field에 사용되는 Type에 경우 언어에 따라 조금씩 다르기 때문에 API reference 문서를 참조하면 된다.

 

3) Enumeration

>> 기본 문법

이번엔 첫번째 값을 default value로 삼는 enum형을 살펴본다. enum은 상수를 저장하는 열거형 데이터이며, encoding 하면 const 상수형태로 저장되고, 각 name/value 별 map이 별도로 지정된다.

 

예를 들어 메시지 유형을 정의할때, enum에 원하는 상수 값을 넣어두면 이걸 값으로도 참조할 수 있다. enum형을 쓸때 유의할것은 default value에 정의됐다시피 첫번째 값이 default 값으로 지정되므로 첫상수는 0으로 지정해줘야 한다는 점이다. 그 밖에 값들은 세미콜론(;)으로 구분한다.

>> 옵션

enum에서 사용할 수 있는 옵션이 2가지가 있다. enum 전체에 적용되는 allow_alias 옵션을 주면 다른 name에 같은 값을 줄 수 있다. 향후 value를 참조할 때는 복수 개의 값이 있을테니 oneof로 하나만 선정하여 사용한다.

 

enum 값을 삭제하거나 주석처리하여 update 시켰다면 다른 사용자가 이 값을 재사용하게될 수도 있는데, 이는 proto file의 버전이 안맞을 경우 데이터 손상 등의 문제를 일으킬 수 있다. 이를 방지하고 싶다면, value / name을 더이상 접근할 수 없게 reserve한다. 단, 아래 예제와 같이 reserved만 선언한다면 error가 리턴되므로 default 값을 고려하여 enum 첫줄엔 하나 이상의 상수를 선언해야 한다.

 

4) Maps

다음은 map이다. 흔히 아는 일련의 key-value 쌍이다. 이 맵의 필드들은 repeated 될 수 없으며, 순서가 있는 데이터도 아니고 언어별로 구현 방식이 상당히 다르다. key type은 string과 scalar type의 자료형만 지정 가능하면 value type은 map 외의 모든 자료형 지정이 가능하다.

map의 value type으로는 map이 지원되지 않음

이런 경우엔 map field를 갖는 message를 다시 사용하는 방식으로 구현한다. 하지만 이렇게하면 쓸데없이 stub code가 복잡해지는 터라 권장하지 않으면, 꼭 필요하다면 message name을 줄이면서 용량을 축소시킬 수 있다.

6. 인증 (Authentication)

암호화 인증이 필요한 경우 ssl/tls 기반 그리고 token 기반, 두가지 메커니즘을 지원한다. 대체적으로 전자를 사용하여 인증 후 암호화 통신을 진행한다. 아래 그림과 같이 client의 경우 dial 옵션으로, Server에서 신규 grpc 서버 생성시 적용된다. token 기반 인증은 gRPC로 Google API 접근시 OAuth2 token과 같은 google access token이 지원된다.

7. Failover Test

gRPC 운영환경에서 일어날 수 있을만한 case를 가정하여 테스트한 결과.

 

1) Server down일때 Client 가 요청하는 경우

Server가 준비되지 않은 상태에서 Client가 요청하는 경우, retransmission은 8번씩 새로운 session이 맺어질때까지 무한정 시도된다.

2) Server down 후 다시 up 되었을때

일반적으로 server가 down된다면 timeout에 걸려 통신이 종료된다. timeout을 일정치 이상 준 경우에 server down 감지 후 recovery 되어 올라왔을때 retransmission에 응답하여 새로운 세션으로 연결된다.

3) Client timeout이 충분히 커도, 8회 초과시 close

단 timeout이 충분히 큰 경우라도 8회 Retransmission까지 응답이 없으면 해당 통신은 종료된다.

 

4) L4 환경에서 failover

VIP로 gRPC 요청할때, session이 할당된 active server down 시엔 기존 session은 FIN 종료 후 즉시 새로운 session으로 연결된다.


더보기

gRPC에서는 아직 브라우저 관련 API가 제공되지 않기 때문에 브라우저에서 직접 gRPC 서비스를 호출하는 것은 불가능하다. 또한 기존 데이터 통신과 다르게 텍스트 기반이 아니라 Encoding 된 Binary Stream이기 때문에 사람이 읽기는 어렵다. 하지만 아래와 같이 장점이 훨씬 큰 기술이므로 서비스 개발시 높은 생산성, 다양한 언어, 빠른 속도 등의 좋은 퍼포먼스를 보여줄 것이다.

 

1) 높은 생상선과 효율적인 유지보수

  - ProtoBuf의 IDL만 정의하면 높은 성능을 보장하는 서비스와 메시지에 대한 소스코드가 자동으로 생성

2) 다양한 언어와 플랫폼 지원

  - IDL을 활용한 서비스 정의 한 개로 다양한 언어와 플랫폼에서 동작하는 서버와 클라이언트 코드가 생성

3) HTTP/2 기반의 양방향 스트리밍

  - 서버와 클라이언트가 서로 동시에 데이터를 스트리밍으로 주고받음

4) 높은 메시지 압축률과 성능

  - HTTP/2에 의한 압축뿐만 아니라 protoBuf에 의한 메시지 정의에 의해서 메시지 크기를 획기적으로 줄임

5) 다양한 gRPC 생태계

  - 필요에 따라 Authentication, Tracing, Load Balancing, Health Checking, API Gateway 등의 다양한 도구 지원

 

어떤 서비스에 gRPC를 사용하면 좋을까?

거의 모든 서버 시스템 개발에 효율적으로 적용될 수 있지만, 특히 Microservice Architecture 서비스에 적합하다. 마이크로서비스는 작은 서비스들을 유기적으로 결합해 하나의 응용프로그램을 개발하는 방법론이다. 구성 서비스가 독립적이기에 개발 및 배포 운영이 용이하며, 확장을 유연하게 할 수 있다. 때문에 새로운 기술 도입 및 변경에도 용이한 면을 보인다.

 

하지만 분산 시스템 특성상 공통 기능의 중복이 발생하여 메모리를 비효율적으로 사용할 수도 있고, 프로그램의 규모가 커질수록 구성원들의 철학이나 기술 스택이 제각기 다르니 운영도 어려워진다. 이에 gRPC는 앞서 언급한 특징 덕에 이러한 단점을 보완하며 장점을 극대화 시킬 수 있다.

 

브라우저를 사용하지 않는 백엔드간 서버 통신이나, 자원 한정적인 환경에서도 유용하다. byte/호출/cpu 수 등으로 과금되는 클라우드 환경에서는 비용 절감의 효과도 생각할 수 있다. 최근엔 시스코, 주니퍼 등 주요 네트워크 장비에서고 grpc를 모두 지원하고 있어 모니터링이나 자동화 등 인프라 운영에도 활용 방안이 많을 것으로 기대한다.