gRPC 훑어보기
2021.02.10 12:59
gRPC 훑어보기
안녕하세요. NHN EDU 아이엠클래스팀 이광신입니다.
이번에 컴퓨터비전팀과 함께 시험용 OMR 인식 및 판독기 기능을 신규 개발하면서 컴퓨터비전팀에서는 딥러닝을 활용하여 python으로 개발이 되었고,
제가 익숙한 언어는 자바였기 때문에 OMR 문서 스캔/업로드는 자바로, 판독은 python 서버에서, 그리고 다시 EXCEL 파일 생성을 자바에서 개발을 하였고,
판독 요청을 python 서버로 전달하기 위해 어플리케이션 간 통신 방식을 gRPC로 정하게 되면서 사용을 하게 되었습니다.
이런 목적으로 팀내에서 공유하기 위해서 gRPC 에 대해 조사한 내용을 함께 공유하고자 합니다.
RPC
우선 RPC에 대해 간단히 확인하고 본론으로 넘어가도록 하겠습니다.
RPC는 Remote Procedure Call 의 약자로 분산 네트워크 환경에서 조금 더 편하게 프로그래밍 하기 위한 등장하였습니다.
클라이언트 - 서버 간의 커뮤니케이션에 필요한 상세한 정보는 최대한 감추고 개발자는 각 로직에만 집중할 수 있도록 클라이언트/서버는 일반 메소드를 호출하는 것처럼 개발을 진행하면 됩니다.

- caller/callee
개발자가 필요한 비지니스 로직을 생성하고 정의된 IDL(interface definition language)로 작성하여 stub 을 호출합니다. - Stub
Stub compiler 가 IDL 파일을 읽어 원하는 language 로 생성하고 파라미터를 마샬링/언마샬링 처리하여 RPC 프로토콜로 전달합니다. - RPC runtime
통신하여 각 메세지를 전달하게 됩니다.
gRPC
그럼 이제 gRPC에 대해 확인을 해보도록 하겠습니다.
공식 페이지의 설명을 기반으로 간단하게 요약해 보았습니다. (https://grpc.io/)
gRPC는 google 에서 마이크로서비스에 사용하던 단일 범용 RPC 인프라 Stubby 에서 시작하였습니다. Stubby 다음 버전을 계확하면서 외부에 오픈하기로 결정하하였다고 하네요. 높은 생산성과 효율적인 유지보수, 다양한 언어와 플랫폼 지원, 높은 메세지 압축률과 성능, 이러한 특징으로 한마디로 높은 성능의 오픈소스 범용 RPC 프레임워크입니다.

각 특징에 대해 추가로 확인해보겠습니다.
높은 생산성과 효율적인 유지보수
IDL(Identity Definition Language)로 protocol buffers(protobuf) 사용합니다. IDL만 정의하면 높은 성능을 보장하는 서비스와 메세지에 대한 소스코드가 각 언어에 맞게 자동 생성됩니다.
따라서 개발자들은 생성된 코드를 클라이언트, 서버 간의 사용 언어에 구애받지 않고 사용하기만 하면 되며 정해진 규약을 공통으로 사용하기 때문에 의사소통 비용이 감소하게 됩니다.
다양한 언어와 플랫폼 지원
앞서 말씀드린 것과 같이 IDL 정의 한개로 다양한 언어와 플랫폼에서 동작하는 서버와 클라이언트 코드가 생성됩니다.
현재 공식 지원 언어들은 아래와 같습니다.

https://www.grpc.io/docs/
높은 메시지 압축률과 성능
gRPC 는 내부적으로 HTTP/2 사용하여 헤더 압축률이 높고 protobuf 에 의해 통신시점에는 바이너리 데이터로 통신하기 때문에 메시지 크기가 작습니다.
그리고 추가로 HTTP/2 이기 때문에 양방향 스트림 통신이 가능하죠.
단점
이런 장점들이 있지만 간단한 RESTful API 를 제공을 목적으로 한다면 부적합하죠. 그리고 protobuf 및 HTTP/2 에 대한 아주 약간의 러닝 커브가 존재합니다.
또한 일반 REST API 와 다르게 메세지가 바이너리로 전달하게 되어 개발자들의 테스트가 쉽지 않은데, 이건 조사하다보니 POSTMAN과 유사한 bloomRPC(https://github.com/uw-labs/bloomrpc) 툴을 통해 gRPC 테스트를 해볼 수 있더라고요.
Protocol Buffers
Protocol Buffers 에 대해서도 간단히 확인해 보겠습니다.
gRPC 에서 IDL 로 사용하고 있는 언어입니다. 구글에서 만들고 사용하는 데이터 직렬화 라이브러리로 아래와 같을 문법을 사용합니다.
이렇게 작성된 proto파일을 통해서 protoc 컴파일러를 통해 각 언어로 소스 코드가 생성됩니다.
아래와 같은 예시로 protocol buffer 가 선언되었고, 이는 통신하는 시점에 아래 이미지와 같이 인코딩되어 송수신됩니다. 따라서 본문의 사이즈가 훨씬 간결해지죠.
message Person {
required string user_name = 1;
optional int64 favourite_number = 2;
repeated string interests = 3;
}

Sample
이제 간단한 샘플 코드를 통해 gRPC 사용법에 대해 알아보고자 합니다.
JAVA로 서버로 구성하고 Python 으로 클라이언트를 구성하였고요. 단순하게 Member 등록하는 기능을 샘플로 만들어보았습니다.
1. proto생성
우선 proto 파일을 생성해야 합니다.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.ks.grpc";
option java_outer_classname = "Member";
package grpc;
// enum 정의
enum Gender {
MALE = 0;
FEMALE = 1;
}
// message = object 정의
message Class {
// int32 -> int(java)
int32 classNo = 1;
// string -> String(java)
string className = 2;
}
message MemberRequest {
string name = 1;
// 위에 정의한 enum 값 활용
Gender gender = 2;
string phoneNo = 3;
repeated Class classList = 4;
}
message MemberResponse {
// int64 -> long(java)
int64 memberNo = 1;
}
// 자동 생성되는 클래스
service RegisterMember {
// 클래스 메소드 생성
//unary : 한 번 호출에 한 번 응답
rpc Register(MemberRequest) returns (MemberResponse);
//client stream : 클라이언트에서 스트림으로 전달, 서버에서는 한번 응답
rpc RegisterClientStream(stream MemberRequest) returns (MemberResponse);
//server stream : 클라이언트에서 한 번 전달, 서버에서 스트림으로 응답
rpc RegisterServerStream(MemberRequest) returns (stream MemberResponse);
//bi stream : 클라이언트/서버 모두 스트림으로 응답
rpc RegisterBiStream(stream MemberRequest) returns (stream MemberResponse);
}
syntax 로 사용할 문법을 선언합니다.
enum, message 로 Object를 정의하고 service 로 클래스가 생성됩니다.
service 안에는 메소드들을 선언해주면 되는데, 여기서 stream 문구를 통해 스트림 사용에 대해서 정의할 수 있습니다.
그에 따라 메소드 통신 방식이 일부 다르게 적용됩니다.
2. source generate
이제 위에 생성한 proto 파일을 통해 서버단인 자바와 파이썬에서 사용할 소스를 생성합니다.
각 언어별로 컴파일러가 존재하여 해당 컴파일러를 사용하여 소스를 생성하면 됩니다.
JAVA
자바에선 maven 기반으로 소스를 생성합니다.
proto 파일을 src/main/proto 폴더에 위치 해준 후에
grpc 용 dependency 를 추가하였고
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.18.0</version>
</dependency>
build 설정을 통해 compile 시점에 소스가 생성됩니다.
소스는 target 폴더 하위에 생성됩니다.
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.4.1.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

python
클라이언트 샘플로 사용할 파이썬에서도 소스 생성을 하기 위한 준비가 필요합니다.
python 3.5 이상 버전이 필요하며 pip 버전 9.0.1 이상이 필요합니다.
라이브러리 설치가 우선 진행된 후
$ python -m pip install grpcio
$ python -m pip install grpcio-tools
라이브러리 명령어를 통해 소스를 생성합니다.
$ python -m grpc_tools.protoc -I./ --python_out=. --grpc_python_out=. ../sample.proto

3. 비지니스 로직 구현
이제 통신을 위한 준비는 모두 완료되었으며 각 어플리케이션에서 비지니스 로직을 구현하기만 하면 됩니다.
클라이언트에서는 메소드를 호출해주기만 하면 되며, 서버는 생성된 서비스를 상속받아 로직를 구현하기만 하면 완료됩니다.
JAVA Server
빌드되면서 생성된 서비스 클래스를 상속받아 각 메소드를 오버라이드하여 구현하면 서버 로직은 완료된다.
package com.ks.grpc.demo;
import com.ks.grpc.MemberRequest;
import com.ks.grpc.MemberResponse;
import com.ks.grpc.RegisterMemberGrpc;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class MemberService extends RegisterMemberGrpc.RegisterMemberImplBase {
public static Logger logger = LoggerFactory.getLogger(MemberService.class);
//unary
@Override
public void register(MemberRequest request, StreamObserver<MemberResponse> responseObserver) {
logger.info("### unary Stream : {} ", request.toString());
MemberResponse response = MemberResponse.newBuilder()
.setMemberNo(1)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
//client stream
@Override
public StreamObserver<MemberRequest> registerClientStream(StreamObserver<MemberResponse> responseObserver) {
StreamObserver<MemberRequest> stream = new StreamObserver<MemberRequest>() {
@Override
public void onNext(MemberRequest memberRequest) {
logger.info("### Client Stream : {} ", memberRequest.toString());
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onCompleted() {
MemberResponse response = MemberResponse.newBuilder()
.setMemberNo(1)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
};
return stream;
}
//server stream
@Override
public void registerServerStream(MemberRequest request, StreamObserver<MemberResponse> responseObserver) {
logger.info("### Server Stream : {} ", request.toString());
MemberResponse response = MemberResponse.newBuilder()
.setMemberNo(1)
.build();
responseObserver.onNext(response);
responseObserver.onNext(response.toBuilder().setMemberNo(2).build());
responseObserver.onCompleted();
}
//bi stream
@Override
public StreamObserver<MemberRequest> registerBiStream(StreamObserver<MemberResponse> responseObserver) {
StreamObserver<MemberRequest> stream = new StreamObserver<MemberRequest>() {
@Override
public void onNext(MemberRequest memberRequest) {
logger.info("### Bi Stream : {} ", memberRequest.toString());
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onCompleted() {
MemberResponse response = MemberResponse.newBuilder()
.setMemberNo(1)
.build();
responseObserver.onNext(response);
responseObserver.onNext(response.toBuilder().setMemberNo(2).build());
responseObserver.onCompleted();
}
};
return stream;
}
}
그리고 gRPC에서 제공하는 서버 객체를 생성하여 port 와 service 를 등록하면 됩니다.
python client
python 에서는 channel을 생성 후 생성된 stub 소스를 통해 메소드를 호출해주기만 하면 요청이 완료됩니다.
import grpc
import sample_pb2
import sample_pb2_grpc
def run():
channel = grpc.insecure_channel('localhost:8877')
stub = sample_pb2_grpc.RegisterMemberStub(channel)
response = stub.Register(sample_pb2.MemberRequest(name="python",gender=0,phoneNo="01028406735"))
print("### Response : " + str(response.memberNo))
if __name__ == '__main__':
run()
마치며
이전 회사에서 프로젝트 진행시에 gRPC 를 적용해서 사용한 적이 있었습니다. 앱 클라이언트 - 서버 간 통신이었기에 앱 개발자와 서버 개발자 들이 동일한 proto 파일을 공유하여 사용하였었는데, 그때 당시에는 proto 파일 공유시 메일로 주고 받았던 기억이 납니다. 어느 한 파트에서 proto 적용을 누락...할 위험이 있었죠.
결국 proto 파일을 중앙에서 관리하여 변경된 부분이 누락되지 않도록 추가 작업은 필요하다고 생각합니다.
위와 같이 조사한 것을 팀원들과 공유 후 현재 서비스를 대체할 필요가 있을지 고민해본 결과, 네트워크 통신 비용이 줄어들며 각 모듈이 proto 파일을 통해 정의된 그대로 개발하면 되기 때문에 의사 소통 비용도 줄어들 수 있다고 생각하였습니다. 따라서 새로 구축하게 되는 신규 서비스에서는 활용을 해보면 좋을 것 같으나, 대부분 현재 제가 서비스하고 있는 어플리케이션이 이미 json-rpc 를 사용하고 있어 이를 gRPC 로 대체하는 것은 큰 메리트가 없다고 판단하였습니다.
MSA 환경을 구축하고 새로운 모듈을 개발하게 된다면 그때는 적용을 고민을 해보면 좋을 것 같습니다.
마지막으로 gRPC 만으로는 웹 브라우저 클라이언트에서는 사용을 할 수 없기 때문에 gRPC-web 이라는 추가 프로젝트가 진행된다고 합니다.
JavaScript 에서 사용할 수 있다고 하며 아직은 Unary와 Server-side streaming 방식만 지원이 된다고 하네요.
https://github.com/grpc/grpc-web
참고
작성자
-
이광신 NHN Edu / 아이엠클래스팀
NHN EDU 에서 학원 관리 솔루션인 아이엠클래스 및 평가센터 서버 개발을 담당하고 있습니다.
-
[출처] https://meetup.toast.com/posts/261
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.


