메뉴

문서정보

목차

Protocol Buffer

Protocol Buffer(이하 PB)는 구글에서 개발한 직렬화 프로토콜이다. 구글 얘네들이 검색 색인 작업에 사용하려고 만들었다고 한다. 다양한 형태의 데이터를 대량으로 빠르게 직렬화&역직렬화 할 수 있다고 한다. 구글에서 다루는 데이터가 워낙에 다양하고 양도 많으니 당연한 요구사항 이었겠지.

선택한 이유

JSON을 대체하기 위한 목적으로 사용하려 한다. JSON은 직관적이며, 읽고, 쓰기 편하다. 특히 웹 애플리케이션을 개발하기 위한 최적의 프로토콜이라 할 만하다. OpenAPI 서비스들의 대부분이 JSON을 문서형식으로 사용하고 있다. 반면 텍스트 기반이라서, 파싱하는데 비용이 상당히 많이 들고 데이터의 크기가 커진다는 단점이 있다.

이런 단점들이 웹 애플리케에서는 별 문제가 아닐 수 있지만, IoT 플랫폼에서는 문제가 될 수 있다. IoT의 경우 저전력 환경에서 작동하는 컴퓨팅 파워가 약한 기기들을 수용할 수 있어야 한다. IoT에서라면 만들어지는 메시지의 양이 대규모일 수 밖에 없으므로 빠르고 효율적으로 처리할 수 있는 프로토콜이 중요하다. 메시지의 크기를 30~40% 줄이는 것으로도 인프라의 효율을 크게 높일 수 있다. 정리하자면
  1. 기기들이 사용할 효율적인 직렬화 프로토콜
  2. 인프라에서 사용할 효율적인 직렬화 프로토콜
이 필요하다. 내 최근 관심은 IoT 인프라 개발이다. 자연스럽게 효율적인 직렬화 프로토콜에 관심을 가지게 됐다.

PB 외에도 Thrift, Avro 등의 프로토콜들이 있다. 왜 하필 PB냐 하면, 귀에 많이 들려서다. (다른 녀석들과 성능이나 기능에 큰 차이가 있는 것도 아닌 것 같고, 그래서 선택했다. 즉 생각하기 귀찮아서..)

JSON과 XML, Protocol buf에 대한 일반적인 비교

JSON XML Protocol Buffer 개인적으로 최근 웹 애플리케이션 프로젝트에서 XML을 사용한 적이 없다. 인터넷에 공개되는 부분은 JSON, 내부처리는 PB 정도로 생각하고 있다.

왜 프로토콜 버퍼인가 - Google 설명

주소록을 만든다고 가정해보자. 주소록의 각 레코드는 사람의 이름, ID, 이메일, 주소, 전화번호를 가지고 있을 것이다. 이와 같은 구조화된 데이터를 직렬화하고 검색할 필요가 있다. 특히 인터넷이 일반적인 환경이 되면서, 서로 다른 언어들이 서버/클라이언트로 연결하는 상황도 고려해야 한다.

프로토콜 버퍼는 효율적이며 자동화된 솔류션이다. 프로토콜 버퍼를 사용해서 데이터 구조에 대한 정보를 담고 있는 .proto 파일을 작성 할 수 있다. 프로토콜 컴파일러는 .proto 파일을 읽어서 각 언어에 맞는 클래스를 출력한다. 이 클래서는 효율적인 이진 데이터로의 자동 인코딩/디코딩을 구현하며, 프로토콜 버퍼를 구성하는 필드에 대한 getter 및 setter를 제공한다.

성능

Server CPU % Avg. Client CPU % Avg. Time
REST - XML 12.00% 80.75% 05:27.45
REST - JSON 20.00% 75.00% 04:44.83
RMI 16.00% 46.50% 02:14.54
Protocol Buffers 30.00% 37.75% 01:19.43
Thrift - TBinary Protocol 33.00% 21.00% 01:13.65
Thrift - TCompactProtocol 30.00% 22.50% 01:05.12

PB 사용 프로젝트들

PB 지원 언어들

Protocol buffer 개발 가이드

이 문서는 Go 언어를 대상으로 한다.
# go version
go version go1.12.4 linux/amd64

Proto2 vs Proto3

현재(2019년 8월 17) 프로토콜 버퍼의 최신버전은 proto3다. 이 문서는 proto3를 기준으로 한다.

프로토콜버퍼 개발 툴 설치

프로토콜버퍼 컴파일러를 설치 한다. https://github.com/protocolbuffers/protobuf/releases 에서 다운로드 할 수 있다. 나는 protoc-3.9.1-linux-x86_64.zip를 다운로드했다. 다운로드 한 파일은 /opt/proto 밑에 압축을 풀었다.
# mkdir /opt/proto
# curl https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-linux-x86_64.zip 
# mv protoc-3.9.1-linux-x86_64.zip /opt/proto
# cd /opt/proto
# unzip protoc-3.9.1-linux-x86_64.zip
파일 구조는 아래와 같다. /opt/proto/bin은 PATH 환경변수에 추가하자.
.
├── bin
│   └── protoc
├── include
│   └── google
│       └── protobuf
│           ├── any.proto
│           ├── api.proto
│           ├── compiler
│           │   └── plugin.proto
│           ├── descriptor.proto
│           ├── duration.proto
│           ├── empty.proto
│           ├── field_mask.proto
│           ├── source_context.proto
│           ├── struct.proto
│           ├── timestamp.proto
│           ├── type.proto
│           └── wrappers.proto
└── readme.txt

Go 프로토콜 버퍼 플러그인을 설치한다.
# go get -u github.com/golang/protobuf/protoc-gen-go
플러그인은 ~/go/bin 디렉토리에 설치된다.

작동방식

개발자는 프로토콜 버퍼에서 제공하는 여러 메시지 타입 유형을 이용해서 .proto파일에 직렬화할 정보를 기술한다. 각 프로토콜 버퍼 메시지는 name-value 형태를 가지며, 논리적인 정보세트로 구성된다. 아래는 개인 정보를 포함하는 간단한 프로토콜 버퍼 파일 예제다.

syntax = "proto3";
package person;

message Person {
    string name = 1;
    int32 id =2 ;
    string email = 5;
                                    
    enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;                                 
    }

    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
    }
                                    
    repeated PhoneNumber phone = 4;
}
메시지 형식은 간단하다. 메시지는 name-value로 구성된 고유한 번호를 가지는 필드로 구성된다. 값(value)는 string, int32, string와 같은 타입을 가진다. 메시지는 계층적으로 구성을 할 수 있으며, 필수, 선택, 반복 필드를 지정 할 수 있다.

메시지를 정의한 후에는 .proto 파일을 응용 프로그램언어의 프로토콜 버퍼 컴파일러를 이용해서 데이터엑세스 클래스를 만든다. 클래스는 각 필드에 대한 간단한 접근자를 제공하며, 전체 구조를 직렬화/파싱하기 위한 메소드도 제공한다. 만약 위의 파일을 C++로 변환한다면 Person 클래스를 생성 하고, go로 변환한다면 Person 스트럭처를 만들 것이다.

C++로 변환해보자.
# protoc --cpp_out=./ person.proto
# ls 
person.pb.cc  person.pb.h  person.proto

go로 변환해보자.
# mkdir person
# protoc --go_out=person person.proto 

내 주력언어는 go 언어이므로 go 파일을 살펴보기로 했다. 파일 구성은 아래와 같다.
# tree 
├── person
│   └── person.pb.go
└── person.proto
person.pb.go가 person.proto로 부터 컴파일된 go 파일이다. 파일의 주요 내용을 분석해보자.
// 이 파일은 수정하면 안된다. 
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: person.proto

// 프로토콜 버퍼의 package 이름이 go package 이름이 됐다. 
package person

import (
    fmt "fmt"
    proto "github.com/golang/protobuf/proto"
    math "math"
)

// 생략 ......

// message Person이 go 스트럭처로 컴파일 됐다.
// 프로토콜 버퍼의 필드 유형은 go 언어에 대응되는 적당한 필드로 변환된 걸 확인 할 수 있다.
// 프로토콜 버퍼의 정보들은 구조체 태그로 표현되고 있다. 
type Person struct {
    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,5,opt,name=email,proto3" json:"email,omitempty"`
    Phone                []*Person_PhoneNumber `protobuf:"bytes,4,rep,name=phone,proto3" json:"phone,omitempty"`
    XXX_NoUnkeyedLiteral struct{}              `json:"-"`
    XXX_unrecognized     []byte                `json:"-"`
    XXX_sizecache        int32                 `json:"-"`
}

// Reset, String 등 메서드를 자동으로 만들었다.
func (m *Person) Reset()         { *m = Person{} }
func (m *Person) String() string { return proto.CompactTextString(m) }
func (*Person) ProtoMessage()    {}
func (*Person) Descriptor() ([]byte, []int) {
    return fileDescriptor_4c9e10cf24b1156d, []int{0}
}

// 프로토콜 버퍼의 Name, Id, Email 필드를 가져오기 위한 메서드도 만들어졌다.
func (m *Person) GetName() string {
    if m != nil && m.Name != nil {
        return *m.Name
    }
    return ""
}

func (m *Person) GetId() int32 {
    if m != nil && m.Id != nil {
        return *m.Id
    }
    return 0
}

func (m *Person) GetEmail() string {
    if m != nil && m.Email != nil {
        return *m.Email
    }
    return ""
}

아래는 Person 프로토컬 버퍼를 사용하는 예제 프로그팸이다.
package main

import (
    proto "github.com/golang/protobuf/proto"
    pb "github.com/yundream/test/person"
    "io/ioutil"
    "log"
)

func main() {
    phone := []*pb.Person_PhoneNumber{
        &pb.Person_PhoneNumber{Number: "0100000xxxx", Type: pb.Person_MOBILE},
        &pb.Person_PhoneNumber{Number: "0101111zzzz", Type: pb.Person_HOME},
    }
    addressBook := &pb.Person{
        Name:  "yundream",
        Id:    1234,
        Email: "yundream@gmail.com",
        Phone: phone,
    }
    out, err := proto.Marshal(addressBook)
    if err != nil {
        log.Fatalln(err)
    }
    if err := ioutil.WriteFile("test.data", out, 0644); err != nil {
        log.Fatalln(err)
    }
}
프로그램을 실행하면 test.data가 만들어진다.

test.data를 읽는 프로그램이다.
package main
                                
import (
    "fmt"
    "github.com/golang/protobuf/proto"
    pb "github.com/yundream/test/person"
    "io/ioutil"
    "log"
)

func main() {
    in, err := ioutil.ReadFile("test.data")
    if err != nil {
        log.Fatalln(err)
    }
    book := &pb.Person{}
    if err := proto.Unmarshal(in, book); err != nil {
        log.Fatalln(err)
    }
    fmt.Println(book.GetId())
    fmt.Println(book.GetName())
}
실행결과
# go run read.go 
1234
yundream

앞으로 할 것들

참고