티스토리 뷰

구글 프로토콜 버퍼 (Google Protocol Buffer) 란?

Google에서 개발한 protocol buffer의 특징은 아래와 같습니다.

  • language-neutral
  • platform-neutral
  • extensible mechanism for serializing structed data

  쉽게 말하면 XML, Json등 과 같이 데이터를 저장하는 하나의 포맷이라고 할 수 있습니다. 하지만 가볍고, 빠르고, 그리고 사용하기에 쉽습니다. 사용법은 최초에 우리가 사용하고자 하는 데이터를 구조화하고, 사용하는 언어의 코드로 컴파일링을 하면 자동으로 코드가 생산됩니다. 자동으로 생성된 코드는 파일을 쓰고/읽는데 사용하면 됩니다. 구글 프로토콜 버퍼는 Java, Python, 그리고 C++을 지원하고 있습니다. proto3부터는 Go, JavaNono, Ruby, 그리고 C#까지 지원이 가능하다고 합니다.  

Proto 다운받고, 설치하기

  설치하기 위해서는 https://github.com/google/protobuf 에 접속하면 다운로드가 가능합니다. 
protocol compiler는 C++로 작성되어 있기 때문에 직접 설치가 가능합니다. 하지만 C++유져가 아니라면, pre-built binary를 통해서 설치가 가능합니다.
[pre-built binary 다운받기]

Proto 포맷 정의하기

  처음에는 사용하고자 하는 데이터들을 구조화 하는 작업이 필요합니다. 하나의 틀 이라고 생각하시면 됩니다. 이 정의된 틀을 통해 proto는 각 언어에 맞는 코드를 자동으로 생성을 해줍니다. 이렇게 생성된 코드는 쓰기/읽기에 사용이 가능합니다. 문법은 약간 C++, Java와 비슷한 형태를 띄우고 있습니다. 

  예를들어서 addressbook.proto를 생성합니다. 

package tutorial;
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";

message Person {

  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

  코드에 대해서 간단하게 설명을 드리면, .proto의 시작은 package로 정의 해야 합니다. 그 이유는 다른 프로젝트들간에 충돌을 막기 위해서 입니다. 위 예제는 java이기 때문에 java_package, java_outer_classname을 정의해 줍니다. 결과적으로 proto로 컴파일링을 하게 되면  /PROJECT_ROOT/com/example/tutorial/AddressBookProtos.java의 파일이 생성됩니다. 

  변수 옆에 붙는 숫자는 "tag"로 binary encoding할때 사용되는 필드입니다. (중복이 되면 안됩니다.) 변수 앞에 붙는 필드 required, optional, repeated

  • required는 항상 값이 요구되는 값으로, 없으면 에러가 발생합니다. 
  • optional은 값이 있어도 되고, 없어도 되는 값을 말합니다. 값을 넣지 않으면 default값으로 해당 filed의 default값이 들어가게 됩니다. 
  • repeated는 배열이라고 생각하시면 됩니다.

 위 예제를 풀어 설명하면 AddressBook에는 여러명의 Person이 저장될 수 있고, Person에는 여러개의 PhoneNumber가 들어갈 수 있는 데이터 구조입니다. 또한 PhoneNumber에는 number라는 string이 항상 필요하고, PythoneType의 값은 설정을 안해도 됩니다. 설정을 안하면 default로 HOME이라는 1이 들어갑니다. 

Proto 컴파일링

이렇게 만들어진 .proto는 설치된 protoc를 통해서 컴파일링을 하고 결과적으로 com/example/tutorial/의 경로에 AddressBookProtos.java의 파일이 생성이 됩니다. 

$ protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto 


만약 여기서 python의 코드로 생성을 하려고 한다면 아래와 같이 컴파일링하고 결과적으로 
.py의 파일이 생성이 됩니다. 

$ protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto


  생성된 코드를 사용해 쓰기/읽기 하는 방법 (자동으로 생성된 코드는 수정을 하지 않습니다.) 생성된 AddressBookProtos.java의 파일을 보면 AddressBookProtos라는 class가 생성이 되어있습니다. 각 class마다 자신의 Builder의 클래스를 갖습니다. 이 클래스는 instance를 생성하는데 사용하게 됩니다. message의 경우에는 오직 getters만 갖고 있고, builders의 경우에는 getters와 setters를 갖고 있습니다. 

Person

public boolean hasName();
public String getName();

// required int32 id = 2;
public boolean hasId();
public int getId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();

// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<phonenumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);

Person.Builder

// required string name = 1;
public boolean hasName();
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName();

// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();

// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<phonenumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
public Builder setPhone(int index, PhoneNumber value);
public Builder addPhone(PhoneNumber value);
public Builder addAllPhone(Iterable<phonenumber> value);
public Builder clearPhone();

PhoneType은 Person의 nested로 자동 생성 됩니다.

public static enum PhoneType {
  MOBILE(0, 0),
  HOME(1, 1),
  WORK(2, 2),
  ;
  ...
}

Builder vs Message

  message는 protocol buffer의 compiler에 의해 immutable하게 생성되는 클래스 입니다. 즉, 한번 객체가 생성이 되면 수정이 불가능 합니다. (java의 String처럼) message를 구성하기 위해서는 첫번째로 builder를 만들어야 합니다. 그 다음 값들을 set, add를 한뒤에 build()의 함수를 통해 만들 수 있습니다. 

Person john =
  Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .addPhone(
      Person.PhoneNumber.newBuilder()
        .setNumber("555-4321")
        .setType(Person.PhoneType.HOME))
    .build();

Message의 Methods

  • isInitialized() : required 필드가 모두 세팅이 되었는지, (만약에 required 필드가 하나라도 누락되면 에러를 발생시킵니다.)
  • toString() : debugging의 사용에 유용합니다. print문으로 값을 확인하고자 할때 사람이 일을 수 있는 표현의 메세지를 리턴합니다.
  • mergeFrom(Message other) : builder에게 있는 메소드로, 다른 message와 통합하는 것을 말합니다. (singular filed는 overwriting 되고, repeats는 concatenating됩니다)
  • clear() : builder에게만 있고 모든 필드르 empty state로 합니다. 

Parsing and Serialization

protocol buffer 클래스는 message를 binary format으로 읽고 쓰는 메소드를 갖고 있습니다. 

  • byte[] toByteArray(): message를 serialize하고, byte array에 포함되어 있는 raw bytes를 리턴하는 메소드
  • static Person parseFrom(byte[] data) : 주어진 byte array로 부터 message를 parses하는 메소드
  • void writeTo(OutputStream output): message를 serialize하고, OutputStream으로 쓰는 메소드
  • static Person parseFrom(InputStream input): InputStream으로 부터 메시지를 read, parse를 하는 메소드

Writing A Message

import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;

class AddPerson {
  // This function fills in a Person message based on user input.
  static Person PromptForAddress(BufferedReader stdin,
                                 PrintStream stdout) throws IOException {
    Person.Builder person = Person.newBuilder();

    stdout.print("Enter person ID: ");
    person.setId(Integer.valueOf(stdin.readLine()));

    stdout.print("Enter name: ");
    person.setName(stdin.readLine());

    stdout.print("Enter email address (blank for none): ");
    String email = stdin.readLine();
    if (email.length() > 0) {
      person.setEmail(email);
    }

    while (true) {
      stdout.print("Enter a phone number (or leave blank to finish): ");
      String number = stdin.readLine();
      if (number.length() == 0) {
        break;
      }

      Person.PhoneNumber.Builder phoneNumber =
        Person.PhoneNumber.newBuilder().setNumber(number);

      stdout.print("Is this a mobile, home, or work phone? ");
      String type = stdin.readLine();
      if (type.equals("mobile")) {
        phoneNumber.setType(Person.PhoneType.MOBILE);
      } else if (type.equals("home")) {
        phoneNumber.setType(Person.PhoneType.HOME);
      } else if (type.equals("work")) {
        phoneNumber.setType(Person.PhoneType.WORK);
      } else {
        stdout.println("Unknown phone type.  Using default.");
      }

      person.addPhone(phoneNumber);
    }

    return person.build();
  }

  // Main function:  Reads the entire address book from a file,
  //   adds one person based on user input, then writes it back out to the same
  //   file.
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage:  AddPerson ADDRESS_BOOK_FILE");
      System.exit(-1);
    }

    AddressBook.Builder addressBook = AddressBook.newBuilder();

    // Read the existing address book.
    try {
      addressBook.mergeFrom(new FileInputStream(args[0]));
    } catch (FileNotFoundException e) {
      System.out.println(args[0] + ": File not found.  Creating a new file.");
    }

    // Add an address.
    addressBook.addPerson(
      PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
                       System.out));

    // Write the new address book back to disk.
    FileOutputStream output = new FileOutputStream(args[0]);
    addressBook.build().writeTo(output);
    output.close();
  }
}

Reading A Message

import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;

class ListPeople {
  // Iterates though all people in the AddressBook and prints info about them.
  static void Print(AddressBook addressBook) {
    for (Person person: addressBook.getPersonList()) {
      System.out.println("Person ID: " + person.getId());
      System.out.println("  Name: " + person.getName());
      if (person.hasEmail()) {
        System.out.println("  E-mail address: " + person.getEmail());
      }

      for (Person.PhoneNumber phoneNumber : person.getPhoneList()) {
        switch (phoneNumber.getType()) {
          case MOBILE:
            System.out.print("  Mobile phone #: ");
            break;
          case HOME:
            System.out.print("  Home phone #: ");
            break;
          case WORK:
            System.out.print("  Work phone #: ");
            break;
        }
        System.out.println(phoneNumber.getNumber());
      }
    }
  }

  // Main function:  Reads the entire address book from a file and prints all
  //   the information inside.
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage:  ListPeople ADDRESS_BOOK_FILE");
      System.exit(-1);
    }

    // Read the existing address book.
    AddressBook addressBook =
      AddressBook.parseFrom(new FileInputStream(args[0]));

    Print(addressBook);
  }
}

[참고 사이트]
https://developers.google.com/protocol-buffers/

언어별 튜토리얼
https://developers.google.com/protocol-buffers/docs/tutorials

개발자 가이드 문서
https://developers.google.com/protocol-buffers/docs/overview

언어별 API 문서
https://developers.google.com/protocol-buffers/docs/reference/overview

예제 코드 
https://github.com/google/protobuf/tree/master/examples

댓글
댓글쓰기 폼