5 min read

Protocol buffer 기초

Protocol buffer 기초
Photo by Markus Spiske / Unsplash

Protocol buffer 기본 개념

  • Protocol Buffers 란?
    • 구글이 개발한 언어 중립적이고, 플랫폼 중립적인, 확장 가능한 직렬화(serialization) 포맷
      • 직렬화 - 데이터(객체)를 저장하거나 전송할 수 있도록 연속된 바이트 형태로 변환하는 과정
      • 언어 중립적이므로 python, c 등 언어와 무관하게 일정한 데이터 형식을 공통적으로 사용할 수 있음.
    • 특징
      • .proto 라는 스키마 파일을 사용해서 메시지 구조를 정의
      • 스키마 파일을 바탕으로 각 언어에 맞는 코드를 자동 생성해줌
  • 언제 사용하는가?
    1. 시스템 간 통신 (gRPC, RPC 호출 등)
      1. gRPC는 기본 직렬화 포맷으로 protobuf 사용
    2. TFRecord 저장 시
      1. TensorFlow의 TFRecord 파일은 내부적으로 protobuf를 사용해서 Example, Features 등을 직렬화
    3. 모델 메타 정보 저장
      1. 모델 버전, 하이퍼파라미터 정보, 실험 로그 등도 protobuf 기반으로 저장하면 스키마 명확성 + 경량성 확보 가능
    4. Configuration 관리
      1. .proto로 설정 스키마를 정의하고, 설정 파일을 바이너리로 배포 가능
    5. 그 외 다양한 데이터 구조를 표현 할 수 있음

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=...
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 ❌")