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가지 비용 이 있다:
- 크기: 모든 키가 텍스트로 반복.
{"userId":123,"userId":456,...}의"userId"가 N 번. - 타입 모호:
1,1.0,"1"모두 가능.int64인지int32인지 모름. - 파싱 비용: 텍스트 → 트리 → 객체. 큰 데이터에서 CPU 많이 먹음.
목적별로 다른 해법이 나왔다.
메시징 (gRPC, Kafka): protobuf, Avro
- 키 대신 숫자 태그 (
field number = 1, 2, 3). - 스키마는 별도 파일(
.proto). - 결과: JSON 대비 30
70% 크기, 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) - RLE —
1,1,1,1→(1, 4) - Delta — 정렬된 ID 의 차이만 저장
- Dictionary encoding — 반복되는 문자열을 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
비교표
| 포맷 | 스키마 | 압축률 | 파싱 속도 | 컬럼 액세스 | 주 사용처 |
|---|---|---|---|---|---|
| JSON | X | 1x | 1x | X | 웹 API |
| MessagePack | X | 1.5~2x | 3x | X | Redis, IoT |
| CBOR | X | 1.5~2x | 3x | X | IoT, COSE |
| BSON | X | ~1x | 2x | X | MongoDB |
| protobuf | O | 3~5x | 5~10x | X | gRPC, IPC |
| Avro | O (포함) | 3~5x | 5~10x | X | Kafka, Hadoop |
| FlatBuffers | O | 3x | 무한 (zero-copy) | 부분 | 게임 |
| Parquet | O | 5~10x | 5~10x | O | 분석 (디스크) |
| ORC | O | 5~10x | 5~10x | O | 분석 (Hive) |
| Arrow | O | ~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 ... ARROW1protobuf, 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.avroParquet 의 인덱싱 — 실제 효과
$ 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 encodingWhat-if — 함정
1) protobuf — 호환성 깨기
int64 → int32 변경: 큰 수 잘림, 사일런트 데이터 손실.
필드 번호 재사용 (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 — 1 이 1.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) 둘 다 “바이너리”지만 인덱싱 축이 정반대다.