본문으로 건너뛰기

NodeJS gRPC protoc plugins 비교

· 약 12분

Background

회사에서 gRPC를 통한 통신을 하던 와중 NodeJS에서도 이를 지원해야 할 일이 생겼다. NodeJS에서도 역시 gRPC를 사용하기 위해서는 proto 파일을 컴파일하여 JS 파일로 변환해야 하는데, 이를 위해 protoc를 사용한다. 하지만 typescript를 사용하고 있기 때문에, protoc에서 생성하는 JS 파일을 그대로 사용하기에는 불편함이 있었고, protoc에서 생성하는 JS 파일을 typescript로 변환해주는 라이브러리들을 찾아보기로 했다. (공식 지원은 아직 없다.)

목표

*.proto 파일을 통해서 nodejs 환경에서 사용가능한 클라이언트 코드를 자동 생성하는 것

#1 Proto file example
#1 Proto file example

얘가

#2 proto가 *.ts로 변환된 모습
#2 proto가 *.ts로 변환된 모습

얘로 변하고 그걸 잘 말아서 Frontend server(node.js)에 serving 해줘야 한다.

과정

proto 파일을 바탕으로 각 언어 별로 사용 가능한 구현체를 만들기 위해서는 protoc(protocol buffer compiler)이 필요함.

문제는 protoc로 nodejs 관련 코드를 만들어 내는 예제나 문서가 거의 찾아보기 어렵다는 점이다. 특히나 typescript로 type definition까지 만들기는 어렵다.

일단 공식으로 protoc에서 typescript를 만드는 걸 지원하지는 않는다.

대신 plugin을 통해서 지원하기는 하는데, 현재 이를 지원하는 라이브러리는 메이저하게 3가지 종류가 있다.

(공식은 없고 모두 커뮤니티에서 만든 것)

#3 typescript 생성을 도와주는 protoc plugin 3종 비교
#3 typescript 생성을 도와주는 protoc plugin 3종 비교

각 라이브러리 별로 수행하는 방식은 다음과 같다.

ts-protoc-gen

명령어

# -------------
# ts-protoc-gen
# -------------
# Path to this plugin, Note this must be an abolsute path on Windows (see #15)
PROTOC_GEN_TS_PATH="./node_modules/.bin/protoc-gen-ts"
# Path to the grpc_node_plugin
PROTOC_GEN_GRPC_PATH="./node_modules/.bin/grpc_tools_node_protoc_plugin"
# Directory to write generated code to (.js and .d.ts files)
OUT_DIR="./output"

protoc \
--plugin="protoc-gen-ts=${PROTOC_GEN_TS_PATH}" \
--plugin="protoc-gen-grpc=${PROTOC_GEN_GRPC_PATH}" \
--js_out="import_style=commonjs,binary:${OUT_DIR}" \
--ts_out="service=grpc-node,mode=grpc-js:${OUT_DIR}" \
--grpc_out="grpc_js:${OUT_DIR}" \
./src/proto/author.proto # target proto files
#4 ts-protoc-gen 결과물
#4 ts-protoc-gen 결과물

해석은 다음과 같다.

  • proto file로부터 protoc-gen-js (내장, protoc 공식)을 사용해서 js_out 옵션에 따라서 실제 실행가능한 js 파일을 생성한다.
    • 파일은 보통 {service_name_prefix}_pb.js 로 생성된다.
    • 이 파일은 request / response serialize, deserialize, binary, 등을 수행한다.
  • proto file로부터 ts-protoc-gen 플러그인이 google-protobuf 및 grpc-js 에 맞는 interface (type definition) 파일을 생성한다.
    • google-protobuf를 사용하는 구현체 파일을 위한 인터페이스 파일을 만든다. 이는 보통 {service_name_prefix}_pb.d.ts 로 생성된다. 이 파일이 위에서 만든 js 파일의 type definition이 된다.
    • grpc 클라이언트 코드 구현체를 위한 인터페이스 파일을 만든다. 이는 보통 {service_name_prefix}_grpc_pb.d.ts 로 생성된다.
  • proto file로부터 grpc_tools_node_protoc_plugin 을 사용해서 grpc 클라이언트 코드 구현체 파일을 생성한다.
    • {service_name_prefix}_grpc_pb.js 로 생성된다.

사용방법

const authorServiceClient = new AuthorServiceClient('fake-endpoint.com:50052', credentials.createInsecure());

const authorId = req.query.id as string;

/*
@grpc 자체가 callback으로의 구현체만 지원하고 promise를 지원하지 않습니다.
node의 내장 promisify 등을 사용해서 이를 깔끔하게 바꾸기도 합니다만,
지금은 그냥 구현만이 목적이므로 promise를 만들어서 사용합니다.

해당 내용은 예전부터 꾸준히 요청이 들어가고 있는 것 같은데 구글 측에서 답변이 없는 듯 합니다.
*/
const author = await new Promise((resolve, reject) => {
// request parameter를 생성합니다.
// Java like한 방식이라고 생각했습니다만, 뭐 여러가지를 할 수 있어서 선택지는 많은 것 같습니다.
const authorRequest = new GetAuthorRequest()
// .setAuthorName(authorName)
// ...
.setAuthorId(authorId);

// 실제 요청을 날립니다.
authorServiceClient.getAuthor(authorRequest, (err, author) => {
if (err) {
reject(err);
} else {
// author가 author 객체가 아니라 response 객체가 오기 때문에
// toObject를 통해서 JSON like한 객체로 바꾸어줍니다.
// response 객체에는 각 field에 대한 getter/setter 및 clear 같은 메서드들이
// 존재하고 있습니다.
resolve(author.toObject());
}
});
});

console.log(author);

장, 단점

장점

  • 실행되는 js 구현체의 경우 어쨌든 google 공식 protoc에서 지원하는 기능이다. 앞으로도 믿고 사용하는 것이 가능하다.
  • Request, Response 객체 및 실제 google-protobuf 기능까지 다 사용할 수 있으므로 자유도 및 확장성이 뛰어나다.

단점

  • 초기 세팅이 많이 까다롭다.
    • protoc에서 protoc-js가 따로 빠져나와서 해당하는 내용을 찾기 어렵다. 나중에 ci에서 빌드 세팅할 때 따로 바이너리 파일을 불러와서 PATH부터 이상한 세팅들을 좀 해줘야 할 듯 싶다. (권한 문제도 있고)
    • 문서 지원이 아쉽다.
  • request, response가 모두 클래스로 이루어져 있다는 점. 주어지는 메서드와 자유도는 높아지지만 그 기능들을 다 사용할까 의문이며, 그 확장성을 위해서 request를 만들어서 set 을 하나씩 해주는 게 앞으로 너무 귀찮을듯.
  • grpc client (공식)을 제외한 다른 클라이언트를 지원하는지 의문이다.
  • gprc, ts 모두 공식 지원이 아니다. 다른 건 아예 공식지원이 아니므로 이게 단점이 될까 싶지만…
  • 마지막 업데이트로부터 벌써 1년이 넘었다.

ts-proto

명령어

# --------
# ts-proto
# --------

protoc \
--plugin="./node_modules/.bin/protoc-gen-ts_proto" \
--ts_proto_out=. \
--ts_proto_opt="outputServices=grpc-js,env=node,useOptionals=messages,exportCommonSymbols=false,esModuleInterop=true" \
./src/proto/author.proto # target proto files
#5 ts-proto 결과물
#5 ts-proto 결과물

명령어는 간단하게 proto파일들을 ts-proto 플러그인을 ts_proto_opt 에 있는 option들을 사용해서 —ts_proto_out에 실행가능한 파일로 컴파일하라는 것이다.

ts-protoc-gen 과는 다르게 구현체 js에 ts definition 파일을 생성하는 것이 아니라 native ts 파일을 생성한다.

사용방법

import { AuthorServiceClient } from '@src/proto/author';

const authorServiceClient = new AuthorServiceClient('fake-endpoint.com:50052', credentials.createInsecure());

const authorId = req.query.id as string;

// 여기도 @grpc 구현체의 한계로 callback에 promise를 감싸서 사용했습니다.
const author = await new Promise((resolve, reject) => {
/*
request class를 만드는 게 아니라 바로 넣을 수 있습니다.
type definition이 되어있어서 자동완성 및 필요한 파라미터에 대해 힌트를 얻을 수 있습니다.

interface GetAuthorRequest {
authorId: string;
}
*/
authorServiceClient.getAuthor({ authorId }, (err, author) => {
if (err) {
reject(err);
} else {
/*
response 역시 바로 사용할 수 있는 객체로 type definition이 되어 들어옵니다.

interface Author {
authorId: string;
name: string;
paperCount: number;
citationCount: number;
createdDate?: Date;
hindex: number;
papers: Author_AuthorPaper[];
affiliation?: Author_AuthorAffiliation | undefined;
topics: Author_AuthorTopic[];
coauthors: AuthorCoauthor[];
}
*/
resolve(author);
}
});
});

console.log(author);

장점

  • 다양한 구현체를 사용할 수 있다.
    • Twirp, grpc-web, grpc-js, nestjs, nice-grpc, starpc
    • typescript first로 파일 떨어지는 것 및 이후 SDK화나 컴파일이 깔끔하다.
    • nice-grpc나 starpc처럼 ts-proto를 기반으로 만들어진 mini framework가 존재한다.
    • 이 경우 @grpc에서 답답했던 지원들 (promise가 지원이 되지 않는다거나 하는) 을 개선한 경우가 대부분인듯.
    • 아직 리서치가 덜 되서 다음 리서치 및 문서에서 포함할 예정
  • 잦은 release, issue, PR이 maintain 되고 있는 라이브러리 라는 느낌을 많이 준다.
  • 설치가 그나마 쉽다.
    • protoc 만 빌드 머신에 설치하면 나머지는 npm install 한번에 끝난다.

단점

  • protoc 에서 공식으로 지원하는 라이브러리는 아니다.
  • promise 패턴이나 iterable 패턴을 편하게 사용하려면 rpc client도 nice-grpc나 starpc 같은 서드 파티 툴을 사용해야 한다.

protoc-gen-ts

명령어

protoc \
-I="proto" \
--ts_out="proto/typescript" \
--ts_opt="unary_rpc_promise=true" \ # 이 옵션이 promise로 만드는 옵션
proto/author.proto # target proto files
#6 protoc-gen-ts 결과물
#6 protoc-gen-ts 결과물

사용방법

const authorServiceClient = new AuthorServiceClient('fake-endpoint.com:50052', credentials.createInsecure());
// getter setter 패턴 및 fromObject를 통해서 Request 생성 가능
const authorRequestMessage = GetAuthorRequest.fromObject({ author_id: authorId });

const metadata = new Metadata();

// promise를 기본으로 리턴해주게끔 설정 가능함
// 편함
const author = await authorServiceClient.GetAuthor(authorRequestMessage, metadata);

// 떨어지는 건 Response 구현체 이기 때문에 toObject로 변환해야 res로 보낼 수가 있음
const authorObject = author.toObject();

장점

  • ts-proto와 달리 callback이 아니라 promise based interface를 제공함
  • ts native 구현체를 제공함
  • enum이 string이 아니라 실제 enum으로 컴파일
  • 실제 @grpc/js를 잘 말아서 옵션 하나로 promise 패턴을 사용가능하다.
  • request class 만드는 인터페이스도 유연한 것 같음.

단점