2020년 3월 4일 수요일

[Java] 프로토콜 버퍼 (Protocol Buffers) 기초

아래의 내용은 Java 언어를 이용해 프로토콜 버퍼를 사용하는 것에 대한 내용이다.

자세한 내용은 아래 링크를 참고하시면 됨.
https://developers.google.com/protocol-buffers/docs/javatutorial

프로토콜 버퍼는 직렬화 라이브러리로 프로그래밍 언어를 통해 만들어진 데이터를 bytes로 변환해 준다. 프로토콜 버퍼는 XML, JSON 방식보다 더 작고(smaller data) 빠르게 동작한다. 데이터를 텍스트가 아니라 bytes로 변환하기 때문이다. 알려진 내용에선 프로토콜 버퍼가 텍스트 포맷 송수신 대비 10배 적은 용량을 사용하면서도 100배 빠르게 동작한다고 한다.

게다가 사용자가 하기 싫어하는 지저분한 작업들을 프로토콜 버퍼가 대신 해준다. 직렬화/역직렬화는 물론이고 enum과 문자열 조작에 대한 것들이 이러한 작업이다. 또한 서로 다른 언어간 데이터를 주고 받을 때 발생하는 문제점들도 프로토콜 버퍼가 해결해준다. 사실 이게 가장 강력한 기능일 것이다.

튜토리얼에 따르면 프로토콜 버퍼를 사용하기 위해선 아래의 3가지를 알아야 한다고 되어 있다.

• .proto 파일을 정의하는 방법.
• 프로토콜 버퍼 컴파일러를 이용해 .proto 파일을 .java 파일로 변환하는 방법
• 프로토콜 버퍼의 Java API로 메시지를 읽고 쓰는 방법.

프로토콜 버퍼를 사용하는 이유는 다음과 같다.

• 직접 직렬화 라이브러리를 작성해야 하는 경우 또는 빌트-인 직렬화 기능보다 더 나은 솔루션이 필요한 경우
• 직렬화된 객체를 각기 다른 언어로 작성된 프로그램에서 읽어서 사용해야 하는 경우

* 물론 "단순한 객체를 단일 언어에서" 간단하게 객체를 읽고 쓰기 위함이라면 (임시로) 자체적인 로직을 작성하거나 XML/JSON 등의 텍스트 형식으로 이를 처리할 수 있을 것이다.


□ .proto 파일을 정의하는 방법.


프로토콜 버퍼의 프로토콜 포맷은 .proto 확장자를 이용한다.

C의 struct 정의와 유사한 문법으로 메시지 형식을 정의하는데 message 키워드를 이용해 각각의 데이터 스트럭쳐를 정의할 수 있다. 이렇게 만들어진 .proto 파일은 프로토콜 버퍼 컴파일러를 통해 타겟 언어의 소스 코드로 변환된다. 그리고 각자 작성하는 프로그래밍 언어에서 이 파일을 참조해 목적에 맞는 작업을 하면 된다.

message A { message B { ... } } 식의 nested 방식을 지원하며 bool, int32, float, double, string등의 익숙한 자료형을 쓸 수 있다. enum을 지원하며 repeated 키워드를 이용해 리스트 형식을 표현할 수 있다.

* Java 언어인 경우 package, java_package 옵션을 이용해 이름 충돌이 발생하지 않도록 패키지 이름을 정의해주어야 한다.

필드엔 반드시 값이 지정되어야 한다는 의미의 required와 값이 지정되지 않아도 된다는 의미의 optional이 위치하게 된다. 튜토리얼에 의하면 required는 가급적 사용하지 않는 것이 좋다고 한다. 메시지 구조의 유연함을 떨어뜨리는 요인으로 나중에 가서 하위 호환성을 지키기 어려워지기 때문이다.

마지막으로 한가지, 모든 필드엔 " = 1", " = 2" 식의 숫자 태그를 붙여주어야 한다. 필드를 직렬화할 때 정보를 컴파일러에게 알려주는 일을 하는데 하위 호환성을 제공하려면 한번 부여한 숫자는 절대로 변경해선 안된다. 태그가 변경되면 바이너리 포맷이 틀어지기 때문이다.


□ 프로토콜 버퍼 컴파일러를 이용해 .proto 파일을 .java 파일로 변환하는 방법


protoc.exe가 제공되는데 이를 가지고 타겟 프로그래밍 언어로 프로토콜 포맷에 대한 소스코드를 생성할 수 있다.
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/프로토콜포맷파일명


□ 프로토콜 버퍼의 Java API로 메시지를 읽고 쓰는 방법.


필드에 대한 getter(), setter()는 물론이고 빌더-패턴을 제공하기 때문에 객체에 값을 지정하고 읽는 것엔 큰 어려움이 없을 것이다.

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

객체를 읽고 쓰는덴 아래의 4가지 API가 제공된다.

byte[] 버전과,
  • byte[] toByteArray();
  • static Person parseFrom(byte[] data);

스트림 버전이 있다.
  • void writeTo(OutputStream output);
  • static Person parseFrom(InputStream input);

추가로, 통신으로 객체를 주고 받을 수도 있는데 Netty와 궁합이 좋다.
디코딩 단계에선 ProtobufVarint32FrameDecoderProtobufDecoder를,
인코딩 단계에선 ProtobufVarint32LengthFieldPrependerProtobufEncoder를 사용하면 된다.
사용자의 입장에선 데이터 프레임의 길이만 앞에 붙여서 서로 주고 받으면 되는 것이다.


댓글 없음:

댓글 쓰기