gRPC with BSR

2022. 10. 6.

BE 이지훈

What is RPC(Remote Procedure call)?

RPC는 위키피디아의 설명을 빌리면 다음과 같이 설명할 수 있다.

별도의 원격 제어를 위한 코딩 없이 다른 주소공간에서 함수나 프로시저를 실행할 있게 해주는 프로세스간 통신 기술이다. 
다시 말해, 원격 프로시저 호출을 이용하면 프로그래머는 함수가 실행 프로그램에 로컬 위치에 있든 원격 위치에 있든 동일한 코드를 이용할 있다


위는 client가 server의 GetUser라는 서버의 로직을 호출하는 rpc 단계를 나타낸 다이어그램이다.


What is gRPC?

gRPC란 위에서 설명한 RPC 앞에 g가 붙었다. 여러분이 생각하는 그게 맞다. 바로 Google이다.
google Remote Procedure call 이 바로 gRPC이다.

구글에서 개발한 어디에서나 동작하는 고성능 RPC 프레임워크 오픈소스이다.


gRPC에서 클라이언트 애플리케이션은 다른 시스템의 서버 애플리케이션에 있는 메서드를 local 개체인 것 처럼 직접 호출할 수 있으므로 분산 애플리케이션과 서비스를 쉽게 만들 수 있다.

많은 RPC 시스템에서와 마찬가지로 gRPC는 서비스를 정의하고, 매개 변수와 반환 유형을 사용하여 원격으로 호출할 수 있는 메서드를 지정하는 아이디어를 기반으로 한다.

서버는 해당 인터페이스를 구현하고 gRPC 서버를 실행하여 클라이언트 호출을 처리한다.

클라이언트는 서버와 동일한 방법을 제공하는 stub을 가진다.


Supported languages

현재 gRPC 는 아래와 같은 언어들을 지원하고 있다.


REST vs gRPC

gRPC가 HTTP/2 와 후술 할 ProtoBuf를 사용하는 등 여러 이유로 인해 REST 대비 좋은 performance를 가지고 있다.


What is ProtoBuf(Protocol Buffer)?

  • google에서 개발한 구조화된 데이터를 Serialization 하는 기법이다.

  • 데이터 유형, 데이터 타입 등을 1byte로 식별하고, 주어진 length 만큼만 읽어서 용량이 작다.


Proto File

.proto 확장자를 갖는 ProtoBuf의 기본 정보를 명세하는 파일이다.


1. Message

syntax = "proto3"; // proto buf version 
message MessageName { 
  string name = 1; 
  int32 id = 2; 
  optional bool do_you_wanna_build_snowman = 3; 
  repeated int32 snowman_made_date = 4; 
}
  • message 이름은 CamelCase, field 이름은 snake 형태로 사용하는것을 권장.

  • field 이름을 숫자로 시작할 수 없다. ( 1_name → name_1)

  • field 는 고유한 번호를 가지게 되는데, 1 ~ 536,870,911 까지 사용 가능하다(19000 ~ 19999는 reversed된 값으로 사용불가)

  • requiredoptionalrepeated 옵션을 사용할 수 있다.

    • required - 반드시 하나의 필드를 가져야한다. (proto3 부터 사용 x)

    • optional - 없거나 하나만 가져야 하는 필드.

    • repeated - 반복적으로 여러번 사용될 수 있다. 순서는 보존된다.

    • map - key/value 필드 타입. See Maps.

    • oneof - 아래 proto와 같이 정의하면. 아래 go 코드 예시처럼 generate 된다. (당연하지만 언어마다 생성되는 코드는 다르다.)

package account; 
message Profile {   
  oneof avatar {     
    string image_url = 1;     
    bytes image_data = 2;   
  } 
}
type Profile struct {         
// Types that are valid to be assigned to Avatar:         
//      *Profile_ImageUrl         
//      *Profile_ImageData         
Avatar isProfile_Avatar `protobuf_oneof:"avatar"` 
} 

type Profile_ImageUrl struct {         
  ImageUrl string 
} 

type Profile_ImageData struct {         
  ImageData []byte


2. Package

package seoul.company; 

message TestBank { 
  // ... 
} 

message Resume { 
  seoul.company.Testbank testbank_1 = 1; // possible 
  Testbank testbank_2 = 2 // possible 
}
  • pakage는 message 타입 이름을 중첩없이 구분할 때 사용한다.


3. Service

service HelloWorld { 
  rpc Hello (HelloRequest) returns (HelloResponse) {}; 
  rpc HelloClientStream (stream HelloClientStreamRequest) returns (HelloClientStreamResponse) {}; 
  rpc HelloServerStream (HelloServerStreamRequest) returns (stream HelloServerStreamResponse) {}; 
  rpc HelloBiStream (stream HelloBiStreamRequest) returns (stream HelloBiStreamResponse) {}; 
}
  • RPC를 통해 Server가 Client에게 제공할 함수의 형태를 정의한다.

  • CamelCase를 권장한다.

  • 기본적으로 단일 요청/응답으로 동작하지만, stream 옵션을 사용해서 RPC를 구현할 수 있다.


What is BSR?

BSR(Buf Schema Registry) 은 Protobuf 파일들을 버전된 모듈로 저장하고 관리해주는 저장소이다.

해당 저장소를 이용하면 개인이나 기관이 API들을 마찰없이 사용하고 배포할 수 있다.


BSR은 탐색가능한 UI와 의존성 관리, API 검증과 버저닝, 문서 생성 그리고 원격 코드 제너테이터를 통해 확장 가능한 플러그인 시스템을 제공한다.

❗ 22/09/26 일 기준 베타 버전으로 무료이나 베타버전 이후에 team으로 사용하려면 유료(10달러)로 전환 될 예정이다.

❗ gRPC, protoBuf 를 사용하기 위해 해당 서비스를 반드시 이용할 필요는 없다.


Install

$ brew install bufbuild/buf/buf


Login

$ buf

위 명령어를 입력하고, 유저네임, 토큰을 입력하면 로그인 된다.


정상적으로 로그인이 완료되면 ~/.netrc 파일에 아래와 같이 저장된다.

machine buf.build 
    login <USERNAME> 
    password <TOKEN> 
machine go.buf.build 
    login <USERNAME> 
    password <TOKEN

로그인이 완료되면 BSR의 접근 권한 설정이 끝났다.
이제 repository를 생성하고 module을 push 할 수 있다.


How to use

시작하기 앞서서 기본적인 용어들을 확인해보자.

  • Modules

Modules 은 Buf와 BSR의 핵심이다. module 은 구성되고, 빌드되고, 버전이 명시된 논리단위의 Protobuf의 집합이다. module은 buf.yaml 을 초기화할 때 생성한다.

  • Repositories

module는 repository 에 저장된다. 레포지토리는 모듈의 모든 버전을 저장한다. 각 버전은 커밋이나 선택적으로 태그에 의해서 식별된다.
Git 저장소와 대략 비슷하지만 BSR 저장소는 원격 위치일 뿐이며 저장소 "클론"이라는 개념은 없다. 즉, 리포지토리는 여러 위치에 존재하지 않는다.

  • Module names

module은 이름과 3개의 다른 컴포넌트를 갖는다.

  • Remote: BSR에 호스팅 하기위한 DNS이름이다. [buf.build](<http://buf.build>) 와 같다.

  • Owner: 개인이나 기관과 같은 repo의 주인이다.

  • Repository: repo의 이름이다.


예시,


Create Repository

command line 에서 레포를 생성하는 방법과 buf.build 콘솔에서 Create repository 를 클릭해서 생성하는 방법이 있다.


  • command line

buf beta registry repository create buf.build/$BUF_USER/$REPO_NAME --visibility public


  • buf.build console (home → 우측 상단 name 클릭 → Repositories → Create repository)


Configure

buf 는 buf.yaml 과 함께 구성된다. 아래 커맨드로 생성할 수 있다.

$ buf mod init


Set name

buf.yaml 파일에서 name을 매칭해 주면 된다.

version: v1 
name: <Remote>/<Owner>/<Repository> 
  lint: 
    use: 
      - DEFAULT 
  breaking: 
    use


Push the module

buf.yaml 이 있는 폴더에서 아래 커맨드를 수행하면 된다.

$ buf push

해당 커맨드를 수행하면 Buf Registry에 code가 push 된다.


Write Configuration

buf.yaml

buf.yaml 파일은 모듈을 정의하고 Protobuf 파일의 루트에 위치한다.

buf.yaml 의 구성은 buf에게 .proto 파일은 어디에 있고 lint, breaking 등 옵션을 설정할 수 있다.


buf.lock

buf mod update 커멘드를 통해 생성할 수 있다. 자동으로 생성되는 부분이니 수정하지 말자.


buf.gen.yaml

buf.gen.yaml 파일은 buf generate 커맨드가 protoc 플러그인들을 어떤식으로 실행하는지에 대한 통제이다.

version: v1 
plugins: 
  - name: cpp 
    out: gen/proto/cpp 
  - name: java 
    out: gen/proto/java 
  - name: go 
    out: gen/proto/go 
    opt: paths=source_relative 
  - name: go-grpc 
    out: gen/proto/go 
    opt


buf.work.yaml

buf.work.yaml은 workspace를 정의할 때 사용한다. 하나 이상의 모듈이 공통된 리텍토리에 존재할 때 local 모듈이 다른 local모듈을 import 할 수 있게 해준다.

. 
├── buf.work.yaml 
├── testbank-rpc 
├── testbank 
└── contents 
└── v1 
└── user.proto 
└── buf.yaml 
└── solve-rpc 
    ├── solve 
    └── solve
    └── v1 
    └── problem.proto 
    └── buf.yaml


예를 들어 위와 같은 디렉토리 구조를 갖고 있을 때,

# buf.work.yaml 
version: v1 
directories: 
  - testbank-rpc 
  - solve-rpc

위와 같이 buf.work.yaml을 작성하면 testbank-rpc에서 solve-rpc, 혹은 그 반대, 를 import 해서 사용할 수 있다.


breaking changes

version: v1 
lint: 
  use: 
    - DEFAULT
breaking: 
  use

  • Option : FILE, PACKAGE, WIRE, WIRE JSON


default 값은 FILE 이다. API의 사용자 간에 최대한의 호환성을 보장할 것을 권장한다.

일반적으로 lint구성을 지정할 때 처럼 특정 변경 규칙을 포함/제외하기보다는 이러한 옵션 중 하나만 선택하는 것이 좋다.


// pet/v1/pet.proto 
message Pet { 
- PetType pet_type = 1; 
+ string pet_tyep = 1; 
  string pet_id = 2; 
  string name = 3; 
}

만약 위의 예시처럼 Pet.pet_type field를 PetType 에서 string 으로 변경하고 나서,

buf breaking 명령어를 수행해 보면 변경된 부분의 에러가 발생한다.


$ buf breaking --against "../../.git#branch=main,subdir=start/petapis" 
  
pet/v1/pet.proto:20:3:Field "1" on message "Pet" changed type from "enum" to "string"


lint

buf lint 를 사용하면 API 정의에 가장 좋은 사례를 결정하게 해서 일관성을 강제하고 지켜준다.

$ buf lint --error-format=json 

{"path":"google/type/datetime.proto","start_line":17,"start_column":1,"end_line":17,"end_column":21,"type":"PACKAGE_VERSION_SUFFIX","message":"Package name \\"google.type\\" should be suffixed with a correctly formed version, such as \\"google.type.v1\\"."} 
{"path":"pet/v1/pet.proto","start_line":44,"start_column":10,"end_line":44,"end_column":15,"type":"FIELD_LOWER_SNAKE_CASE","message":"Field name \\"petID\\" should be lower_snake_case, such as \\"pet_id\\"."} 
{"path":"pet/v1/pet.proto","start_line":49,"start_column":9,"end_line":49,"end_column":17,"type":"SERVICE_SUFFIX","message":"Service name \\"PetStore\\" should be suffixed with \\"Service\\"."}


buf.yaml 파일에서 ignore 옵션으로 특정 파일을 lint에서 무시할 수 있다.

version: v1 
  lint: 
    use: 
      - DEFAULT 
    ignore: 
      - google/type/datetime.proto
  breaking: 
    use: 
      - FILE


Using module

아래와 같이 각 언어에서 원하는 템플릿으로 push된 모듈을 모듈을 받아서 사용할 수 있다.

  • go

$ go get go.buf.build/${template_owner}/${template_name}/${owner}/${repository
  • js

$ npm config set @buf:registry <https://npm.buf.build> 
$ npm install @buf/${template_owner}_${template_name}_${owner}_${repository}


혹은, buf.gen.yaml 파일을 작성했다면 buf generate 를 통해서 로컬에서 생성할 수 있다.

$ buf generate

위 커맨드를 실행하면 buf.gen.yaml 의 out으로 지정한 폴더에 생성된다.


End

현재 테스트뱅크는 통신에서 gRPC를 사용하고 해당 ProtoBuf를 BSR을 사용해서 관리하고 있다.

gRPC를 사용하기 위해 작성하는 ProtoBuf에 따른 큰 이점이 있다.

  1. ProtoBuf를 이용하여 사용하는 언어에 맞는 코드를 generate해서 사용할 수 있다.

  2. ProtoBuf 자체로 API명세서 기능을 하기 때문에 따로 작성할 필요가 없다.


처음 기능을 설계할 때 ProtoBuf 작성에 신경을 많이 쓰면 이후에 개발이 편해진다.

BSR의 사용은 위에서 언급한 것 처럼 필수가 아니지만, 해당 저장소를 사용하면 commit과 tag 등으로 ProtoBuf를 관리하기 수월하고 module을 쉽게 내려받아서 사용할 수 있다.