Microservices with gRPC

본 글에서는 gRPC에 대한 소개와 gRPC를 활용한 마이크로서비스에 대해서 이야기하려고 합니다. 필자는 gRPC가 공식적인 릴리즈(1.0)을 하기 전부터 제품에 적용해보면서, 실제로 높은 성능과 혁신적인 생산성을 직접 경험하였습니다. 특히 마이크로서비스의 구현에 매우 적합한 프레임워크임에도 아직 한국에는 널리 알려지지 않은 것 같아서 포스팅합니다.


What is gRPC?

gRPC의 역사는 구글의 데이터센터에서 실행되는 수많은 마이크로서비스들이 10년이상 사용한 단일 범용 RPC 인프라인 Stubby에서 시작됩니다. 구글의 내부시스템은 오래전부터 마이크로서비스 아키텍처를 받아들여 연구해왔고, 내부 서비스들을 연결하기 위해서 Stubby를 만들었습니다. 2015년 3월에 Stubby의 다음 버전을 계획하면서 소스를 오픈하기로 결정하였고, 구글의 내외부 서비스뿐만 아니라 모바일, IOT 등 다양한 엔드포인트(endpoint)에서도 활용하기 위해서 gRPC를 만들었습니다. gRPC는 높은 성능의 오픈소스 범용 RPC 프레임워크입니다.


Why should I use it?

gRPC는 서비스와 메시지를 정의하기 위해서 Protocol Buffers(이하 ProtoBuf)를 사용합니다. 아래와 같은 ProtoBuf의 IDL만 정의하면 높은 성능을 보장하는 서비스와 메세지에 대한 소스코드가 자동으로 생성됩니다.

자세한 예제는 뒷 부분에서 다시 다루겠지만, 위와 같은 서비스 정의만으로 서버와 클라이언트(Stub) 코드가 자동으로 생성됩니다. 위 예제의 경우는 sayHello()라는 서비스의 비지니스 로직과 클라이언트에서 Stub을 사용하여 호출해주는 부분만 구현해주면 됩니다.

ProtoBuf의 IDL을 활용한 서비스 정의 한개로 다양한 언어와 플랫폼에서 동작하는 서버와 클라이언트 코드가 생성됩니다. 공식적으로 지원하는 언어 및 플랫폼은 Officially Supported Platforms를 참고하시면 됩니다.

gRPC는 HTTP/2를 기반으로 통신합니다. HTTP/2의 특징과 장점에 대해서는 뒷 부분에서 자세히 다루도록 하겠습니다. HTTP/2에서는 양방향 스트리밍이 가능합니다. 즉, 일반적인 요청/응답 방식이 아니고 서버와 클라이언트가 서로 동시에 데이터를 스트리밍으로 주고 받을 수 있다는 것 입니다. 이 부분도 뒤에서 예제를 통해서 자세히 다루도록 하겠습니다.

HTTP/2의 또다른 장점중에 하나는 HTTP를 사용하는 전송보다 높은 헤더 압축률을 보장한다는 점입니다. gRPC에서는 HTTP/2에 의한 압축뿐만 아니라 protoBuf에 의한 메시지 정의에 의해서 메시지 크기를 획기적으로 줄일 수 있습니다. 메시지의 크기가 줄어드는 것은 곧 네트워크 트래픽이 줄어드는 의미하기 때문에 시스템 리소스를 절약하고 성능을 높일 수 있습니다.

gRPC는 필요에 따라 활용할 수 있는 Authentication, Tracing, Load Balancing, Health Checking, API Gateway 등의 생태계를 가지고 있습니다. 벡엔드 서버를 개발 하다보면 필요한 다양한 추가 기능이나 도구를 꽤 많이 갖추고 있어서 새로운 기술 스택을 추가로 리서치해서 사용할 일이 많지 않습니다.


Who is using it?

gRPC는 기본적으로 구글에서 사용하고 있습니다. 구글의 인프라 내부에서 돌아가는 수많은 서비스들이 gRPC 또는 gRPC의 전신인 Stubby를 사용하여 서비스되고 있습니다. 또한 Square, Netflix, CoreOS 등 회사의 다양한 언어와 플랫폼에서 개발된 수백개의 서비스들로 구성된 클라우드 환경에서 gRPC가 활용되고 있습니다. 즉, 다양한 회사의 마이크로서비스 아키텍쳐에 채택되고 있습니다.


When is it used?

gRPC는 단일 인스턴스로 돌아가는 CRUD 웹 애플리케이션에서부터 수백개의 서비스 인스턴스가 상호작용하는 마이크로서비스 아키텍쳐(이하 MSA)까지 거의 모든 서버 시스템 개발에 적합합니다. 마이크로서비스에 대해서는 아래 포스팅에서 자세히 다룬바 있으니 참고하시기 바랍니다.

gRPC가 MSA에 적합한 이유는 아래와 같습니다.

gRPC를 활용하면 비니지스 로직에 집중하여 빠른 서비스 개발이 가능하고, 간단한 설치와 빠른 배포가 가능합니다. 또한, 다양한 언어 및 플랫폼 지원으로 폴리글랏 언어와 기술스택을 지향하는 MSA의 철학과도 일맥상통합니다.

ProtoBuf가 지원하는 IDL 활용한 서비스 및 메시지 정의는 MSA의 다양한 기술 스택의 공존으로인한 중복 발생의 단점을 보완하고, 수많은 서비스간의 API 호출로 인한 성능 저하를 개선합니다.

MSA를 기반으로 하면서 분산처리를 위해서 필요한 Security, API Gateway, Tracing, Monitoring, Health Checking 등의 기능들을 Pluggable하게 포함하고 있습니다.

ProtoBuf에 의한 높은 메시지 압축률은 시스템 전체의 네트워크 트래픽을 획기적으로 줄여줍니다. 이것은 동일한 자원 제약에서 더 많은 서비스 인스턴스를 띄울 수 있다는 것을 의미합니다.

When is it not used?

gRPC는 RPC로 통신하기 때문에 간단한 REST API를 제공하는 서비스 개발에는 적합하지 않습니다. 하지만 MSA에서 외부에 REST API를 제공할때는 grpc-gateway를 활용할 수 있습니다.

gRPC의 튜토리얼이나 가이드는 상당히 쉽고 빠르게 습득이 가능합니다. 하지만 실제로 제품에 적용하고 운영할때는 ProtoBuf, HTTP2 등 이해해야 할 기반 기술들이 많습니다. 따라서 어느 정도의 Learning Curve는 각오해야 합니다.


How can I use it?

gRPC를 사용하는 것은 간단하지만, 제대로 사용하기 위해서는 몇가지 기반 기술 및 도구에 대한 이해가 필요합니다. 여기서는 간단한 예제를 포함해서 고려해야할 요소 및 기술들에 대한 설명을 포함합니다.

RPC(Remote Procedure Call)는 원격 컴퓨터나 프로세스에 존재하는 함수를 호출하는데 사용하는 프로토콜 이름입니다.

물론 RPC라는 개념이 존재하기 전부터 소켓 프로그래밍을 통해서 네크워크 상에 존재하는 서비스를 호출할 수는 있었습니다. 그러나 네크워크 상의 원격 통신은 느려지거나 서버가 응답하지 않는 등의 다양한 장애가 발생할 수 있습니다. 소켓 프로그래밍으로 직접 구현한다면 네크워크에서 발생 가능한 다양한 예외상황들을 개발자가 직접 핸들링해야만 합니다.

RPC는 이러한 네크워크 통신과 관련된 작업들을 대신해 줍니다. 개발자는 원격 컴퓨터나 프로세스에 존재하는 함수를 동일 프로세스에 존재하는 함수를 호출하는 것 처럼 호출할 수 있습니다.

gRPC는 ProtoBuf를 IDL 및 기본 메시지 교환 포멧으로 사용할 수 있습니다(JSON 등 다른 직렬화 방식을 사용할 수도 있음). ProtoBuf는 구글에서 만들고 사용하는 데이터 직렬화 라이브러리 입니다. ProtoBuf 3.0부터 gRPC를 지원하며 IDL로 메시지뿐만 아니라 서비스도 정의가 가능합니다.

// Person.proto
message Person {
string name = 1;
int32 id = 2;
bool has_ponycopter = 3;
}

IDL로 정의된 메시지를 기반으로 데이터 접근을 위한 코드를 생성하기 위해서 protoc 컴파일러를 사용합니다. 간단한 컴파일러 명령어로 원하는 언어의 코드를 생성할 수 있고, Person 클래스(자바의 경우)에 setter/getter 등 필요한 모든 메서드가 만들어 집니다.

위 예제는 간단한 gRPC helloworld 예제의 풀 버전입니다. proto 파일을 작성하기 위해서 여러가지 ProtoBuf 옵션 및 IDL에 대한 공부가 필요한 것을 알 수 있습니다. 자세한 내용은 Protocol Buffers 공식 사이트에서 확인할 수 있습니다. gRPC와 함께 사용하기 위한 문법은 proto3 가이드를 참고해야 합니다.

gRPC에서 사용하는 통신 프로토콜인 HTTP/2에 대해서 살펴보겠습니다.

HTTP/1은 기본적으로 클라이언트가 서버에 요청을 보내고, 서버가 요청에 대한 응답을 보내는 구조입니다. 따라서 요청 단위로 클라이언트와 서버를 왕복해야 합니다. 또한 쿠키를 포함한 헤더 크기는 불필요하게 큽니다. 이런 특징때문에 HTTP/1은 느립니다. 성능을 개선하기 위해서 구글은 SPDY를 개발하고, 이를 기반으로 HTTP/2 표준이 만들어집니다.

성능이 개선된 HTTP/2의 주요 특징은 아래와 같습니다.

Header Compression

Header Table과 Huffman Encoding 기법을 사용하여 HTTP/2 헤더정보를 압축하였습니다.

Multiplexed Streams

HTTP/1에서 요청마다 새로운 커넥션을 자주 만드는 것과는 달리 HTTP/2는 한개의 커넥션으로 동시에 여러개의 메시지를 주고 받을 수 있습니다.

Server Push

HTTP/2에서는 클라이언트의 요청없이도 서버가 리소스를 보낼 수 있습니다. 클라이언트 요청이 최소화되기 때문에 성능이 향상될 수 있습니다.

Stream Priority

요청에 우선순위를 지정하여 중요한 리소스를 먼저 전달받을 수 있습니다.

gRPC는 이런 HTTP/2의 특징을 기반으로 하기때문에 양방향 스트리밍이 가능하고, 기본적인 통신 속도가 빠릅니다. on-connection 상태에서 비동기 통신의 구현이 용이합니다.

간단한 gRPC 자바 예제를 통해서 개발자의 작업 플로우를 살펴보겠습니다. (원본 소스코드)

1) .proto 파일에 서비스를 정의합니다.

gRPC는 일반적인 RPC뿐만 아니라 server-to-client streaming RPC, client-to-server streaming RPC, bidirectional streaming RPC를 만들어서 사용할 수 있습니다.

2) 서버와 클라이언트 코드를 생성합니다.

코드를 생성하기 위해서는 ProtoBuf의 컴파일러인 protoc를 사용합니다. gradle이나 maven을 사용하고 있다면 protoc 빌드 플러그인을 사용하여 코드를 생성할 수 있습니다. 관련 내용은 README에 자세하게 설명되어 있습니다.

ProtoBuf 메시지 정의에 따라서 Feature.java, Point.java, Rectangle.java 등의 메시지 파일이 생성되고 각 필드에 대한 getter, setter, 직렬화 코드 등이 생성됩니다.

gRPC 서비스 정의에 따라서 RouteGuideGrpc.java 파일이 생성되고, RouteGuideGrpc.RouteGuideImplBase를 상속한 서비스 코드에 비지니스 코드를 작성하면 됩니다.

RouteGuide 서비스의 메서드를 호출하기 위한 클라이언트(stub) 코드들이 생성됩니다. 클라이언트에서 이 Stub을 사용하여 서비스의 함수를 호출하면 됩니다.

3) 서비스를 구현합니다.

4) 서버를 실행 코드를 작성합니다.

서비스의 비니지스 로직은 위에서 작성했지만, 서비스가 실행될 서버 코드가 필요합니다. 서버 코드에서는 포트를 설정하고, 작성된 서비스를 실행하기 위한 부분입니다. gRPC에서는 ServerBuilder를 사용하여 쉽게 서버 코드를 작성할 수 있습니다.

5) 클라이언트를 구현합니다.

gRPC에서는 클라이언트(Stub) 코드도 생성됩니다. 클라이언트에서 생성된 코드를 사용하여 gRPC Stub을 생성하고 서비스의 각 메서드를 호출하는 코드를 구현합니다.