Protobuf와 Thrift를 알아보자!

maketheworldwise·2022년 6월 19일
0


이 글의 목적?

우리는 일반적으로 JSON 형태의 데이터를 많이 활용하는데, JSON만을 이용하는 것이 정답이라고 할 수는 없다. JSON 대신에는 Thrift나 Protobuf를 이용하여 직렬 처리하는 경우도 많다고하니 이 두 개를 알아보자.

💡 직렬화란?

직렬화는 데이터를 파일로 저장하거나 네트워크 통신에 사용하기 위한 형식이며 바이트 스트림 형태로 변환하는 것을 의미한다.

RPC

Thrift와 Protobuf에 대해 알아보기 전에 RPC라는 녀석을 이해해야한다. RPC(Remote Procedure Call)는 원격에 있는 함수를 호출해주는 프로세스간 통신 기술이다. 플랫폼에 제약 없이 사용할 수 있어 분산 시스템 기법에 효과적이라고 한다.

RPC는 IDL(Interface Definition Language)를 사용하여 인터페이스를 명시하는데 대표적인 구현체는 다음과 같다.

  • Google의 Protobuf (grpc)
  • Facebook의 Thrift
  • Twitter의 Finalge

잘 이해가 되지 않으니 실제 RPC 동작 흐름을 살펴보자.

  1. 클라이언트에서 매개변수(Arguments)를 Stub에 전달한다.
  2. 클라이언트 Stub에서 매개변수를 메시지로 마샬링한다.
  3. 클라이언트 Stub는 메시지를 전송계층으로 전달하여 서버에 보낸다.
  4. 서버에서 메시지를 받아 Stub로 전달하고, 매개변수를 언마샬링하여 함수를 호출한다.
  5. 함수 수행이 완료되면 서버 Stub로 반환되며 반환값을 메시지로 마샬링한다.
  6. 결과 메시지를 클라이언트에 보내고 클라이언트닌 언마샬링 후 반환값을 호출자에게 반환한다.

💡 Stub 이란?

Stub 계층은 Stub 컴파일러가 IDL파일을 읽어 원하는 언어로 생성해주는 단계를 의미한다. 즉, 파라미터 객체를 메시지 형태로 마샬링 / 언마샬링하는 계층이다.

즉, 서버와 클라이언트는 다른 주소 공간을 사용하고 있기 때문에 함수호출에 사용되는 매개변수를 변환하는 작업이 필요하는데 이에 대한 처리를 담당하는 것이 Stubd이다.

💡 Marshalling, Unmarshalling 이란?

Marshalling은 데이터를 바이트로 쪼개어 TCP/IP 같은 통신 채널을 통해 전송될 수 있는 형태로 변환해주는 과정이고, Unmarshalling은 반대로 전송받은 바이트를 원래의 형태로 복원하는 과정을 의미한다.

추가적으로 MSA 구조의 서비스는 다양한 언어와 프레임워크로 개발되는데, 이처럼 다양한 환경으로 개발되는 구조에서 프로토콜을 맞춰 통신해야하는 비용이 발생하게 된다. 이 경우에 RPC를 이용하여 언어에 구애받지 않고 원격에 있는 프로시저를 호출하여 조금 더 비즈니스 로직에 집중하는 개발을 할 수 있다고 한다.

결국 내가 이해한게 맞다면, 클라이언트가 다양한 언어로 구축된 여러 서버와 통신하기 위한 기술이라고 보면 될 것 같다.

Protobuf

Protobuf는 Protocol Buffer로, 구글에서 오픈소스로 공개한 구조화된 데이터를 직렬화하는 방식을 의미한다. 내가 참고한 레퍼런스의 예제로 이해해보자.

하단의 JSON 데이터의 크기는 공백을 제거하면 총 82 바이트가 사용된다. (참고로, 나처럼 Favorite 스펠링이 불편하다고 생각할 수 있는데, 예제에서 사용된 Favourite 단어는 영국에서 사용하는 스펠링으로, Favorite과 동일한 의미의 단어라고 한다.)

{											# -> 1
    "userName": "Martin",					# -> 20
    "favouriteNumber": 1337, 				# -> 23
    "interests": ["daydreaming", "hacking"]	# -> 37
}											# -> 1

이를 Person 객체에 대한 Protobuf 스키마로 표현하면 다음과 같다.

message Person {
    required string user_name        = 1;
    optional int64  favourite_number = 2;
    repeated string interests        = 3;
}

그리고 이 스키마를 이용하여 데이터를 인코딩할 경우 33 바이트로 표현이 가능해진다고 한다. 여기서 핵심은, Proto 파일로 작성한 스키마에서 각 속성값을 필드 넘버로 대체했다는 점이다.

뭔가 감이 잡히긴하나, 확실하게 이해되지 않으니, 다른 레퍼런스의 예제를 가지고 와보자.

마찬가지로 하단의 JSON 데이터는 계산해보면 25 바이트를 사용한다.

{						# -> 1
    "name": "jinny",	# -> 15
    "age": 27 			# -> 8
}						# -> 1

이 JSON 데이터를 Protobuf 포맷으로 직렬화하면 다음과 같이 나온다.

message Person{
	string name = 1; //필드 태그
    int32 age = 2;
}

그리고 이를 하단의 이미지와 같이 인코딩된다. 최초 1 바이트중 5 비트는 필드 넘버를, 3 비트는 필드 타입을 나타낸다고 한다.

레퍼런스의 말을 그대로 가져와보면, Protobuf를 이용하면 직렬화와 역직렬화 속도가 빠르고 직렬화된 파일의 크기를 월등히 줄일 수 있어, 대용량 데이터를 처리할 때 성능이 더 좋다고 했다.

또한 JSON 처럼 사람이 읽기 쉽지는 않고 Proto 파일이 없으면 데이터 해석이 불가능하지만 스키마가 존재하여 쉽게 직렬화 역직렬화가 가능하기 때문에 내부 서비스에서 사용하면 효과적이라고 했다.

추가적으로 Protobuf의 흐름이 정리된 이미지도 많이 돌아다니는데, 이것도 함께 확인해보자.

다음과 같은 순서로 흐름이 이루어진다고 한다.

  1. .proto 파일에서 메시지 형식을 작성한다.
  2. protoc 컴파일러를 통해 원하는 언어로 컴파일한다.
  3. Protobuf를 사용하는 메인 목적은 데이터를 다른 곳에서 구문 분석을 할 수 있도록 직렬화하는 것이기 때문에 직렬화를 위해 인코딩(=마샬링)을 한다.
  4. 직렬화된 데이터를 사용하기 위해 디코딩(=언마샬링)을 한다.

Spring 프로젝트에 Protobuf를 적용하기 위한 예제는 하단의 링크에서 확인할 수 있다.

Thrift

Thrift는 앞에서 언급했듯 페이스북에서 개발한 RPC 프레임워크로, RPC의 특징을 그대로 가지고 있으며 오픈소스 아파치 프로젝트로 등록되어있다.

동일한 예제에서 이해해보자.

{											# -> 1
    "userName": "Martin",					# -> 20
    "favouriteNumber": 1337, 				# -> 23
    "interests": ["daydreaming", "hacking"]	# -> 37
}

그리고 이를 Thrift 스키마로 변경하면 다음과 같다.

struct Person {
  1: string       userName,
  2: optional i64 favouriteNumber,
  3: list<string> interests
}

Thrift 인코딩 방식에는 Binary, Compact 두 가지가 있다. Binary 방식은 간단하지만 인코딩을 하는데 59바이트가 필요하고, Compact 방식은 가변 길이 정수와 비트 패킹(?)을 이용하여 34 바이트로 줄여준다고 한다.

추가적으로 Thrift는 Protobuf에 비해 더 다양한 문법과 많은 언어를 제공해주고 있다.

이 글의 레퍼런스

profile
세상을 현명하게 이끌어갈 나의 성장 일기 📓

0개의 댓글