직렬화 방법, 구글의 Protocol Buffers

직렬화란 메모리안의 구조적인 데이타를 IO를 통해 타 프로세스로 보내거나 저장하기 위해 연속된 bit로 만드는 과정이다. c의 구조체나 클래스의 인스턴스와 같이 구조적인 혹은 계층적인 데이타를 iO를 통해 내보내려면 한줄로 쭉 정렬해야 합니다. 이런 한 줄로 쭉 세우는 과정이 직렬화이고, 반대의 작업이 역직렬화이다. 외부의 프로세스와 통신을 하거나 데이타를 파일로 저장을 한다고 하려면 어떤 방식이건 간에 직렬화를 해야 한다. 소켓 통신에서 메시지의 전문 포멧을 정하는 것이나, xml로 변환하거나, java의 Serializable이나, c의 structure 메모리 매핑을 그대로 보내는 것들 전부가 직렬화 방법들이다. 각 방법마다 장단점이 있다. 속도, 다른 OS간 통신, 다른 언어간 통신, 직렬화된 데이타의 사이즈, 개발 생산성, 개발 편의성, 유지보수성 정도가 고려되는 항목이다. xml의 경우 언어에 관계없고, 프로세스가 동작하는 OS에도 관계없지만, 직렬화/역직렬화의 속도가 떨어지고 직렬화된 데이터 사이즈가 크다는 단점이 있다. 데이타 전문의 경우 속도는 빠르지만 직렬화/역직렬화 구현을 전부 손으로 해야하기 때문에 개발편의성이 좋지 않고, 유지보수가 쉽지 않고, 버그를 찾기가 어려운 단점이 있다. c structure의 메모리 매핑은 메모리의 내용을 그대로 사용하는 것인데, OS만 달라도 사용이 불가하고, 물론 타 언어간의 통신은 불가하다. 대신 별도의 직렬화작업이 없는 이유로 속도는 가장 빠르다.

Protocol Buffers는 구글에서 사용하는 직렬화 방법이다. 구글 내에 많은 시스템이 있고, 당연히 다양한 OS들이 있고, 다양한 언어가 사용되는 환경에서 서로 통신하기 위한 직렬화 방법으로 개발한 것을 공개한 것이다. 라이센스는 new BSD이다. c structure 정의와 비슷한 형태로 데이타 구조를 정의하고, 컴파일러를 사용하여 c++과 java의 소스코드를 자동생성한다. 어플이케이션에서는 이 코드를 사용하여 값을 설정하고 직렬화하고 조회한다. 다음과 같은 장점을 가지고 있다.

    - 속도 빠르고(xml에 비해 10~100배)
    - 데이타 사이즈 작고(xml에 비해 3~10배)
    - 코드 더 간단하고
    - 개발 쉽고


Subtype.proto 파일에 다음과 같이 메시지를 정의하고

package Subtype;
message Root {
    optional sint64 id = 1;
    optional string name = 2;
    optional Some some = 3;
}
message Some {
    optional sint64 someId = 1;
    optional string someName = 2;
    optional Some some = 3;
}



콘솔에서 다음과 같이 컴파일 한다.

protoc -cpp_out=. Subtype.proto    // cpp 코드 생성
protoc -java_out=. Subtype.proto    // java 코드 생성

컴파일 하면 Subtype.pb.cc, Subtype.pb.h, Subtype.java 파일이 생성된다.



다음은 c++ 어플리케이션에서 생성된 코드를 사용하는 코드이다.

Subtype::Root* root = new Subtype::Root();    // 자동 생성된 코드에 정의된 클래스.

root->set_id(1);
root->set_name("Tom");

Subtype::Some* sub1 = root->mutable_some();
sub1->set_someid(2);
sub1->set_somename((xc8*)"Jerry"); 

Subtype::Some* sub2 = sub1->mutable_some();
sub2->set_someid(3);
sub2->set_somename((xc8*)"Brute"); 

// 인코딩된 메시지 사이즈를 얻는다. (인코딩은 하기 전인데도.)
int encodedSize = root->ByteSize();

// 인코딩된 데이터가 담길 버퍼
char* encoded = (xuc8*)malloc(encodedSize);

// 인코딩 하고
int rtn = root->SerializeToArray(encoded, encodedSize);
if(!rtn) { print(“encoding failed\n”); }

delete message; message = NULL;

// 디코딩해서 담을 빈 메시지를 하나 만들고
Subtype::Root* newRoot = new Subtype::Root();

// 디코딩
rtn = newRoot ->ParseFromArray(encoded, encodedSize);
if(!rtn) { printf(“decoding failed\n”); }

free(encoded);


printf("Root.id = %d\n", newRoot ->id());
printf("Root.name = %s\n", newRoot ->name());

printf("Root.Some.someId = %d\n", newRoot ->some().someid());
printf("Root.Some.someName = %s\n", newRoot ->some().somename());

printf("Root.Some.Some.someId = %d\n", newRoot ->some().some().someid());
printf("Root.Some.Some.someName = %s\n", newRoot ->some().some().somename());

delete newRoot ; newRoot = NULL;



다음은 java 어플리케이션에서의 사용 코드이다.

Subtype.Root.Builder root = Subtype.Root.newBuilder();    // 자동 생성된 코드에 정의된 클래스

root.setId(1);
root.setName("Tom");

Some.Builder sub1 = Some.newBuilder();
sub1.setSomeId(2);
sub1.setSomeName("Jerry");

Some.Builder sub2 = Some.newBuilder();
sub2.setSomeId(3);
sub2.setSomeName("Brute");

sub1.setSome(sub2);

root.setSome(sub1);


// 인코딩된 데이터가 담길 버퍼
byte[] encoded = null;


// 인코딩 하고
try {
    encoded = root.build().toByteArray();
} catch(UninitializedMessageException e) {
    System.out.println(“encoding failed. e=“+e);
}


// 디코딩해서 담을 빈 메시지를 하나 만들고
Subtype.Root newRoot = null;

// 디코딩
try {
    newRoot = sample.Subtype.Message.Root.parseFrom(encoded);
} catch(InvalidProtocolBufferException e) {
    System.out.println("decoding failed. e="+e);
}


println("Root.id = "+newRoot .getId());
println("Root.name = "+newRoot .getName());

println("Root.Some.someId = "+newRoot .getSome().getSomeId());
println("Root.Some.someName = "+newRoot .getSome().getSomeName());

println("Root.Some.Some.someId = "+newRoot .getSome().getSome().getSomeId());
println("Root.Some.Some.someName = "+newRoot .getSome().getSome().getSomeName());


기타 사항은 다음과 같다.

    - 기본적으로 c++, java, python을 지원한다.

    - AddOn에 의해 다음언어가 가능하다. Action Script, c, c#, Java ME, Javascript, Objective C, Perl, Php, Ruby, Visual Basic, Clojure, Common Lisp, D, Erlang, Haskell, Mercury, R

    - 메시지 말고 RPC 인터페이스를 proto 파일에 정의하고 stub 코드가 자동생성된다.

    - AddOn에 의해 RPC 구현체의 자동생성이 가능하다.

    - 패킷캡쳐 프로그램인 wireshark의 플러그인 존재

    - NetBeans IDE의 플러그인 존재

    - 컴파일러의 소스로 제공. linux, Unix는 빌드필요. windows의 경우 실행파일 protoc.exe만 따로 제공.

    - java의 경우 필요한 jar파일을 maven repository에서 다운받을 수 있다.

    - 메시지의 필드는 태그값(optional sint64 id = 1; 정의의 1)에 의해 관리된다. 

    - 필드의 값 설정의 횟수의 타입은 required, optional, repeated 세가지가 있다. required는 반드시 한번, optional은 0 또는 1회, repeated는 횟수 제한없다.

    - 필드 타입은 double, float, bool, string, bytes와 정수형 관련 int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64들이 있다.

    - java의 경우 생성될 코드의 패키지와 클래스 이름의 개별 지정이 가능하다.

    - c++의 경우 네임스페이스의 정의가 가능하다.

    - nested 메시지 가능.

    - 타 파일에 정의한 메시지를 import 가능.

    - 컴파일 시 성능 최적화, 크기 최적화가 가능하고(메시지별) 일부 기능을 제외한 lite 버전이 가능하다.

    - 필드의 메타데이타를 사용하기 위한 reflection, descriptor 지원.

    - 정의한 메시지가 수정된되더라도 하위 호환이 보장된다. 즉 모든 시스템을 업그레이드하지 않아도 된다. 이쪽 시스템에서 새로운 메시지로 보냈을 때 저쪽에서는 기존의 필드는 처리하고, 모르는 필드는 무시한다. 

    - java에서는 각 메시지 마다 read only의 Message와 read & write의 Builder 두개가 있다. immutable인 String과 같이 성능상의 이유인 듯.


성능

테스트 환경은 Linux 2.6.18 Ubuntu, Intel Xeon X5460 @ 3.16GHz, memory 8G, java 1.5였다.
싱글 thread로 테스트 하였으며, java 컴파일 시 비디버깅모드로 하였다.
Protocol Buffers의 버번은 2.3.0.

측정된 값은 인코딩된 데이타 사이즈, 인코딩 시간, 디코딩 시간이었으며, 인코딩 시간은 자동생성된 코드에 정의된 객체를 생성하고 직렬활를 하는 시간이며, 디코딩 시간은 역직렬활를 하여 객체를 받기 까지의 시간이다.

두가지 데이타가 사용되었으며, 이 중 짧은 데이타는 다음과 같다.(Json으로 표현)

{
    "result":       100,
    "vcID": 9,
    "srcCSAID":     1,
    "targetCSAID":  2,
    "css":  [{
        "srcCSID":      1,
        "newCSID":      2
     }],
    "legs": [{
        "srcCSID":      1,
        "newCSID":      2,
        "srcLegID":     1,
        "newLegID":     3
     }, {
        "srcCSID":      1,
        "newCSID":      2,
        "srcLegID":     2,
        "newLegID":     4
    }]
}


인코딩된 메시지 사이즈는 짧은 메시지가 34byte, 긴 메시지가 1234 byte였다.
c++은 초당 130만회, 11만회 직렬화와 역직렬화를 하였다.
java는 초당  87만회, 6만회 직렬화와 역직렬화를 하였다.

같은 조건에서 cJSON, Pxml2를 사용한 결과를 비교하면 Protocol Buffers가 100배, 10배 빠르다. 메지시 사이즈는 2배, 1.6배 짧다.






Reference

Protocol Buffers home
    http://code.google.com/intl/ko-KR/apis/protocolbuffers/
Third-Party Add-ons for Protocol Buffers
    http://code.google.com/p/protobuf/wiki/ThirdPartyAddOns
Protocol Buffers c++ API
    http://code.google.com/intl/ko-KR/apis/protocolbuffers/docs/reference/cpp/index.html
Protocol Buffers Java API
    http://code.google.com/intl/ko-KR/apis/protocolbuffers/docs/reference/java/index.html

by 어플로잇 | 2010/03/30 11:47 | IT | 트랙백 | 핑백(3) | 덧글(5)
트랙백 주소 : http://aploit.egloos.com/tb/5233561
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Linked at 저장소 : 직렬화 방법, Fa.. at 2010/03/30 12:19

... cebook의 직렬화 방법이다. 2010년 3월 현재 apache의 인큐베이팅 프로젝트로 등록되어 있다. 전 구굴의 개발자가 만들었다고 하고 Protocol Buffers와 무척 유사하다. 하며, 다음과 같은 차이점이 있다.- java API가 좀더 깔끔하다. - 별도의 setter, ... more

Linked at flexible gameser.. at 2010/12/03 15:26

... ews.oreilly.com/2008/07/interview-google-open-sources.html java예제도 다루고 있는 참고 링크 http://aploit.egloos.com/5233561 ... more

Linked at Protocol Buffers.. at 2012/02/18 13:48

... ruary 9, 2012 by ychang http://code.google.com/apis/protocolbuffers/docs/tutorials.html http://aploit.egloos.com/5233561 This entry was posted in General and tagged Data, Protocol Buffers by ychang. Bookm ... more

Commented by kay at 2011/04/15 17:31
안녕하세요! protocol buffers를 사용해보려고 하는 학생입니다 !

현재 여러 글들을 보면서 c++로는 샘플코드를 만들었는데요 ~ c#은 자료가 참 어렵네요 ㅜㅜ .. 정작 필요한 것이 c#이라서요 ..

영어에 무지한지라 ㅜㅜ .. 거의 영어로 되있어서 해석을 해도 참 .. 힘드네요 ㅜㅜ 혹시 방법 알고 계시면 도움 좀 부탁드립니다 !!
Commented by 어플로잇 at 2011/04/18 00:51
저도 c#으로 테스트 해보진 않았고, 또 c#에 대한 문서가 그렇게 눈에 띄지도 않았었던 것 같은 기억입니다. 그래서 직접적으로 도움이 되드리지 못해서 유감이네요. c든 java든 그리고 c#이든 그 기본적인 목적과 사용방법은 유사할 것입니다. 언어가 다를 뿐이고. 차라리 c보다는 java의 것과 더 유사하지 않을까 추측해 봅니다.


그리고 딴 얘기인데, 개발자를 업으로 하려면 기술 문서를 읽기 위한 영어는 필수 입니다. 대화나 writing은 몰라도 원하는 내용을 찾고 내용 파악하는 데는 문제 없어야 합니다. 아주 보편적인 사항을 아닌 경우, 문제 해결을 위한 문서는 거의다 영어로 되어 있습니다. 영어는 필수 입니다.

역시 도움이 안되는 얘기였네요.

댓글 남겨 주셔서 감사합니다.
Commented by kay at 2011/04/18 09:28
정말 영어 꼭 배워야겠네요 ! ㅜㅜ 졸업이 얼마 안남아 현실을 직시했네요 .. 무튼 감사합니다 ^^
Commented by 강용균 at 2017/02/15 14:32
Subtype::Some* sub1 = root->mutable_some();
root->set_someid(2);
root->set_somename((xc8*)"Jerry");

Subtype::Some* sub2 = sub1->mutable_some();
root->set_someid(3);
root->set_somename((xc8*)"Brute");

위 부분에 버그 인것 같네요. 아래와 같이 고쳐야 하지 않을까요?
Subtype::Some* sub1 = root->mutable_some();
sub1->set_someid(2);
sub1->set_somename((xc8*)"Jerry");

Subtype::Some* sub2 = sub1->mutable_some();
sub2->set_someid(3);
sub2->set_somename((xc8*)"Brute");
Commented by 어플로잇 at 2017/06/25 15:45
수정했습니다. 감사합니다. ^^

:         :

:

비공개 덧글

< 이전페이지 다음페이지 >