Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

  • 대대적인 수정 작업 중 .. 2019년 8월 20일

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에 비해서 가볍다.
  • 딱히 표준이라 할 만한 것들이 없다.
XML
  • 인간이 읽고 쓸 수 있다. : JSON 보다는 복잡하긴 하지만..
  • 스키마에 대한 정보 없이, 사용할 수 있다.
  • SOAP등의 표준이 있다.
  • xsd, xslt, sax, dom 과 같은 좋은 툴들이 많다.
  • JSON에 비해서 좀 장황하다.
Protocol Buffer
  • 데이터 압축률이 좋다.
  • 정보를 얻기 위해서는 스키마에 대한 정확한 지식이 필요하다.
  • 처리속도가 빠르다.
  • 이진 데이터라서 사람의 눈으로 확인할 수 없다.
개인적으로 최근 웹 애플리케이션 프로젝트에서 XML을 사용한 적이 없다. 인터넷에 공개되는 부분은 JSON, 내부처리는 PB 정도로 생각하고 있다.

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

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

  • Go에서 Gob는 데이터 구조화를 위한 좋은 솔류션이지만, 다른 언어로 작성된 응용 프로그램과 데이터를 공유해야 하는 경우에는 효과가 없다.
  • 문자열로 인코딩하는 방법도 있다. 예를 들어 "12:3:-23:67"과 같이 4개의 정수를 인코딩하는 방법을 개발 할 수 있다. 이는 애플리케이션 작성자들이 서로 인코딩 규칙을 알고 있어야 하긴 하지만 간단하고 유연한 접근 방식이다. 간단한 데이터를 인코딩하는데 적당하다.
  • XML로 직렬화 할 수 있다. XML은 사람이 읽을 수 있으며, 다양한 언어에서 사용 할 수 있기 때문에 매력적이다. 다른 응용 프로그램과 데이터를 공유해야 할 때 좋은 방법이다. 하지만 XML은 공간을 많이 사용하는 것으로 악명이 높으며, 인코딩/디코딩에 상대적으로 많은 리소스를 소비한다. 또한 XML DOM 트리를 탐색하는 것은 일반적인 클래스의 필드를 탐색하는 것보다 훨씬 복잡하다. XML에 대한 관심은 최근 크게 줄어들고 있다.
  • JSON은 의도적으로 간결함과 집중을 추구한 틀이다. 최근에는 복잡하고 구조화된 개념을 하나의 언어에서 단독으로 표현하는 것은 그다지 유용하지 않다는 인식이 확산되고 있다. JSON은 XML을 대신해서 확산되는 추세다.
프로토콜 버퍼는 효율적이며 자동화된 솔류션이다. 프로토콜 버퍼를 사용해서 데이터 구조에 대한 정보를 담고 있는 .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 사용 프로젝트들

  • Google : 구글에서 만든 녀석이니까.
  • ActiveMQ : 메시지 저장에 PB를 사용한다.
  • Netty (Protobuf-rpc)
  • Apache Camel

PB 지원 언어들

  • Java
  • C++
  • Python
  • Ruby
  • C
  • Go
  • Erlang
  • Javascript
  • Lua
  • Perl
  • PHP
  • R
  • Rust
  • Scala
  • Swift
  • Dart

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

앞으로 할 것들

  • GRPC 문서 다시 작성

참고