gRPC 전송 최적화: 플래트닝 및 비트셋 기반의 효율적인 솔루션

발행: (2025년 12월 17일 오후 12:17 GMT+9)
8 min read
원문: Dev.to

Source: Dev.to

위 링크에 있는 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다. (코드 블록, URL 및 마크다운 형식은 그대로 유지됩니다.)

배경 및 기술적 과제

데이터베이스 미들웨어용 API를 설계할 때, 우리는 종종 다중 행 쿼리 결과를 반환해야 합니다. 기존 gRPC 정의를 사용하면 다음과 같은 문제가 발생할 수 있습니다:

페이로드 중복 (키 반복)

단순한 Protobuf 정의는 각 행을 map 또는 객체에 매핑합니다:

message Row {
  map data = 1;
}
message Response {
  repeated Row rows = 1;
}

문제:
예를 들어 customer_id, created_at, status 필드를 가진 레코드 10 000개를 반환하는 쿼리의 경우, 필드 이름(키)이 10 000번 전송되어 심각한 페이로드 부풀림과 불필요한 대역폭 사용을 초래합니다.

Protobuf NULL 값 제한

Proto3은 스칼라 타입을 널 허용이 아닌 것으로 취급합니다:

  • NULLstring 필드는 빈 문자열 "" 로 직렬화됩니다.
  • 클라이언트는 “빈 값”과 데이터베이스에서 온 실제 NULL을 구분할 수 없습니다.

래퍼 타입(예: google.protobuf.StringValue)을 사용하면 해결할 수 있지만, 추가적인 중첩과 처리 오버헤드를 발생시킵니다.

Source:

해결책: 데이터베이스‑유사 저수준 아키텍처

AI는 전통적인 “행당 하나의 객체” 사고방식에서 벗어나 컬럼형 스토리지ODBC/JDBC 드라이버 구현에서 아이디어를 차용할 것을 제안했습니다. 핵심 최적화는 두 부분으로 구성됩니다:

평탄화

키 중복을 없애고 다음을 전송합니다:

  • 헤더 (메타데이터): 필드 정의(columns)를 한 번만 전송.
  • 바디 (값): 모든 데이터 값을 하나의 1차원 배열(values)에 담아 전송.

이렇게 하면 행당 키 전송이 사라져 페이로드가 크게 감소합니다.

비트셋 (비트맵) 메커니즘

NULL 값을 별도의 래퍼 메시지 없이 처리합니다:

  • 각 값의 null 상태를 기록하는 바이너리 필드(bytes)를 도입.
  • 비트셋 규칙:
    • 1 → 값이 NULL
    • 0 → 값이 유효

공간 효율성: 8개의 값마다 1 바이트만 필요합니다. 1 000행 × 8열의 경우 오버헤드는 대략 1 KB 수준입니다.

구현 핵심

아래는 서버 측 압축클라이언트 측 복원을 위한 핵심 코드 스니펫입니다.

Protobuf 정의 (proto/query.proto)

message QueryResponse {
  repeated string values      = 3; // Flattened values
  repeated Column columns     = 4; // Field definitions
  bytes null_bitmap           = 7; // NULL marker bitstream
  int32 row_count             = 6;
}

서버 측 – 인코딩 및 압축

서버는 결과 집합을 한 번 순회하면서 평탄화된 값 리스트와 비트맵을 동시에 구축합니다.

// $result['rows'] is the 2‑D array returned by the database
$values       = [];
$packedBytes  = "";
$currentByte  = 0;
$bitIndex     = 0;

foreach ($result['rows'] as $row) {
    foreach ($row as $value) {
        if ($value === null) {
            $values[] = "";                     // Placeholder for empty string
            $currentByte |= (1  0) {
    $packedBytes .= chr($currentByte);
}

클라이언트 측 – 디코딩 및 복원

클라이언트는 열 개수에 따라 평탄화된 배열을 슬라이스하고 비트맵을 참조하여 NULL 값을 복원합니다.

$fetchedRows = [];
$columns     = $response->getColumns();
$colCount    = count($columns);
$values      = $response->getValues();      // Flattened array
$bitmap      = $response->getNullBitmap();  // Bitmap string
$rowCount    = $response->getRowCount();

for ($r = 0; $r > $bitPos) & 1;

        if ($isNull) {
            $row[] = null;                     // Restore NULL
        } else {
            $row[] = $values[$flatIndex];      // Restore actual value
        }
    }
    $fetchedRows[] = $row;
}
} else {
    $row[] = $values[$flatIndex];
}
}
$fetchedRows[] = $row;

핵심 요약

  • Flattening은 반복적인 필드 이름을 제거하여 페이로드 크기를 크게 줄입니다.
  • BitsetNULL 상태를 나타내는 컴팩트하고 상수 시간 방식(값당 1 bit)을 제공합니다.
  • 이 두 가지 접근 방식을 결합하면 대규모 결과 집합에 적합한 가볍고 고처리량의 gRPC 페이로드를 제공하며, NULL과 빈 문자열을 구분하는 능력을 손상시키지 않습니다.

최적화 이점 분석

이 아키텍처를 채택하면 다음과 같은 구체적인 이점을 얻을 수 있습니다:

극도의 전송 효율성

  • 평탄화 설계를 통해 페이로드 크기가 데이터 양에 따라 선형적으로 증가하며, 필드 이름 길이에 영향을 받지 않습니다. 대용량 데이터 조회 상황에서 대역폭 절감 효과가 매우 큽니다.

정밀한 타입 복원

  • 클라이언트는 null_bitmap을 읽어 데이터베이스 NULL 상태를 정확히 복원할 수 있어, gRPC 기본 타입의 한계를 해결합니다.

파싱 성능 향상

  • PHP 및 기타 언어에서 평면 1차원 배열(인덱스 배열) 처리는 대규모 복잡한 중첩 객체를 처리하는 것에 비해 CPU 캐시 적중률이 높고 메모리 단편화가 적어 일반적으로 더 나은 성능을 제공합니다.

Conclusion

이 최적화 사례는 현대 분산 시스템에서 low‑level system design thinking을 적절히 도입하는 것이 중요함을 보여줍니다. AI와의 협업을 통해 우리는 단순 API 설계의 틀을 넘어 bitwise operationsdata‑structure optimization을 활용하여 데이터베이스‑애플리케이션 시나리오에서 gRPC/Protobuf의 고유한 한계를 매우 낮은 비용으로 해결했습니다.

Back to Blog

관련 글

더 보기 »