본문 바로가기
Framework and Tool/gRPC and Protobuf

Protocol buffer - Proto3

by ocwokocw 2022. 4. 24.

- 출처: https://developers.google.com/protocol-buffers/docs/proto3

- Message Type 정의

아래는 .proto 파일에 메시지를 정의한 예제이다.
 
syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
 
  • 맨 첫줄 syntax = "proto3";은 proto3 문법을 사용하겠다는것을 나타낸다. 만약 이 선언이 없다면 proto2를 사용한다고 가정한다.
  • 위의 .proto 파일에는 SearchRequest 메시지 안에 3개의 field가 존재한다. 그리고 각 field는 이름과 형을 갖는다. 예제에서는 scala type들만 사용했지만(int32와 string) 열거형이나 다른 메시지 타입도 정의 가능하다.
 
각 field들에 숫자가 할당되어 있는데 이 field 번호는 이진 형식의 필드를 식별하는데 사용되며 메시지 유형이 사용중이면 변경해서는 안 된다. 1~15 의 field 숫자는 해당 숫자와 형을 포함해서 1 바이트를 차지한다. 16~2047은 2 바이트를 차지한다. 그래서 1~15 값은 매우 자주 발생하는 메시지를 위해 남겨놓는게 좋다. 총 범위는 1 ~ 2^29-1 이며 19000 ~ 19999는 사용할 수 없다. 
 
또한 아래와 같이 Field Rule도 지정할 수 있다.
 
  • singular: 0개 또는 1개를 나타내며 proto3 문법의 default field rule이다.
  • repeated: 0개 이상의 반복을 나타내며 순서가 유지된다.
 
하나의 .proto 파일에는 여러 개의 메시지를 정의할 수 있다. 만약 SearchRequest에 대한 응답 메시지도 정의하고 싶다면 아래와 같이 선언하면 된다.
 
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

message SearchResponse {
 ...
}
 
그리고 당연히 comment도 지원한다. //와 /* ... */ 문법을 지원한다.
 
/* SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response. */

message SearchRequest {
  string query = 1;
  int32 page_number = 2;  // Which page number do we want?
  int32 result_per_page = 3;  // Number of results to return per page.
}
 
만약 어떤 field를 삭제하거나 comment 처리하면 나중에 사용자가 해당 field를 재사용할 위험성이 있다. 그때 이전 버전의 같은 .proto 를 로드하면 버그가 발생한다. 이를 방지하기 위해서 삭제된 field 들의 번호를 reserved 키워드로 지정하면 된다.
 
message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}
 

 

- Scalar Value Types

message 에서 정의한 field type이 string이나 int32와 같이 우리에게 익숙하긴 하지만 어쨌든 .proto 파일을 이용해서 코드를 생성하면 각 언어에 맞는 형으로 생성되기 마련이다. .proto의 메시지 field에서 정의한 형이 내가 사용하는 언어에서 정확히 어떤 형으로 변환되는지 참조하려면 https://developers.google.com/protocol-buffers/docs/proto3#scalar 를 참조하자.

- Default 값들

메시지가 파싱될 때 encode된 메시지가 특정 단일 요소를 포함하고 있지 않으면 기본값으로 세팅된다.
 
  • string은 빈 string 값을 갖는다.
  • byte는 빈 byte 값을 갖는다.
  • bool은 false 이다.
  • numeric은 0이다.
  • enum은 처음 정의된 열거형 값이다.
  • message 는 설정되지 않으며 정확한 값은 언어적 특성에 종속된다.
  • repeated 는 비어 있다. 

- 열거형

message내의 어떤 field가 정의된 값중 하나를 갖도록 제한하고 싶을 때에는 enum을 사용한다. 
 
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}
 
위의 예제에서 Corpus enum의 첫번째 상수는 0으로 설정되어 있다. 모든 enum 정의는 반드시 처음 요소로 0에 맵핑되는 상수를 가져야 하는데 그 이유는 아래와 같다.
 
  • 첫번째 요소가 0값이어야 numeric 기본값으로 0을 사용할 수 있다.
  • 첫번째 enum 값이 언제나 기본값인 proto2 와의 의미 호환성을 위해 0값이 첫번째 요소가 되어야 한다.

- 다른 Message 참조

프로그래밍 언어에서 하나의 자료형에서 다른 자료형을 참조할 수 있듯이 proto에서도 다른 message를 field로 참조할 수 있다. 만약 SearchResponse message에서 Result message를 참조하고 싶다면 아래와 같이 하면 된다.
 
message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}
 

-  중첩 Type

message type 안에 다른 message type을 중첩으로 사용할 수 있다. 
 
message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}
 
만약 외부의 message에서 Result를 재사용하고 싶다면 아래와 같이 사용해야 한다.
 
message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

- Message Type 수정

message type을 갱신할 때 기존의 message 타입의 동작을 보장하면서 수정해야 하는 경우가 있다. 그럴때에는 아래와 같은 규칙을 명심하면서 작업하도록 하자.
 
  • 기존에 존재하던 field number를 변경하지 말것
  • 새로운 field를 추가한 후 .proto에서 생성된 코드를 통해 message를 파싱할 때 기존의 message도 파싱된다.
  • field number가 수정된 message 형에서 다시 사용되지 않으면 field를 삭제할 수 있다. 대신 field 명을 OBSOLETE_와 같은 접두사를 붙여줘서 이름을 변경해주거나 해당 field number를 reserved로 만들어주도록 하자.
  • int32, uint32, int64, uint64, bool은 모두 호환된다.
  • sint32, sint64는 호환되지만 다른 integer 형들과는 호환되지 않는다.
  • string과 bytes는 호환된다 단, bytes가 유효한 UTF-8이어야 한다.
  • Embedded message들은 bytes와 호환된다.
  • 단일 값을 새로운 oneof의 멤버로 변환하는것은 안전하며 binary 호환된다. multiple field를 새로운 oneof로 옮기는 것은 코드가 한번에 둘 이상을 설정하지 않을 경우 안전하다. any field를 이미 존재하는 oneof로 옮기는것은 안전하지 않다.
  • 그 외의 추가 정보는 https://developers.google.com/protocol-buffers/docs/proto3#updating 여기서 참조 바람.

- Any

Any message 형은 .proto 에서 별도 정의 없이 embedded 형으로 message를 사용할 수 있도록 해준다. Any는 bytes로 직렬화된 임의의 메시지를 포함하며 해당 메시지내에는 전역적으로 고유하게 식별하는 URL 정보도 포함되어 있다. Any 형을 사용하기 위해서는 google/protobuf/any.proto 를 import 해야 한다.
 
import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}
 
message 형에 주어지는 기본 URL은 https://type.googleapis.com/_packagename_._messagename_ 이다.

- Oneof

만약 message에 여러 개의 field가 있고 그 중에서 최대 1개의 field만 사용되어야 한다면 oneof를 이용해서 이 기능을 구현하여 메모리를 절약할 수 있다.
 
Oneof field는 oneof 공유 메모리의 모든 field를 제외하고 일반 field와 같으며 최대 하나의 field를 동시에 설정할 수 있다. oneof 의 어떤 member라도 세팅되면 나머지 member들을 제외한다. oneof의 어떤 값이 세팅되었는지를 case()나 WhichOneof() 메소드를 사용해서 확인할 수 있으며 선택하는 언어에 따라 다르다.
 
.proto 에서 oneof를 정의하기 위해서는 아래와 같이 사용하면 된다.
 
message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}
 
oneof 정의에 oneof field를 더해주면 된다. map과 repeated field를 제외한 나머지 field는 모두 사용할 수 있다.
 
생성된 코드에서 oneof field는 일반적인 field 처럼 getter/setter를 동일하게 갖는다. 또한 어떤 값이 사용되었는지 확인할 수 있는 특별한 method도 생성된다. 
 
oneof는 몇 가지 특징을 갖는데, 아래 특징을 숙지한 후 사용하도록 하자.
 
  • oneof field를 설정하면 oneof의 나머지 member들은 자동으로 clear 된다. 만약 oneof field들을 여러 번 설정하면 맨 마지막 field만 값을 갖는다.
 
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());
 
  • 파서가 같은 oneof의 member를 여러 번 만나면 마지막 member만 파싱한다.
  • oneof는 repeated가 될 수 없다.
 
하위 호환성 issue와 관련해서 oneof field를 추가하거나 삭제할 때에는 조심해야 한다. oneof 확인 값이 None/NON_SET을 반환했다면 oneof가 셋팅되지 않았거나 oneof의 다른 version의 field가 설정된것이다.  
 
field를 oneof 안으로 혹은 밖으로 옮기면 message가 직렬화되고 파싱된 후에 일부 정보를 잃을 수 있다. 하지만 하나의 field를 oneof의 새로운 field로 옮기는것은 안전하며 오직 하나만 설정된다는 보장이 있으면 여러 개의 field를 옮겨도 된다.

- Packages

package 를 사용하면 protocol message type들 간의 name 충돌을 방지할 수 있다.
 
package foo.bar;
message Open { ... }
 
위의 예제에서 Open을 message의 field로 사용하고 싶다면 아래와 같이 사용하면 된다.
 
message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}
 
package는 지정자는 언어에 따라 생성되는 코드에 영향을 줄 수 있다. 여러 언어가 설명되어 있지만 나는 go를 사용하고 있기 때문에 go 부분만 기술한다. Go에서는 .proto 파일에서 명시적으로 option go_package 를 설정하지 않으면 Go package name으로 package를 사용한다.

- 그 외

Project를 진행하다가 proto3에 대해 알아야 할 필요가 있고 관련된 부분만 정리했지만 기술하지 않은 다른 특징들도 많다. UnKnown Fields, Maps, Service, JSON Mapping, Options 등도 있으니 각자 필요한 부분 참조하자.
 
 

'Framework and Tool > gRPC and Protobuf' 카테고리의 다른 글

Protocol buffer - Convention  (0) 2022.04.29
Protocol buffer  (0) 2022.03.13
gRPC with protobuf  (0) 2022.03.13
RPC(Remote procedure call)  (0) 2022.03.13

댓글