Protocol Buffer와 인코딩/디코딩 그리고 gRPC 내부 통신 동작 원리

2025. 4. 7. 00:15·gRPC

 

gRPC는 JSON이나 XML과 같은 텍스트 형식을 사용하는 대신 Protocol Buffer 기반 바이너리 프로토콜을 사용하므로 훨씬 프로세스 간 통신에 효율적이다.

 

이러한 점 외에도 여러 가지 장점들이 존재하는데, 개발자로써 그렇구나 하고 넘어가는 것보다는

정의한 .proto 파일은 어떻게 직렬화되고, gRPC는 이를 내부적으로 어떻게 처리할까?라는 궁금증들을 해소하고 싶었다.

 

그래서 이번 글에서는 단순히 사용법이 아니라, 좀 더 들어가서 내부적으로 저수준의 동작 원리들에 대해 공부한 내용들을 기록하고자 한다.

 

Protocol Buffer

프로토콜 버퍼(Protocol Buffer = protobuf)란 구조화된 데이터를 직렬화하고자 언어에 구애 받지 않고
플랫폼 중립적이며 확장 가능한 메커니즘인 데이터 직렬화 프로토콜이다.

 

공식 문서에 나올 법한 설명이라 좀 딱딱하게 느껴질 것 같아서

텍스트 기반의 데이터 형식인 JSON과 비교하고자 한다.

아래 JSON에서 공백을 모두 제거해 인코딩한 경우, 총 82바이트의 데이터 크기를 사용한다.

{
    "userName": "Martin",
    "favouriteNumber": 1337,
    "interests": ["daydreaming", "hacking"]
}

 

반면, 아래와 같이 서비스 인터페이스가 정의되어 .proto 파일에 표현된 프로토콜 버퍼를 인코딩하면 어떻게 될까?

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

해당 프로토콜 버퍼를 인코딩을 하면 아래 사진과 같이 총 33바이트를 사용하는 것을 확인할 수 있다.

위 코드에서 보다시피 프로토콜 버퍼의 경우, 메시지 바이너리 형식에서 필드를 식별하기 위한 고유 필드 번호를 갖고, 이를 아래 그림의 좌측(field tag)에서도 확인할 수 있다.

  • user_name = 1, favourite_number = 1, interests = 1 ---> X
  • user_name = 1, favourite_number = 2, interests = 3 ---> O

https://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html

 

결국 핵심은 user_name과 같이 불필요한 속성값을 숫자 형태의 고유 필드 번호로 대체한 것이다.

바로 다음 섹션에서 더 자세하게 다루겠지만,

데이터의 최초 1바이트는 5bit와 3bit로 나누어 고유 필드 번호와 필드 유형을 나타낸다.

그래서 위 사진의 첫 번째 줄을 보면 user_name의 고유 필드 번호는 1이고, 문자열(string) 유형을 가지므로

"00001"과 "010"으로 나타내어 2진수로 구성된 1바이트를 16진수로 표현하면 0000 | 1010이므로 0a로 표시되는 것이다.

지금은 문자열 유형이 "010"으로 표시된다는 것만 알아두자

 

 

그 다음에는 위 사진을 통해 짐작이 가겠지만, 이어질 데이터의 길이를 두 번째 바이트로 나타내게 되고

이런식으로 프로토콜 버퍼를 인코딩하여 단 33바이트만으로 표현할 수 있게 된다.

 

 

JSON이나 XML과 같은 텍스트 기반 데이터 형식은 사람이 읽기에는 용이하지만, 

내부 서버 간 통신에서는 오히려 불필요한 오버헤드가 될 수 있다. 

이러한 방식은 프로토콜 버퍼에 비해 데이터 용량이 크고, 처리 속도도 느리다는 단점이 있다. 

반면, 프로토콜 버퍼와 HTTP/2.0 기반의 gRPC를 활용하면 데이터 전송 효율성과 처리 성능을 크게 향상시킬 수 있어, 특히 마이크로서비스 간의 통신처럼 빈번하고 대량의 요청이 오가는 환경에서는 이러한 장점이 더욱 두드러진다.

 

 

 

Protocol Buffer Encoding

앞서 사진을 통해 간략하게 전체적인 프로토콜 버퍼 인코딩 방식을 알아보았다면,

이번에는 좀 더 구체적으로 알아보고자 한다.

프로토콜 버퍼로 인코딩된 바이트 스트림

 

 

메시지 필드의 태그

태그들은 위 사진처럼 필드 인덱스와 와이어 타입 두 가지 값으로 구성된다.

필드 인덱스는 .proto 파일에서 메시지를 정의할 때 각 메시지 필드에 할당된 고유 번호이며,

와이어 타입은 필드가 가질 수 있는 사전에 정의된 데이터 타입으로 매핑된다.

 

와이어 타입 종류 필드 타입
0 가변 길이 정수(varint) int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64비트 fixed64, sfixed64, double
2 길이 구분 (Length-delimited) string, bytes, embedded messages, packed repeated fields
3 시작 그룹 (Start group) groups (사용 중단)
4 종료 그룹 (End group) groups (사용 중단)
5 32비트 fixed32, sfixed32, float

 

특정 필드의 필드 인덱스와 와이어 타입을 알면, 아래 식을 사용해 필드의 태그 값을 결정할 수 있다.

Tag value = (field_index << 3) | wire_type

 

그래서 아까 앞서 사용된 예제에서 필드 인덱스 1을 가지고 문자열 필드 유형을 가진 user_name = 1;을 예시로 들자면 문자열 필드 유형을 가지므로 와이어 타입을 2가 되어,

바이너리 형식으로 표현하면 필드 인덱스는 00000001이고 와이어 타입은 0000010과 같다.

이 값들을 위의 식에 대입해 보면 다음과 같이 구해지는 것이다.

Tag value = (00000001 << 3) | 0000010
	  	  = 0000 1010

 

 

 

메시지 필드의 값

프로토콜 버퍼는 여러 데이터 타입에 따라 다른 인코딩 기술을 사용해 데이터를 인코딩한다.

예를 들어 문자열 값인 경우 프로토콜 버퍼는 UTF-8을 사용해 값을 인코딩하고,

int32 필드 타입인 정수 값은 가변 길이 정수(varint)라는 인코딩 기술을 사용한다.

 

문자열 값 인코딩

이번에도 user_name 필드를 예시로 보자면, 해당 필드의 값은 Martin이고, 이를 UTF-8 인코딩한다면 

0x4D 0x61 0x72 0x74 0x69 0x6E로 나타낼 수 있다.

그리고 인코딩된 값 앞에 인코딩된 값의 길이를 지정하여, 최종적으로 인코딩된 값 Martin의 16진수 표현은 아래와 같다.

A 6 4D 61 72 74 69 6E

A | 6 | 4D 61 72 74 69 6E
태그 | 인코딩된 문자열의 길이 | 인코딩된 값

 

가변 길이 정수 인코딩

가변 길이 정수는 하나 이상의 바이트를 사용해 정수를 직렬화하는 방법으로, 각 값에 할당된 바이트 수는 고정돼 있지 않고 값에 따라 다르다. 위의 와이어 타입 테이블에 있는 필드 타입들이 가변 길이 정수로 구분되어 인코딩된다.

 

가변 길이 정수에서 중요한 점은, 마지막 바이트를 제외한 각 바이트에 1bit를 최상위 비트(MSB, Most Significant Bit)로 가진다. 이 의미는 MSB가 1이라면 뒤에 더 많은 바이트가 있음을 나타내고, 0이라면 이어지는 바이트 스트림과 분리된다는 걸 의미한다.

 

그리고 각 바이트의 나머지 하위 7bit는 해당 수에 대한 2의 보수 표현으로 저장되며, Least Significant Group First 룰을 따르기 때문에 하위 바이트부터 저장된다.

 

인코딩 과정

가변 길이 정수로 300을 인코딩을 한다면

  1. 300은 이진수로 256 + 32 + 8 + 4 = 100101100으로 표기된다.
  2. 가변 길이 정수 직렬화를 하기 위해서 바이트 당 MSB가 포함되어야 하므로, 7bit 단위로 구분해준다.
    1. □000 0010 □010 1100
  3. Least Significant Group First 룰에 맞게 바이트를 역순으로 나열해준다.
    1. □010 1100 □000 0010
  4. MSB를 설정해준다.
    1. □010 1100 □000 0010 --> 1010 1100 0000 0010

디코딩 과정

1010 1100 0000 0010라는 직렬화 데이터를 받은 후, 디코딩을 수행하려면

위에서 진행한 인코딩 과정을 거꾸로 수행해주면 된다.

  1. MSB를 제외한다.
    1. 1010 1100 0000 0010 --> □010 1100 □000 0010
  2. 인코딩 과정에서 Least Significant Group First 룰에 의해 역순으로 나열되어 있으므로 다시 역순으로 정렬한다.
    1. □000 0010 □010 1100
  3. MSB가 들어가야 했던 부분(□)을 제외 후 데이터를 연결해준다.
    1. 100101100 = 300

 

 

 

HTTP/2에서의 gRPC

HTTP/2에서 클라이언트와 서버 간의 모든 통신은 단일 TCP 연결을 통해 처리되며,

이는 임의의 크기의 양방향 바이트 흐름을 전달할 수 있다.

프레임
HTTP/2에서 가장 작은 통신 단위로, 각 프레임에는 프레임 헤더가 포함돼 있으며, 헤더를 통해 프레임이 속한 스트림을 식별

메시지
하나 이상의 프레임으로 구성된 논리적 HTTP 메시지에 매핑되는 온전한 프레임 시퀀스이다
클라이언트와 서버가 메시지를 독립 프레임으로 분류하고 인터리브(interleave)한 후 다른 쪽에서 다시 조립할 수 있는 메시지 멀티플렉스를 지원한다

스트림
설정된 연결에서의 양방향 바이트 흐름이며, 스트림은 하나 이상의 메시지를 전달할 수 있다

 

요청 메시지

gRPC에서 요청 메시지는 항상 클라이언트 애플리케이션에 의해 트리거되며,

헤더, 길이-접두사 지정 메시지, 스트림 종료 플래그 세 가지 주요 요소로 구성된다.

클라이언트가 요청 헤더를 보내면 원격 호출이 시작되고, 스트림 종료(EOS, End Of Stream) 플래그가 전송돼 수신자에게 요청 메시지 전송이 완료됐음을 알린다.

응답 메시지

응답 메시지는 클라이언트 요청에 대한 응답으로 서버에 의해 생성되며,

응답 헤더, 길이-접두사 지정 메시지, 트레일러 세 가지 주요 요소로 구성된다.

클라이언트에 응답으로 보낼 길이-접두사 지정 메시지가 없는 경우, 응답 메시지는 헤더와 트레일러로만 구성된다.

 

 

 

gRPC 각 통신 패턴에서의 메시지 흐름

각 패턴의 전송 레벨에서 어떻게 작동하는지 알아보고자 한다. (HTTP/2)

단순 RPC

단순 RPC에서는 클라이언트가 서버의 원격 기능을 호출하고자 단일 요청을 서버로 보내고,

상태에 대한 세부 정보 및 후행 메타데이터와 함께 단일 응답을 받는다.

단순 RPC 예시

 

아래 사진과 같이 요청 메시지에는 헤더와 하나 이상의 데이터 프레임에 걸쳐 있을 수 있는 길이-접두사 지정 메시지가 포함된다. 클라이언트 측에서 연결 절반 종료(half-close the connection)하려면 요청 메시지의 끝에 스트림 종료(EOS) 플래그를 추가한다.

half-close the connection
클라이언트 측에서 연결을 닫아 더이상 서버로 메시지를 보낼 수는 없지만 여전히 서버에서 들어오는 메시지는 수신할 수 있음

 

서버는 전체 메시지를 받은 후에만 응답 메시지를 만드는데, 헤더 프레임과 길이-접두사 지정 메시지가 포함되며

서버가 상태 정보와 함께 트레일러 헤더를 보내면 통신이 종료된다.

단순 RPC의 메시지 흐름

 

서버 스트리밍 RPC

서버 스트리밍 RPC에서는 서버가 클라이언트의 요청 메시지를 받은 후, 일련의 응답을 다시 보낸다. 이런 일련의 응답을 스트림이라고 한다. 모든 서버 응답을 보낸 후에 서버는 서버의 상태 정보를 후행 메타데이터로 클라이언트에 전송해 스트림의 끝을 알린다.

아래 사진과 같이 주문 검색을 한다고 할 때, 일치하는 모든 주문을 한 번에 발송하지 않고 주문을 발견하는 대로 보내게 된다.

서버 스트리밍 RPC 예시

 

서버는 전체 요청 메시지를 수신 할 때까지 기다렸다가 아래와 같이 응답 헤더와 여러 길이-접두사 지정 메시지를 보낸다. 서버가 상태 정보와 함께 후행 헤더를 보내면 통신이 종료되는 구조이다.

서버 스트리밍 RPC의 메시지 흐름

 

클라이언트 스트리밍 RPC

클라이언트 스트리밍 RPC에서는 클라이언트가 하나의 요청이 아닌 여러 메시지를 서버로 보내고, 서버는 클라이언트에게 단일 응답을 보낸다. 그러나 서버는 클라이언트에서 모든 메시지를 수신해 응답을 보낼 때까지 기다릴 필요는 없다.

클라이언트 스트리밍 RPC 예시

 

클라이언트는 먼저 헤더 프레임을 전송해 서버와의 연결을 설정한다. 연결이 설정되면 아래 그림과 같이 여러 길이-접두사 지정 메시지를 데이터 프레임으로 서버에 보낸다. 최종적으로 클라이언트는 마지막 데이터 프레임에 EOS 플래그를 전송해 half-close the connection을 수행한다. 그러는 동안 서버는 클라이언트에서 받은 메시지를 읽는다. 모든 메시지를 받으면 서버는 후행 헤더와 함께 응답 메시지를 보내므로 연결을 닫게 된다.

클라이언트 스트리밍 RPC의 메시지 흐름

 

양방향 스트리밍 RPC

양방향 스트리밍 RPC에서 클라이언트는 메시지 스트림으로 서버에 요청을 보내고, 서버는 메시지 스트림으로도 응답한다.

양방향 스트리밍 RPC 예시

 

클라이언트가 헤더 프레임을 전송해 연결을 설정하고,

연결이 설정되면 클라이언트와 서버는 모두 상대방이 끝날 때까지 기다리지 않고 길이-접두사 지정 메시지를 보낸다.

양방향 스트리밍 RPC의 메시지 흐름

 

 

 

 

Reference

 

Encoding

Explains how Protocol Buffers encodes data to files or to the wire.

protobuf.dev

 

 

Schema evolution in Avro, Protocol Buffers and Thrift — Martin Kleppmann’s blog

Schema evolution in Avro, Protocol Buffers and Thrift Published by Martin Kleppmann on 05 Dec 2012. So you have some data that you want to store in a file or send over the network. You may find yourself going through several phases of evolution: Using your

martin.kleppmann.com

 

 

gRPC 시작에서 운영까지 | 카순 인드라시리 - 교보문고

gRPC 시작에서 운영까지 | 클라우드 및 마이크로서비스 아키텍처의 출현으로 오늘날 애플리케이션은 프로세스간 통신 기술을 사용해 연결되며, gRPC는 가장 널리 사용되는 효율적인 통신 기술 중

product.kyobobook.co.kr

 

 

[NBP 기술&경험] 시대의 흐름, gRPC 깊게 파고들기 #2

google에서 개발한 오픈소스 RPC(Remote Procedure Call) 프레임워크, gRPC를 알아봅니다.

medium.com

 

 

gRPC와 Protocol Buffer: 섀넌 정보이론, Serialization, RPC 동작 방식

소프트웨어 시스템이 계속 발전함에 따라 서비스 간의 효율적인 통신에 대한 필요성이 그 어느 때보다 중요해졌다. 이를 달성하기 위한 두 가지 인기 있는 접근 방식은 protocol buffer가 있는 gRPC와

loosie.tistory.com

 

 

 

'gRPC' 카테고리의 다른 글

gRPC 통신 구현해보기 (서버 : Java / 클라이언트 : Go)  (0) 2025.01.23
RPC 통신과 gRPC  (0) 2025.01.12
'gRPC' 카테고리의 다른 글
  • gRPC 통신 구현해보기 (서버 : Java / 클라이언트 : Go)
  • RPC 통신과 gRPC
개발이조아용
개발이조아용
IT 개발에서 배운 성장의 기록을 작성합니다.
  • 개발이조아용
    계속 하다 보면?!
    개발이조아용
  • 전체
    오늘
    어제
    • 분류 전체보기 (68)
      • Tibero DB (Tmax AI Bigdata .. (7)
      • Git (2)
      • CI CD (2)
      • Redis (3)
      • SpringBoot (16)
      • SQL 문제 풀이 (8)
      • Apache Kafka (8)
        • 오류 해결 (3)
        • 개념 정리 (4)
        • 보안 (1)
      • Nginx (3)
      • SW마에스트로 (3)
      • Kubernetes (4)
      • AWS (5)
      • gRPC (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    MSA
    KAFKA
    Tibero
    DynamoDB 연동
    grpc
    소프트웨어 마에스트로
    SpringBoot
    sql 문제
    Redis 개념
    redis script
    leetcode
    Kafka 개념
    SQL
    K8S
    Git
    nginx
    Kafka 오류
    SASL 인증
    Kafka SASL
    redis
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
개발이조아용
Protocol Buffer와 인코딩/디코딩 그리고 gRPC 내부 통신 동작 원리
상단으로

티스토리툴바