Protocol Buffers Documentation
protobuf는 어떻게 사용해요?
ProtoBuf에 대한 정리 와 예제를 통한 사용 방법 확인
gRPC - Protobuf란? 구글 프로토콜 버퍼(protocol buffers)
Google Protocol Buffers (ProtoBuf) - Best Alternative to Java Serialization
구글 프로토콜 버퍼 (Protocol buffer)
Protobuf — What & Why?
What is Protobuf?
프로젝트를 진행하며 protobuf를 사용하는 중인데,
모르는 내용이 많아 정리가 필요할 것 같다
syntax = "proto3";
package pt;
option go_package = "prototest/pt";
message Data {
int32 id = 1;
string name = 2;
string address = 3;
string sex = 4;
}
message DataPackage {
repeated Data data_list = 1;
int32 total_count = 2;
}
해당 .proto
파일은 프로젝트에 사용하기 위해 작성하였다.
각 문장을 하나하나 살펴보자.
syntax = "proto3"
proto3
은 Protobuf의 세 번째 버전으로, 최신 버전에서 사용proto3
에서는 기본값이 자동으로 제공됨 (숫자는 0, 문자열은 빈 문자열)required
)인지, 선택적(optional
)인지 명시 불필요package pt
.proto
파일에서 생성된 코드의 네임스페이스 역할 수행pt.Data
로 다른 패키지와 구분된 이름 사용 가능)option go_package = "prototest/pt"
*.pb.go
파일의 패키지를 설정package pt
prototest/pt
는 이 패키지가 속하는 모듈 경로를 의미message Date { ... }
id
: 고유 식별자 (정수형, 1번 필드)
name
: 이름 (문자열, 2번 필드)
address
: 주소 (문자열, 3번 필드)
sex
: 성별 (문자열, 4번 필드)
int32 id = 1
int32
: 필드 타입 지정, int32는 32비트 정수형id
: 필드명= 1
: 이 숫자는 필드 번호repeated Data data_list = 1
repeated
: 여러 개의 값을 저장할 수 있는 배열(리스트) 형식 정의Data
: 배열의 데이터 타입, Daya 메시지 타입 사용data_list
: 필드명, 이 필드에는 여러 개의 Data 객체가 저장될 수 있음= 1
: 이 필드의 필드 번호는 1Protobuf 파일은 다음 두 가지 주요 메시지를 정의:
- Data: 단일 데이터 항목
- DataPackage: Data의 리스트와 그 총 개수를 포함하는 패키지
이 구조는 Tx 서버에서 Rx 서버로 데이터를 전송하거나 Rx 서버에서 클라이언트에게 데이터를 제공할 때 매우 유용하다.
Protobuf는 직렬화된 형식으로 데이터를 압축하여 효율적으로 전송할 수 있게 도와준다.
Protobuf 파일을 컴파일하려면 다음 명령어를 사용하면 된다.
이를 위해 Protobuf 컴파일러(protoc
)와 Go용 Protobuf 플러그인(protoc-gen-go
)이 설치되어 있어야 한다.
protoc --go_out=. --go_opt=paths=source_relative data.proto
1. 명령어 설명
protoc
: Protobuf 컴파일러 실행 명령어--go_out=.
: Protobuf 파일을 Go 언어용으로 컴파일data.pb.go
파일은 현재 디렉토리에 저장--go_opt=paths=source_relative
: 생성된 파일의 import 경로를 Protobuf 파일의 경로와 상대적으로 설정data.proto
: 컴파일할 Protobuf 파일의 이름2. 컴파일 후 결과
위 명령어를 실행하면 현재 디렉토리에 data.pb.go
파일이 생성된다.
해당 파일은 Go 코드에서 사용 가능한 구조체와 관련 함수(예: Marshal, Unmarshal)를 포함하고 있다.
아래는 생성된 data.pb.go
파일이다.
Protobuf 파일(data.proto
)을 컴파일하면 생성되는 data.pb.go
파일은 Protobuf 메시지와 관련된 Go 코드를 포함하고 있다.
이 파일은 Protobuf 정의를 Go 언어로 변환한 코드로,
메시지를 생성하거나, 직렬화/역직렬화, 데이터 전송 등을 처리할 수 있는 함수와 구조체가 포함된다.
data.proto
파일에서 정의한 메시지가 Go의 구조체로 변환됨Marshal
: 메시지를 바이너리 데이터로 직렬화 (/Unmarshal
)이 파일은 직접 수정하지 않고, Protobuf 파일(data.proto
)을 수정한 후 다시 컴파일하는 방식으로 유지보수한다.
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.2
// protoc v3.12.4
// source: data.proto
package pt
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Data struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Address string `protobuf:"bytes,3,opt,name=address,proto3" json:"address,omitempty"`
Sex string `protobuf:"bytes,4,opt,name=sex,proto3" json:"sex,omitempty"`
}
func (x *Data) Reset() {
*x = Data{}
mi := &file_data_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Data) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Data) ProtoMessage() {}
func (x *Data) ProtoReflect() protoreflect.Message {
mi := &file_data_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Data.ProtoReflect.Descriptor instead.
func (*Data) Descriptor() ([]byte, []int) {
return file_data_proto_rawDescGZIP(), []int{0}
}
func (x *Data) GetId() int32 {
if x != nil {
return x.Id
}
return 0
}
func (x *Data) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Data) GetAddress() string {
if x != nil {
return x.Address
}
return ""
}
func (x *Data) GetSex() string {
if x != nil {
return x.Sex
}
return ""
}
type DataPackage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
DataList []*Data `protobuf:"bytes,1,rep,name=data_list,json=dataList,proto3" json:"data_list,omitempty"`
TotalCount int32 `protobuf:"varint,2,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"`
}
func (x *DataPackage) Reset() {
*x = DataPackage{}
mi := &file_data_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DataPackage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DataPackage) ProtoMessage() {}
func (x *DataPackage) ProtoReflect() protoreflect.Message {
mi := &file_data_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DataPackage.ProtoReflect.Descriptor instead.
func (*DataPackage) Descriptor() ([]byte, []int) {
return file_data_proto_rawDescGZIP(), []int{1}
}
func (x *DataPackage) GetDataList() []*Data {
if x != nil {
return x.DataList
}
return nil
}
func (x *DataPackage) GetTotalCount() int32 {
if x != nil {
return x.TotalCount
}
return 0
}
var File_data_proto protoreflect.FileDescriptor
var file_data_proto_rawDesc = []byte{
0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x70, 0x74,
0x22, 0x56, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07,
0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61,
0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x65, 0x78, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x65, 0x78, 0x22, 0x55, 0x0a, 0x0b, 0x44, 0x61, 0x74, 0x61,
0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, 0x25, 0x0a, 0x09, 0x64, 0x61, 0x74, 0x61, 0x5f,
0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x70, 0x74, 0x2e,
0x44, 0x61, 0x74, 0x61, 0x52, 0x08, 0x64, 0x61, 0x74, 0x61, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1f,
0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20,
0x01, 0x28, 0x05, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x42,
0x0e, 0x5a, 0x0c, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x74, 0x65, 0x73, 0x74, 0x2f, 0x70, 0x74, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_data_proto_rawDescOnce sync.Once
file_data_proto_rawDescData = file_data_proto_rawDesc
)
func file_data_proto_rawDescGZIP() []byte {
file_data_proto_rawDescOnce.Do(func() {
file_data_proto_rawDescData = protoimpl.X.CompressGZIP(file_data_proto_rawDescData)
})
return file_data_proto_rawDescData
}
var file_data_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_data_proto_goTypes = []any{
(*Data)(nil), // 0: pt.Data
(*DataPackage)(nil), // 1: pt.DataPackage
}
var file_data_proto_depIdxs = []int32{
0, // 0: pt.DataPackage.data_list:type_name -> pt.Data
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_data_proto_init() }
func file_data_proto_init() {
if File_data_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_data_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_data_proto_goTypes,
DependencyIndexes: file_data_proto_depIdxs,
MessageInfos: file_data_proto_msgTypes,
}.Build()
File_data_proto = out.File
file_data_proto_rawDesc = nil
file_data_proto_goTypes = nil
file_data_proto_depIdxs = nil
}
Protocol Buffers(Protobuf)는 데이터를 효율적으로 저장하고 전송하기 위한 직렬화(Serialization) 시스템
필드 번호 + 타입
과 데이터 값 포함data := &Data{
Id: 1,
Name: "Alex",
Address: "123 Main St",
Sex: "Male",
}
Protobuf는 이 데이터를 다음과 같이 저장:
Id
: 필드 번호 1, 값 1 → 0x08 0x01
Name
: 필드 번호 2, 값 "Alex" → 0x12 0x04 0x41 0x6C 0x65 0x78
Address
: 필드 번호 3, 값 "123 Main St" → 0x1A ...
Sex
: 필드 번호 4, 값 "Male" → 0x22 ...
Protobuf는 데이터를 직렬화하여 저장하고, 필요할 때 역직렬화하여 사용한다.
(1) 데이터 직렬화
Go에서 Protobuf 메시지를 생성하고 이를 바이트 형식으로 변환:
data := &Data{
Id: 1,
Name: "Alex",
Address: "123 Main St",
Sex: "Male",
}
dataBytes, err := proto.Marshal(data) // Protobuf 직렬화: Data 구조체를 바이트 슬라이스로 변환
if err != nil {
log.Fatalf("Failed to serialize data: %v", err)
}
(2) 데이터 역직렬화
수신한 데이터를 바이트 슬라이스에서 Protobuf 메시지로 변환:
receivedBytes := []byte{...} // 수신한 바이트 슬라이스
var receivedData Data
err := proto.Unmarshal(receivedBytes, &receivedData) // Protobuf 역직렬화: 바이트 슬라이스를 Data 메시지로 변환
if err != nil {
log.Fatalf("Failed to deserialize data: %v", err)
}
Q. 포인터(&)를 사용하는 이유?
A. Protobuf에서는 역직렬화 시, 데이터가 직접 수정되어야 하므로 포인터를 전달한다.
Q. 직렬화에서는 포인터를 사용하지 않는 이유?
A. 직렬화 시에는 Protobuf 구조체를 읽기만 하고, 바이트 슬라이스를 생성한다. 따라서 포인터가 필요하지 않다.
Protobuf를 통해 데이터가 전달되고 해석되는 과정을 간단히 설명하면,
데이터를 작은 크기로 압축해서 전송하고, 다른 쪽에서 이를 다시 원래의 구조로 복원하는 과정이라고 볼 수 있다.
데이터가 Protobuf 메시지 형식에 맞춰 준비되면,
→ Protobuf 라이브러리가 이 데이터를 바이너리 형태로 변환(=직렬화)
→ 받은 바이너리 데이터를 Protobuf 라이브러리에 전달하고,
→ 라이브러리는 이 데이터를 미리 정의된 메시지 구조에 맞춰 해석, 직렬화할 때의 데이터와 동일한 구조를 복원!
message Person {
required string user_name = 1;
optional int64 favourite_number = 2;
repeated string interests = 3;
}
위와 같은 Person이라는 Proto 정의가 있을 때, ProtoCol Buffer는 아래 그림처럼 동작하게 된다.
Protocol Buffer의 경우
The following diagram shows how you use protocol buffers to work with your data.
The code generated by protocol buffers
provides utility methods to
The following code samples show you an example of this flow in Java.
As shown earlier, this is a .proto definition:
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
}
Compiling this .proto
file creates a Builder class that you can use to create new instances, as in the following Java code:
Person john = Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.build();
output = new FileOutputStream(args[0]);
john.writeTo(output);
You can then deserialize data using the methods protocol buffers creates in other languages, like C++:
Person john;
fstream input(argv[1], ios::in | ios::binary);
john.ParseFromIstream(&input);
int id = john.id();
std::string name = john.name();
std::string email = john.email();