All Articles

Protocol Buffer

Protocol Buffer는 구글에서 만든 구조화된 데이터를 직렬화 하기 위한 메커니즘이며, 주로 gRPC를 이용해 데이터를 주고 받을 때 사용된다.

XML, CSV, YAML, JSON과 같이 데이터의 직렬화/역직렬화를 위해 사용하는 방법 중의 하나로

보다 빠르고, 작고, 간단한 유지보수를 위해 개발되었다.

하지만 새로운 표현 방법(.proto)을 익혀야 하고, 결과물이 사람에게 익숙한 형태가 아니어서 사용하기 어렵다는 단점이 있다.

protocol buffer 작성하기

구조체를 정의하는 .proto 파일을 작성 후 protoc을 이용해 컴파일해 나온 결과물을 이용해 사용할 수 있다.

Golang의 경우 .pb.go 라는 확장자를 가진 코드가 생성 되는데 해당 소스 코드의 객체를 import하여 사용하면 된다.

메시지 타입 정의하기

protocol buffer에서의 데이터 구조체는 메시지 형태로 작성됩니다.

이러한 메시지는 .proto 파일에 작성되며, 하나의 .proto 파일에는 하나 혹은 여러개의 메시지 구조체를 작성할 수 있다.

syntax = "proto3";

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
}

메시지는 필드 규칙, 필드 타입, 필드 이름, 필드 번호로 구성된다

필드 규칙

메시지의 필드는 다음의 규칙 중 하나를 지정해야 한다.

  • required : 필수 필드, 메시지를 사용할 때 필수적으로 사용 하는 필드
  • optional : 옵션 필드, 메시지를 사용할 때 선택적으로 사용 하는 필드,
  • repeated : 반복 필드, 메시지를 사용할 때 0개 이상 반복적으로 입력되는 필드, 반복되는 값의 순서는 유지된다

옵션 필드의 기본값

옵션 필드의 경우 default 값 지정이 가능하며, 메시지의 해당 필드에 값이 포함되어 있지 않을경우 default 값으로 설정된다.

optional int32 result_per_page = 3 [default = 10];

필드 타입

필드 타입은 필드의 데이터 타입을 정의하며, 자세한 필드 타입은 이곳 에서 확인할 수 있다.

필드 번호

필드 번호는 각 필드마다 고유해야 하며, 이 숫자는 메시지 바이너리 타입엠서 필드를 식별하는데 사용된다.

필드 번호는 1부터 536,870,911까지 사용할 수 있으며, protocol buffer 구현을 위한 예약된 19000 ~ 19999(FieldDescriptor::kFirstReservedNumber ~ FieldDescriptor::kLastReservedNumber) 번은 사용할 수 없다.

protocol buffer compiler 설치

protocol buffer 컴파일러를 통해 .proto 파일로 작성된 코드를 각 언어별로 컴파일하여 사용할 수 있다.

protoc 최신 버전은 protobuf 저장소에서 다운로드 받을 수 있다

그리고 golang 버전으로 compile 하기 위해서는 protoc-gen-go를 추가적으로 설치해주어야 한다.

export GO111MODULE=on
go get -d -u github.com/golang/protobuf/proto
go get -d -u github.com/golang/protobuf/protoc-gen-go
go get google.golang.org/protobuf/cmd/protoc-gen-go

protoc를 이용하여 .proto 코드를 .pb.go 와 같은 확장자의 직렬화 코드로 컴파일할 수 있다.

protoc -I={$IMPORT_PATH} --go_out={$DST_PATH} {$TARGET_FILE}
  • -I : 컴파일할 .ptoto 파일의 위치
  • --go_out : 컴파일된 결과물이 저장될 위치
  • TARGET_FILE : 컴파일할 .proto 파일

사용 방법

Person 메시지를 만들어 직렬화/역직렬화 하는 과정은 다음과 같다.

// ./proto/person.proto

syntax = "proto3";

option go_package = "github.com/violetstair/go-api/util";

message Person {
  string name = 1;
  int32 id = 2;
  optional string email = 3;
}

.proto 파일의 컴파일

protoc -I=./proto --go_out=./out person.proto

컴파일한 결과물은 ./out 디렉토리에 option에 지정한 go_package 경로의 person.pb.go 파일로 저장된다.

person.pb.go 파일을 살펴보면 .proto 파일에 정의된 Person 메시지에 대한 구조체가 정의된 것을 확인할 수 있다.

type Person struct {
  state         protoimpl.MessageState
  sizeCache     protoimpl.SizeCache
  unknownFields protoimpl.UnknownFields

  Name  string  `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
  Id    int32   `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
  Email *string `protobuf:"bytes,3,opt,name=email,proto3,oneof" json:"email,omitempty"`
}

컴파일된 person.pb.go의 사용해 각각 protobufjson 형식으로 파일을 읽고쓰는 예제 코드이다

package main

import (
    "encoding/json"
    "fmt"
    "github.com/golang/protobuf/proto"
    "github.com/violetstair/go-api/util"
    "io/ioutil"
    "log"
)

func encodePerson(name, email string, id int32) {
    person := &util.Person{Name: name, Id: id, Email: &email}

    protobufOut, err := proto.Marshal(person)
    if err != nil {
        log.Fatalf("Failed encode : %v\n", err)
    }

    if err := ioutil.WriteFile("protobuf_person", protobufOut, 0644); err != nil {
        log.Fatalf("Failed write: %v\n", err)
    }

    jsonOut, err := json.Marshal(person)
    if err != nil {
        log.Fatalf("Failed encode : %v\n", err)
    }

    if err := ioutil.WriteFile("json_person", jsonOut, 0644); err != nil {
        log.Fatalf("Failed write: %v\n", err)
    }
}

func decodePerson() {
    protoPerson := util.Person{}
    jsonPerson := util.Person{}

    protobufData, err := ioutil.ReadFile("protobuf_person")
    if err != nil {
        log.Fatalf("Error read protobuf file: %v\n", err)
    }

    if err := proto.Unmarshal(protobufData, &protoPerson); err != nil {
        log.Fatalf("Failed decode protobuf : %v\n", err)
    }

    jsonData, err := ioutil.ReadFile("json_person")
    if err != nil {
        log.Fatalf("Error read json file: %v\n", err)
    }

    if err := json.Unmarshal(jsonData, &jsonPerson); err != nil {
        log.Fatalf("Failed decode json : %v\n", err)
    }

    fmt.Println("proto : ", protoPerson.String())
    fmt.Println("json : ", jsonPerson.String())
}

func main() {
    encodePerson("hong", "hong@email.com", 10)
    decodePerson()
}

당연하게도 protobufjson 두 직렬화 방식 모두 동일한 결과를 출력한다

하지만 생성된 파일의 크기를 보면 protobuf로 작성한 파일의 크기가 json으로 생성된 파일에 절반 수준인것을 확인할 수 있다.

48B json_person
24B protobuf_person

Published May 4, 2021

Right Thoughts, Right Words, Right Action