Scatch note
Protocol Buffer 3 Spec정리
02.01.20232 Min Read — In tech

프로토콜 버퍼(Protocol Buffer)는 gRPC에서 데이터의 스키마를 정의하고 직렬화할때 사용하는 메커니즘입니다. google에서 개발했으며, *.proto형식의 메시지 뿐만 아니라, JSON등의 데이터를 직렬화할때도 사용되는 방식이고, gRPC에서 직렬화 방식으로 사용되고 있습니다.

프로토콜 버퍼가 gRPC에서 어떻게 동작하는지는 링크:이전 글을 참조해주세요

이 글에서는 Protocol Buffer3(Proto3)에서 스키마를 정의하는 방법들 중, 기본적인 내용에 대해 다룹니다.

메시지를 정의하는 방법

syntax= "proto3";
message SearchRequest{
	string query=1;
	int32 page_number=2;
	int32 result_per_page=3;
}

/*
C/C++ Style Comment
*/
//C/C++ Style Comment

위의 프로토콜 버퍼는 아래와 같은 특징을 가지고 있습니다.

  • 확장자: .proto 입니다.
  • 버전 명시: 주석, 빈 라인을 제외한 첫 번째 라인에 Proto3 임을 명시해주어야 합니다. (아니면 기본값 proto2로 지정)
  • 필드 정의 : string,int32/64와 같은 타입과 , query, page_number같은 이름의 쌍으로 필드를 정의합니다. (참고로, 필드명은 snake case, 메시지명은 cammel case를 권장하고있습니다)
  • 주석 : C/C++스타일 주석으로, “//” 와 “/**/” 를 사용할 수 잇습니다.
  • 필드번호 정의: 필드번호는 메시지 내 유일한 숫자로 지정해줍니다.
    • 필드번호를 통해 이진 포멧에서 필드를 식별할 수 있고, 메시지 유형이 사용된경우 변경하면 안됩니다.
    • 1-15 까지의 번호는 1바이트만 사용하지만, 16~2047은 2바이트, 번호가 커질수록 사용하는 바이트 수가 늘어납니다. 또한, 19000 ~19999까지는 예약된 필드번호로, 사용이 불가능합니다.

필드 규칙

메시지의 필드는 아래 4개중 하나의 타입에 해당합니다.

Proto3에서는 field presence라는 개념이 존재합니다. 자세한 내용은 링크(https://protobuf.dev/programming-guides/field_presence/)를 참고하시면 되고, 필드 규칙을 이해하기 위해서는 [ explicit, no ] present의 개념에 대해서만 이해하면 됩니다.

explicit present: 명시적으로 값이 존재하는지를 저장하는 값 체계

no present: 값이 존재하는지 아닌지를 노출하지 않고, 기본값을 통해 대치하는 방식

https://protobuf.dev/programming-guides/field_presence/

  • singular: proto3에서 규칙을 지정하지 않으면 기본값으로 사용됩니다. no-present방식의 값 체계를 따릅니다.
  • optional: proto2에서 기본으로 사용되었으며, 직렬화 시 explicit present방식을 사용합니다.
  • repeated : 반복되는 값에 대해 사용합니다. key-value로 매핑되지만, packed옵션을 통해 값만 반복하도록 설정할 수 있습니다.
  • map: key-value형태의 값에 대해 사용합니다.

기본값

메시지 파싱 시, 특정 필드가 지정되어있지 않으면 필드는 기본값으로 설정됩니다! 그러므로, 메시지 필드가 직렬화 이후 파싱되었을 때, 필드의 값이 기본값인지 지정되지 않았던것인지 판단할 수 없습니다. 주의해야합니다.

각 타입별 기본값입니다.

  • string- 빈 문자열
  • bool - false
  • 숫자타입 - 0
  • enum- 첫 번째로 정의된 열거형 값이고, 0 이어야 함.
  • repeated - 빈 배열(언어에 따라 다릅니다)

proto3 필드 타입과 각 언어별 자료형 매핑

프로토콜버

출처: https://protobuf.dev

메시지 정의 유형

하나의 파일에서 여러개의 메시지를 정의할 수 있으며, 정의한 메시지를 다른 메시지에서 타입으로 사용할 수 있습니다.

message SearchResponse {
  repeated Result results = 1;
}

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

메시지를 원하는만큼 중첩해 정의할 수 있으며, 메시지 내부에 정의된 메시지는 부모 메시지로부터 참조해야합니다

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

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

여러가지 타입들

Map

Map을 통해 키와 값을 매핑시킬 수 있으며, 기본 형식은 아래와 같습니다.

map<key_type, value_type> map_field = N;
  • 키 타입으로는 Integer, String과 관련된 scalar type이 가능하며, byte, float타입은 불가능합니다.
  • Map타입은 repeated를 적용할 수 없습니다.

OneOf

OneOf타입은 상수형 값을 매핑시키는 enum과 다릅니다! 실제 자료형을 통해 선언된 여러 필드들 중, 하나의 값을 가질 수 있습니다. (map과 repeated는 사용할 수 없습니다)

message SampleMessage{
	oneof test_oneof{
		string name=4;
    SubMessage sub_message=9;
	}
}

메모리를 공유하기때문에 OneOf필드를 세팅하면 기존에 가지고있던 값이 사라집니다.

SampleMessage message;
message.set_name("name");
CHECK_EQ(message.name(), "name");
// sub_messgae필드를 없애고, name값을 할당합니다. 
// sub_message to a new instance of SubMessage with none of its fields set.
message.mutable_sub_message();
CHECK(message.name().empty());

언어별로 WhichOf, Case 등, 내부에 있는 값을 확인하는 로직을 통해 사용합니다.

질문

  • Map타입에서 float타입이 안되면, string으로 대체하는건가?
  • reserved : 다시 쓸 수 있나?

Reference

https://protobuf.dev/programming-guides/proto3/#simple

https://protobuf.dev/reference/protobuf/proto3-spec/

https://medium.com/naver-cloud-platform/nbp-기술-경험-시대의-흐름-grpc-깊게-파고들기-1-39e97cb3460

https://protobuf.dev/programming-guides/encoding