Protocol buffer 기초
Protocol buffer 기본 개념
Protocol Buffers
란?- 구글이 개발한 언어 중립적이고, 플랫폼 중립적인, 확장 가능한 직렬화(serialization) 포맷
- 직렬화 - 데이터(객체)를 저장하거나 전송할 수 있도록 연속된 바이트 형태로 변환하는 과정
- 언어 중립적이므로 python, c 등 언어와 무관하게 일정한 데이터 형식을 공통적으로 사용할 수 있음.
- 특징
.proto
라는 스키마 파일을 사용해서 메시지 구조를 정의- 스키마 파일을 바탕으로 각 언어에 맞는 코드를 자동 생성해줌
- 구글이 개발한 언어 중립적이고, 플랫폼 중립적인, 확장 가능한 직렬화(serialization) 포맷
- 언제 사용하는가?
- 시스템 간 통신 (gRPC, RPC 호출 등)
- gRPC는 기본 직렬화 포맷으로 protobuf 사용
- TFRecord 저장 시
- TensorFlow의 TFRecord 파일은 내부적으로 protobuf를 사용해서 Example, Features 등을 직렬화
- 모델 메타 정보 저장
- 모델 버전, 하이퍼파라미터 정보, 실험 로그 등도 protobuf 기반으로 저장하면 스키마 명확성 + 경량성 확보 가능
- Configuration 관리
- .proto로 설정 스키마를 정의하고, 설정 파일을 바이너리로 배포 가능
- 그 외 다양한 데이터 구조를 표현 할 수 있음
- 시스템 간 통신 (gRPC, RPC 호출 등)
Protobuffer 기본 구조
- proto 파일의 구조
syntax = "proto3";
package user;
message UserProfile {
string name = 1;
int32 age = 2;
bool is_active = 3;
}
- 왜 번호를 부여하는가?
- 사람이 읽기 좋은 구조(JSON처럼 이름-값 구조)가 아니라, 머신이 빠르게 파싱 가능한 구조를 만들기 위해 필드에 이름 대신 숫자 ID를 붙여서 식별
- 위 예시에서 name age 등의 이름은 직렬화된 파일에서 사용되지 않고 번호만 사용됨.
- 직렬화 역직렬화 양쪽에서 사용되므로 한 번 배포된 .proto는 절대 필드 번호를 바꾸지 않아야 함.
삭제된 필드 번호는 reserved
해두는 것이 좋음
message User {
reserved 3, 5 to 7;
}
enum
키워드로 상태값, 타입값 등을 정해진 셋으로 관리
message Job {
string id = 1;
JobStatus status = 2;
enum JobStatus {
UNKNOWN = 0;
RUNNING = 1;
DONE = 2;
}
}
repeated
키워드로 List 를 표현할 수 있음
message Article {
string title = 1; // optional
optional string summary = 2; // presence check 가능
repeated string tags = 3; // 0개 이상
}
- 외부 메시지 참조 및 중첩
common.proto
파일
message Timestamp {
int64 seconds = 1;
int32 nanos = 2;
}
user.proto
파일
import "common.proto";
message User {
string id = 1;
Timestamp created_at = 2;
}
Map
,Wrapper
,oneof
syntax = "proto3";
package event;
import "google/protobuf/any.proto"; // Any 타입 사용을 위해 import
import "google/protobuf/wrappers.proto"; // Wrapper 타입 사용을 위해 import
message EventLog {
// wrapper: 값이 없을 수도 있는 optional string (proto3에서는 기본 string은 항상 ""이라 구분 안 됨)
google.protobuf.StringValue session_id = 1;
// map: 태그처럼 key-value 데이터를 유연하게 저장할 수 있음 (e.g. {"region": "US", "device": "mobile"})
map<string, string> attributes = 2;
// oneof: 아래 3개 중 하나만 설정 가능 (예: 하나의 이벤트는 click, view, purchase 중 하나만 발생함)
oneof event_type {
Click click = 3;
View view = 4;
Purchase purchase = 5;
}
// Any: 다양한 구조의 확장 가능한 payload를 받을 수 있음 (새로운 이벤트 타입이 생겨도 schema 변경 없이 처리 가능)
google.protobuf.Any extra_payload = 6;
}
Protobuf 컴파일러 protoc
protoc 컴파일러
란?- .proto 파일을 각 언어에 맞는 클래스/코드로 자동 생성
- 다양한 언어 지원
- Python --python_out=...
- Java --java_out=...
- C++ --cpp_out=...
- Go --go_out=...
- JavaScript/TypeScript --js_out=... 또는 --ts_out=...
- 다양한 언어 지원
- .proto 파일을 각 언어에 맞는 클래스/코드로 자동 생성
protoc --proto_path=. \\
--python_out=./gen \\
user.proto
→ user_pb2.py 파일이 생성되면 파이썬 코드에서 활용하면 됨
import user_pb2
user = user_pb2.User(name="Alice", age=30)
# 직렬화
binary_data = user.SerializeToString()
# 역직렬화
new_user = user_pb2.User()
new_user.ParseFromString(binary_data)
print(new_user.name, new_user.age) # "Alice 30"
- 다른 코드들에서도 사용 (C, java 예시)
#include "user.pb.h"
User u;
u.set_name("Alice");
u.set_age(30);
std::string data;
u.SerializeToString(&data);
// 역직렬화
User u2;
u2.ParseFromString(data);
std::cout << u2.name() << ", " << u2.age() << std::endl;
UserOuterClass.User user = UserOuterClass.User.newBuilder()
.setName("Alice")
.setAge(30)
.build();
byte[] data = user.toByteArray();
UserOuterClass.User user2 = UserOuterClass.User.parseFrom(data);
System.out.println(user2.getName() + " " + user2.getAge());
- protobuf 와 json 사이의 변환 가능
- 파이썬 예시
from google.protobuf.json_format import MessageToDict, ParseDict
# Protobuf → JSON (dict)
json_obj = MessageToDict(event_message)
# JSON (dict) → Protobuf
msg = EventLog()
ParseDict(json_obj, msg)
Protobuf 응용
- Tensorflow 에서 활용
- 학습 데이터를
TFRecord
로 저장시, 안의 구조는 Protobuf 메시지로 정의되어 있음
// tf.train.Example 메시지 구조
message Example {
Features features = 1;
}
message Features {
map<string, Feature> feature = 1;
}
message Feature {
oneof kind {
BytesList bytes_list = 1;
FloatList float_list = 2;
Int64List int64_list = 3;
}
}
Custom option field
- 각 필드에 대해 추가적인 metadata 를 기록할 수 있음
- 활용 예시 - 대략 아래와 같은 사용 예시가 있음
- is_experimental: 모델 학습에는 사용하지만 serving에는 아직 안 쓰는 필드
- is_deprecated: 곧 제거 예정인 필드
- visibility = INTERNAL: 특정 팀만 사용하는 필드
- 사용 예시
- my_option.proto라는 새 파일을 만들어서 옵션 정의
// my_option.proto
import "google/protobuf/descriptor.proto";
message MyFieldOption {
bool is_test_field = 1;
}
// protobuf의 기본 FieldOptions 확장
extend google.protobuf.FieldOptions {
MyFieldOption my_option = 1001;
}
- main proto 파일에서 아래와 같이 option 추가
import "my_option.proto";
message Person {
string name = 1;
int32 age = 2 [(my_option).is_test_field = true];
}
- 사용시 해당 옵션을 파싱해서 구분
for field in Person.DESCRIPTOR.fields:
options = field.GetOptions()
if options.HasExtension(my_option):
if options.Extensions[my_option].is_test_field:
print(f"Field '{field.name}' is marked as test_field ✅")
else:
print(f"Field '{field.name}' has 'my_option', but not marked")
else:
print(f"Field '{field.name}' has no my_option ❌")