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를 사용하는 중인데,
모르는 내용이 많아 정리가 필요할 것 같다


1. 파일 (data.proto)

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 파일은 프로젝트에 사용하기 위해 작성하였다.

각 문장을 하나하나 살펴보자.


1-1. 탐구

  1. syntax = "proto3"
    : Protobuf의 버전을 지정. proto3은 Protobuf의 세 번째 버전으로, 최신 버전에서 사용
  • Protobuf는 데이터를 직렬화하는 데 사용되는 Google의 인터페이스 정의 언어
  • proto3에서는 기본값이 자동으로 제공됨 (숫자는 0, 문자열은 빈 문자열)
  • 필드가 필수(required)인지, 선택적(optional)인지 명시 불필요
  1. package pt
    : Protobuf 메시지와 RPC 서비스의 네임스페이스 정의
  • 이 패키지는 작성된 .proto 파일에서 생성된 코드의 네임스페이스 역할 수행
  • 네임스페이스 설정 → 동일한 이름의 메시지 충돌 방지 (예: pt.Data로 다른 패키지와 구분된 이름 사용 가능)
    • 네임스페이스: 이름 충돌을 방지하기 위해 이름을 구분하는 방법,
      코드에서 동일한 이름의 변수/함수/클래스 등이 있을 경우 네임스페이스를 통해 각 이름이 속한 영역을 정의하여 구분
  1. option go_package = "prototest/pt"
    : Go 언어를 위한 패키지 경로 지정
  • Protobuf 파일을 Go 코드로 컴파일할 때 생성되는 *.pb.go 파일의 패키지를 설정
  • 이 옵션에 따라 생성된 Go 파일의 상단에 아래와 같이 패키지 설정됨
package pt
  • 여기서 prototest/pt는 이 패키지가 속하는 모듈 경로를 의미
  1. message Date { ... }
    : Data는 Protobuf 메시지, 데이터를 저장하거나 전송하기 위해 정의된 구조체(또는 클래스)라고 생각
  • 메시지는 Protobuf의 기본 데이터 구조, 네트워크 상에서 직렬화/역직렬화 가능
  • 해당 메시지에는 4개의 필드 정의됨
    • id: 고유 식별자 (정수형, 1번 필드)

    • name: 이름 (문자열, 2번 필드)

    • address: 주소 (문자열, 3번 필드)

    • sex: 성별 (문자열, 4번 필드)

  1. int32 id = 1
    : Data 메시지가 직렬화될 때 id 필드는 1번으로 인식
  • 구성요소
    • int32: 필드 타입 지정, int32는 32비트 정수형
      → Protobuf는 다양한 데이터 타입 지원 (int32, string, bool, bytes 등)
    • id: 필드명
    • = 1: 이 숫자는 필드 번호
      → Protobuf에서 필드 번호는 데이터를 인코딩/디코딩할 때 사용, 각 메시지 안에서 필드 번호는 고유
  1. repeated Data data_list = 1
    : DataPackage 메시지에서 data_list는 여러 개의 Data 메시지를 포함하는 리스트로 사용
  • repeated: 여러 개의 값을 저장할 수 있는 배열(리스트) 형식 정의
  • Data: 배열의 데이터 타입, Daya 메시지 타입 사용
  • data_list: 필드명, 이 필드에는 여러 개의 Data 객체가 저장될 수 있음
  • = 1: 이 필드의 필드 번호는 1

Protobuf 파일은 다음 두 가지 주요 메시지를 정의:

  • Data: 단일 데이터 항목
  • DataPackage: Data의 리스트와 그 총 개수를 포함하는 패키지

이 구조는 Tx 서버에서 Rx 서버로 데이터를 전송하거나 Rx 서버에서 클라이언트에게 데이터를 제공할 때 매우 유용하다.
Protobuf는 직렬화된 형식으로 데이터를 압축하여 효율적으로 전송할 수 있게 도와준다.


1-2. 컴파일

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 파일의 경로와 상대적으로 설정
    → 이 옵션이 없으면 Go의 기본 GOPATH를 기반으로 경로가 설정
  • data.proto: 컴파일할 Protobuf 파일의 이름

2. 컴파일 후 결과
위 명령어를 실행하면 현재 디렉토리에 data.pb.go 파일이 생성된다.
해당 파일은 Go 코드에서 사용 가능한 구조체와 관련 함수(예: Marshal, Unmarshal)를 포함하고 있다.


1-3. 파일 (data.pb.go)

아래는 생성된 data.pb.go 파일이다.

Protobuf 파일(data.proto)을 컴파일하면 생성되는 data.pb.go 파일은 Protobuf 메시지와 관련된 Go 코드를 포함하고 있다.

이 파일은 Protobuf 정의를 Go 언어로 변환한 코드로,
메시지를 생성하거나, 직렬화/역직렬화, 데이터 전송 등을 처리할 수 있는 함수와 구조체가 포함된다.

  • 메시지를 표현하는 구조체data.proto 파일에서 정의한 메시지가 Go의 구조체로 변환됨
  • 메시지의 직렬화/역직렬화 함수 → Marshal: 메시지를 바이너리 데이터로 직렬화 (/Unmarshal)
  • Protobuf 런타임에서 사용하는 메타데이터 → 파일의 이름, 메시지 타입, 패키지 정보 등이 메타데이터로 정의
  • 메시지 필드와 관련된 태그 및 변환 로직

이 파일은 직접 수정하지 않고, 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
}

2. 데이터 저장 방식

Protocol Buffers(Protobuf)는 데이터를 효율적으로 저장하고 전송하기 위한 직렬화(Serialization) 시스템

  1. 데이터를 작고 빠른 이진 형식으로 직렬화
  • 필드 이름 대신 숫자(필드 번호)를 사용
    • 필드 번호는 1~15는 1바이트, 16~2047는 2바이트로 저장됩니다. 작은 번호가 더 적은 공간을 차지
  1. 데이터를 필드 번호와 값 쌍으로 저장
  • 결합하여 직렬화된 바이트 스트림 생성
  • 각 필드는 필드 번호 + 타입과 데이터 값 포함
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 ...

2-1. 데이터 구조 사용

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에서는 역직렬화 시, 데이터가 직접 수정되어야 하므로 포인터를 전달한다.

  • Protobuf 함수는 바이트 데이터를 받아서 구조체에 값을 채운다.
  • Go에서는 함수 내부에서 값을 수정하려면 포인터를 전달해야 한다.

Q. 직렬화에서는 포인터를 사용하지 않는 이유?
A. 직렬화 시에는 Protobuf 구조체를 읽기만 하고, 바이트 슬라이스를 생성한다. 따라서 포인터가 필요하지 않다.


2-2. 작동 예시

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의 경우

  • 필드명을 Person 메시지의 변수에 정의한 숫자 1,2,3를 사용해 필드명을 대체
    → 데이터의 용량을 줄이는 것
    = protobuf에서는 불필요한 속성값(user_name, interests)은 숫자로 대체해버리고,
    1번이라는 값에 매칭을 해서 값을 가지고 오는 형식으로 데이터를 표현하게 된다.


3. 공식 문서

The following diagram shows how you use protocol buffers to work with your data.

The code generated by protocol buffers
provides utility methods to

  • retrieve data from files and streams,
  • extract individual values from the data,
  • check if data exists,
  • serialize data back to a file or stream, and other useful functions.

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();
profile
공부 기록용 24.08.05~

0개의 댓글

Powered by GraphCDN, the GraphQL CDN