gRPC 전송 최적화: 플래트닝 및 비트셋 기반의 효율적인 솔루션
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은 스칼라 타입을 널 허용이 아닌 것으로 취급합니다:
NULL인string필드는 빈 문자열""로 직렬화됩니다.- 클라이언트는 “빈 값”과 데이터베이스에서 온 실제
NULL을 구분할 수 없습니다.
래퍼 타입(예: google.protobuf.StringValue)을 사용하면 해결할 수 있지만, 추가적인 중첩과 처리 오버헤드를 발생시킵니다.
Source: …
해결책: 데이터베이스‑유사 저수준 아키텍처
AI는 전통적인 “행당 하나의 객체” 사고방식에서 벗어나 컬럼형 스토리지와 ODBC/JDBC 드라이버 구현에서 아이디어를 차용할 것을 제안했습니다. 핵심 최적화는 두 부분으로 구성됩니다:
평탄화
키 중복을 없애고 다음을 전송합니다:
- 헤더 (메타데이터): 필드 정의(
columns)를 한 번만 전송. - 바디 (값): 모든 데이터 값을 하나의 1차원 배열(
values)에 담아 전송.
이렇게 하면 행당 키 전송이 사라져 페이로드가 크게 감소합니다.
비트셋 (비트맵) 메커니즘
NULL 값을 별도의 래퍼 메시지 없이 처리합니다:
- 각 값의 null 상태를 기록하는 바이너리 필드(
bytes)를 도입. - 비트셋 규칙:
1→ 값이NULL0→ 값이 유효
공간 효율성: 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은 반복적인 필드 이름을 제거하여 페이로드 크기를 크게 줄입니다.
- Bitset은
NULL상태를 나타내는 컴팩트하고 상수 시간 방식(값당 1 bit)을 제공합니다. - 이 두 가지 접근 방식을 결합하면 대규모 결과 집합에 적합한 가볍고 고처리량의 gRPC 페이로드를 제공하며,
NULL과 빈 문자열을 구분하는 능력을 손상시키지 않습니다.
최적화 이점 분석
이 아키텍처를 채택하면 다음과 같은 구체적인 이점을 얻을 수 있습니다:
극도의 전송 효율성
- 평탄화 설계를 통해 페이로드 크기가 데이터 양에 따라 선형적으로 증가하며, 필드 이름 길이에 영향을 받지 않습니다. 대용량 데이터 조회 상황에서 대역폭 절감 효과가 매우 큽니다.
정밀한 타입 복원
- 클라이언트는
null_bitmap을 읽어 데이터베이스NULL상태를 정확히 복원할 수 있어, gRPC 기본 타입의 한계를 해결합니다.
파싱 성능 향상
- PHP 및 기타 언어에서 평면 1차원 배열(인덱스 배열) 처리는 대규모 복잡한 중첩 객체를 처리하는 것에 비해 CPU 캐시 적중률이 높고 메모리 단편화가 적어 일반적으로 더 나은 성능을 제공합니다.
Conclusion
이 최적화 사례는 현대 분산 시스템에서 low‑level system design thinking을 적절히 도입하는 것이 중요함을 보여줍니다. AI와의 협업을 통해 우리는 단순 API 설계의 틀을 넘어 bitwise operations와 data‑structure optimization을 활용하여 데이터베이스‑애플리케이션 시나리오에서 gRPC/Protobuf의 고유한 한계를 매우 낮은 비용으로 해결했습니다.