📁 File5. 애플리케이션 파일04 · Binary Formats — protobuf · MessagePack · CBOR · Avro · Parquet · Arrow

04 · Binary Formats — protobuf · MessagePack · CBOR · Avro · Parquet · Arrow

이 문서가 답하는 질문: JSON 이 있는데 왜 바이너리 직렬화가 필요한가? parquet 와 MP4 는 둘 다 “바이너리”지만 무엇이 결정적으로 다른가? MIME: application/x-protobuf, application/cbor, application/vnd.apache.parquet, application/vnd.apache.arrow.file


한 줄 답 (Pyramid Top)

데이터 바이너리는 시간축이 없고 컬럼 축으로 인덱싱 된다 — 미디어 바이너리(MP4)와 정반대. protobuf/Avro 는 스키마-우선 으로 IPC 트래픽을 줄이고, parquet/Arrow 는 컬럼-우선 으로 분석 쿼리를 가속한다. CBOR/MessagePack 은 JSON-호환 의 슬림한 대체재.


Why — JSON 이 못 하는 일

JSON 은 만능에 가깝지만 3가지 비용 이 있다:

  1. 크기: 모든 키가 텍스트로 반복. {"userId":123,"userId":456,...}"userId" 가 N 번.
  2. 타입 모호: 1, 1.0, "1" 모두 가능. int64 인지 int32 인지 모름.
  3. 파싱 비용: 텍스트 → 트리 → 객체. 큰 데이터에서 CPU 많이 먹음.

목적별로 다른 해법이 나왔다.

메시징 (gRPC, Kafka): protobuf, Avro

  • 키 대신 숫자 태그 (field number = 1, 2, 3).
  • 스키마는 별도 파일(.proto).
  • 결과: JSON 대비 3070% 크기, 510배 빠른 파싱.

IoT / 임베디드: CBOR, MessagePack

  • JSON 의 그대로의 데이터 모델 (object, array, string, number, bool, null).
  • 단지 바이너리 표현. 스키마 없음.
  • 결과: 사람이 읽기 어렵지만 JSON 의 30~50% 크기.

분석 (Spark, BigQuery, DuckDB): Parquet, ORC, Arrow

  • 컬럼 단위로 저장name 컬럼만 1 GB 중 5 MB 만 읽으면 됨.
  • 같은 컬럼은 같은 타입이라 dictionary / RLE / delta 인코딩 가능.
  • 결과: SQL SELECT name FROM big WHERE date='2026-05-10' 에서 10~100배 빠름.

How — 각 포맷의 핵심 트릭

protobuf — 태그 + Varint

message User {
  int64 id     = 1;
  string name  = 2;
  bool active  = 3;
}

직렬화:

[tag=1, type=Varint] [val=42]
[tag=2, type=Length-delimited] [len=4] [val="Anne"]
[tag=3, type=Varint] [val=1]

Varint: 작은 수일수록 적은 바이트 (0~127 = 1바이트). DB ID 가 보통 작음 → 큰 절약.

Avro — 스키마를 같이 보냄

protobuf 는 사전에 .proto 공유 필요. Avro 는 스키마를 데이터 헤더에 포함:

┌────────────────────────────────────────┐
│ Schema (JSON)                          │ ← 한 번만
├────────────────────────────────────────┤
│ Block: { records: [...], count, size } │ ← 반복
├────────────────────────────────────────┤
│ Sync marker (16B random)               │
└────────────────────────────────────────┘
  • Hadoop / Kafka 의 표준.
  • 스키마 진화 가 잘 됨 (필드 추가/제거/기본값).

Parquet — Row Group + Column Chunk + Page

File
 ├── Row Group 1
 │    ├── Column Chunk: name        (page1, page2, ...)
 │    ├── Column Chunk: age
 │    └── Column Chunk: country
 ├── Row Group 2
 │    └── ...
 └── Footer (FileMetaData: schema + row group locations)
  • Footer 가 끝에 — PDF · ZIP 과 동일 패턴! 끝부분만 읽으면 모든 컬럼 위치 파악.
  • 각 Column Chunk 안에서:
    • Dictionary encoding — 반복되는 문자열을 ID 로 ("Korea" → 1)
    • RLE1,1,1,1(1, 4)
    • Delta — 정렬된 ID 의 차이만 저장
  • 압축: snappy / gzip / zstd / lz4

Arrow — 메모리 전용 컬럼 포맷

Parquet 가 디스크용이라면 Arrow 는 RAM 용.

  • 직렬화 / 역직렬화 비용 0 (memcpy 만 하면 됨).
  • 모든 언어가 같은 포맷 → Python 의 pandas DataFrame 을 직렬화 없이 Java/Go 로 전달.
  • Arrow Flight: gRPC 기반 데이터 운송.

CBOR / MessagePack — JSON 의 슬림 표현

JSON:   {"a": 1, "b": [1, 2, 3]}     # 22 bytes
CBOR:   A2 61 61 01 61 62 83 01 02 03  # 10 bytes

태그 1 byte 로 타입 + 길이 인코딩. CBOR 는 RFC 8949, COSE/CWT (JOSE 의 바이너리 버전) 의 기반.


What — 비교표 + CLI

비교표

포맷스키마압축률파싱 속도컬럼 액세스주 사용처
JSONX1x1xX웹 API
MessagePackX1.5~2x3xXRedis, IoT
CBORX1.5~2x3xXIoT, COSE
BSONX~1x2xXMongoDB
protobufO3~5x5~10xXgRPC, IPC
AvroO (포함)3~5x5~10xXKafka, Hadoop
FlatBuffersO3x무한 (zero-copy)부분게임
ParquetO5~10x5~10xO분석 (디스크)
ORCO5~10x5~10xO분석 (Hive)
ArrowO~1x (압축 X)무한 (zero-copy)O분석 (메모리)

매직 / 식별

$ xxd file.parquet | head -1
00000000: 5041 5231 ...                    PAR1... 시작·끝
 
$ xxd file.avro | head -1
00000000: 4f62 6a01 ...                    Obj.
 
$ xxd file.arrow | head -1
00000000: 4152 524f 5731 ...                ARROW1

protobuf, MessagePack, CBOR 은 매직이 없다 — 컨테이너에 의존 (HTTP Content-Type 또는 파일 확장자).

CLI

# Parquet 메타 / 스키마 / 통계
$ parquet-tools meta data.parquet
$ parquet-tools schema data.parquet
$ duckdb -c "SELECT * FROM 'data.parquet' LIMIT 10"
 
# protobuf (스키마 알고 있을 때)
$ protoc --decode=User user.proto < binary.bin
 
# CBOR / MessagePack
$ python -c "import cbor2; print(cbor2.load(open('a.cbor','rb')))"
$ python -c "import msgpack; print(msgpack.unpackb(open('a.msgpack','rb').read()))"
 
# Avro
$ avro-tools tojson data.avro

Parquet 의 인덱싱 — 실제 효과

$ ls -lh logs.csv logs.parquet
-rw-r--r--  1 raw  staff   1.2G  logs.csv
-rw-r--r--  1 raw  staff   180M  logs.parquet      # 압축 + 컬럼
 
$ time duckdb -c "SELECT count(*) FROM 'logs.csv' WHERE level='ERROR'"
real   0m4.20s
 
$ time duckdb -c "SELECT count(*) FROM 'logs.parquet' WHERE level='ERROR'"
real   0m0.18s         # level 컬럼만 읽음 + dictionary encoding

What-if — 함정

1) protobuf — 호환성 깨기

int64int32 변경: 큰 수 잘림, 사일런트 데이터 손실. 필드 번호 재사용 (field 5 가 한 번 name 이었다가 email 이 됨): 옛 클라이언트가 잘못 디코드.

원칙:

  • 필드 번호 재사용 절대 금지 (deprecated 마킹 + reserved).
  • 타입 변경은 Major version up.

2) Avro — 스키마 진화의 역설

Avro 는 “리더 스키마”와 “라이터 스키마” 둘 다 알아야 함. Kafka 에 Avro 메시지가 흘러갈 때 Schema Registry 없이는 파싱 불가. 인프라 비용이 추가됨.

3) Parquet — 작은 파일 문제

S3 에 하루 100,000 개 1KB parquet → S3 LIST 가 느림, 쿼리 엔진이 헐떡임. 대응: 시간/날짜 단위로 compaction (hourly → daily merge).

4) Parquet — Schema drift

5월 1일 파일: user_id BIGINT 5월 2일 파일: user_id STRING (실수) 같은 디렉토리 쿼리 시 일부 엔진은 한 쪽을 무시, 일부는 에러. 대응: 스키마 강제 (Iceberg / Delta Lake 의 schema enforcement).

5) MessagePack / CBOR — Number 타입 의도

JS 의 Number 는 64bit float — 11.0 으로 직렬화될 수 있음. 서버가 int 를 기대하면 깨짐. 대응: 명시적 타입 hint 또는 protobuf 로 전환.

6) JSON ↔ 바이너리의 silent re-encode

JSON → MessagePack → JSON 라운드트립에서 키 순서 / 부동소수점 정밀도 손실 가능. 서명/해시 검증에 영향. canonical encoding 사용 (CBOR Deterministic, JCS).

7) Arrow IPC vs Arrow File

같은 columnar 데이터지만:

  • IPC Streaming: 헤더 없음, 끝나는 시점 모름 (스트리밍).
  • IPC File: 끝에 footer (랜덤 액세스). 혼동하면 pyarrow.ipc.open_file 이 streaming 데이터에서 깨짐.

Insight — 흥미로운 이야기

“protobuf 는 Google 의 1999년 사내 도구”

처음에는 인덱서 - 검색 노드 통신용. 2008년 오픈소스화. proto2 → proto3 (2016) 에서 required 필드 제거. 이게 큰 논쟁이었음 — required 를 빼니 의도하지 않은 빈 값이 흐름. 결국 proto3 가 optional 키워드를 다시 도입(2020). 교훈: “스키마를 빡빡하게” vs “진화를 쉽게” 의 영원한 트레이드오프.

“미디어 바이너리(MP4) 와 데이터 바이너리(parquet)의 인덱싱 철학”

MP4 (미디어)Parquet (데이터)
인덱싱 단위시간 (timestamp → byte offset)컬럼 (column → byte offset)
읽기 패턴시간순 + 점프일부 컬럼 전체 스캔
압축 단위프레임 (시간)컬럼 청크 (속성)
인덱스 위치moov box (앞 또는 뒤)Footer (끝)

시간이 1차원이냐 속성이 1차원이냐” — 이게 미디어와 데이터를 가르는 결정적 차이.

“왜 모든 인덱스는 끝에 있는가”

PDF 의 xref, ZIP 의 EOCD, Parquet 의 footer, MP4 의 (faststart 안 된) moov. 이유: append 친화적. 데이터를 흘러가며 쓰고, 마지막에 인덱스만 적으면 끝. 끝에 인덱스가 있어도 HTTP Range + 끝부터 읽기로 랜덤 액세스가 가능 → 옛날 설계가 클라우드 시대에 잘 맞는 우연.

“Arrow 가 zero-copy 인 이유”

모든 언어가 같은 메모리 레이아웃 을 합의함 — int32 배열은 4-byte little-endian, validity bitmap, offset 배열… Python pandas → Java Spark 가 같은 RAM 영역을 가리키기만 하면 됨. 직렬화 비용 0. 데이터 사이언스에서 “polyglot” 이 가능해진 결정적 사건.


요약 + Mermaid

바이너리 직렬화는 JSON 의 한계(크기·타입·속도) 를 푼다. 메시징은 protobuf/Avro 가 스키마 + 태그 + Varint 로, JSON-호환 슬림화는 CBOR/MessagePack 이, 분석은 Parquet/Arrow 가 컬럼 인덱싱 으로 해결한다. 미디어(MP4) 와 데이터(parquet) 둘 다 “바이너리”지만 인덱싱 축이 정반대다.