말로만 듣던 Protobuf, 드디어 써봤습니다.

문지은·2023년 12월 10일
2

도입하게 된 배경

자바를 통해서 api 작업을 한다면 직렬화, 역직렬화는 매우 중요한 포인트 중 하나이다. 데이터를 주고 받거나 데이터를 저장할 때처럼 다양한 상황에 꼭 필요한 요소이기도 하면서 비용이 정말 큰 프로세스이기도 하기 때문이다. 하지만 우리는 대부분의 경우 작업의 편의성을 위해서 따로 데이터를 해석할 필요가 없는 JSON같은 문자열 데이터를 쓰곤 한다.

우리 팀도 JSON을 이용해서 자바 직렬화/역직렬화를 사용하고 있었다. 그러던 중 매우 크기가 큰 데이터를 주기적으로 주고 받는 api를 구현해야할 상황이 생겼다. (리스트의 크기 약 2000개 이상…) 그리고 한 팀원분이 protocol buffers(이하 protobuf) 를 쓰는게 어떻냐는 제안을 하면서 시작됐다.

Protobuf란?

protobuf란 이진 직렬화 프로토콜 중 하나로 구글에서 개발한 언어 및 플랫폼 중립적인 직렬화 메커니즘이다. 구조화된 데이터를 다루는 데에 사용된다. 자바 외에도 C++, C#, Dart, Go, Kotlin, Objective-C, Python 및 Ruby 등에서도 사용 가능하다.

Protobuf를 사용하는 이유

프로토콜 버퍼는 효율적인 이진 직렬화로 인해 대량의 데이터 처리에 강하다. 같은 이진 직렬화를 사용하는 Apache Avro와 비교해도 일반적으로 높은 압축률과 성능을 보여준다.

  • 효율적인 이진 직렬화
    • 프로토콜 버퍼는 이진 형식으로 데이터를 직렬화하므로 데이터 크기가 작아서 네트워크 존성 및 디스크 저장에 효율적이다.
    • 이진 직렬화로 인해 프로토콜 버퍼는 직렬화 및 역직렬화 과정에서 빠른 성능을 가진다.
  • 명시적인 스키마 정의
    • 스키마를 사용하여 데이터 구조를 명시적으로 정의하므로 가독성이 높고 버전 관리가 쉽다.
  • 자동 코드 생성
    • 프로토콜 버퍼 컴파일러를 사용하여 스키마 정의로부터 자동으로 코드를 생성할 수 있다.

자바 스프링 환경에서의 튜토리얼

  1. 프로토버프 관련 라이브러리 추가
implementation 'com.google.protobuf:protobuf-java:3.24.3'
  1. 주고 받을 proto 파일 작성
syntax = "proto3";

// 해당 proto 파일로 컴파일된 자바 파일이 위치할 곳
option java_package = "com.example.tutorial.person";

message Request {
    repeated Person positions = 1;
}

message Person {
     optional string name = 1;
     optional int32 id = 2;
     optional string email = 3;
}
  1. Proto 파일 컴파일
brew install protobuf

protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/person.proto
  1. Spring message converter 에 protobuf message converter 추가
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
    @Bean
    public MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter() 
    
    @Override
    protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new Jackson2HttpMessageConverter());
        converters.add(new ProtobufHttpMessageConverter());
        super.addDefaultHttpMessageConverters(converters);
    }
  1. Protobuf로 데이터를 주고 받을 코드 작성
  • Protobuf로 api 호출
private void callProtobuf(PathRequest request, boolean compression) {
        RestTemplate restTemplate;
        HttpHeaders headers = new HttpHeaders();
        
        restTemplate = new RestTemplate(List.of(new ProtobufHttpMessageConverter()));
        headers.setContentType(ProtobufHttpMessageConverter.PROTOBUF);
        

        HttpEntity<PathRequest> httpEntity = new HttpEntity<>(request, headers);
        restTemplate.exchange("http://localhost:8080/test/protobuf", HttpMethod.POST, httpEntity, Void.class);
    }
  • Protobuf를 받는 api
    @SneakyThrows
    @PostMapping(value = "/test/path/protobuf", consumes = {"application/x-protobuf"})
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void pingPong(@RequestBody PathRequest request) {
        log.info("[protobuf] recved request len: {} ", request.getSerializedSize());
    }
  • Docker를 이용해서 배포할 경우
    Dockerfile base image 에 protoc가 포함된 이미지를 설정해야 한다. 그렇게 하면 proto 파일을 따로 컴파일하거나 컴파일된 파일을 넣어두지 않아도 배포하면서 Dockerfile이 실행될 때 이미지 빌드가 이뤄진다. 이 때 proto 파일들이 자동 컴파일되며 option java_package로 지정했던 경로에 생성된다.

내가 써본 후기

좋았던 점

  • 러닝 커브가 높지 않다.
    • 공식 홈페이지의 tutorial을 보면서 충분히 따라할 수 있는 난이도였다.
  • Json과 비교했을 때 압축률이 확연히 높다.
    • 내가 사용했던 예시에서 주고 받은 데이터의 크기는 다음과 같다.
    • Json : 304kb, Protobuf : 164kb

    아쉬웠던 점

  • 데이터 스키마가 명시적이라서 한번 정해지면 바꿀 때 호출하는 곳, 호출받는 곳 모두 작업이 필요하다.
  • 주고 받은 데이터가 이진 형태로 보여서 확인하기 어렵다.
    • 로그도 이진 형태로 찍혀서 es 설정 코드를 수정해야 했다.
    • 주고 받은 데이터를 사람이 읽을 수 있는 문자열 형태로 확인할 때 실제로 코드를 실행 한 뒤 디버깅해서 진행했다.
  • 테스트가 간편하지 않다.
    • 간단하게 cURL을 이용해서 통합 테스트를 하고 싶을 때도 protobuf 형태로 데이터를 전송하도록 해야하기 때문에 protobuf 용 curl인 protoCURL을 사용하거나 postman에 추가 설정을 해야했는데 이것이 불편해서 직접 호출하는 곳의 어플리케이션을 실행해서 테스트했다.
profile
백엔드 개발자입니다.

2개의 댓글

comment-user-thumbnail
2024년 2월 9일

저는 아직도 못 써봤는데, 말로만 듣던 protobuf를 드디어 써보셨군요
내용을 자세하게 잘 정리해 놓으셔서 실제 사용하는것처럼 느껴질 정도네요
특히 후기의 좋았던점, 아쉬웠던점이 실제 적용해본 사람의 느낌이 있어서 좋았네요

그리고 아쉽지만 여기도 옥의티가 하나 있네요
"네트워크 존성" --> "네트워크 의존성" 인듯 하네요

1개의 답글