같은 데이터라도 어떤 파일 포맷으로 저장하느냐에 따라 쿼리 속도가 100배, 비용이 10배 차이 난다. 데이터 분석가가 "왜 이 쿼리가 5분이나 걸리죠?"라고 물으면, 답은 종종 "데이터가 CSV로 저장돼 있어서"다. CSV를 Parquet으로 바꾸는 한 줄짜리 변환이 가장 효과적인 최적화일 때가 많다.
오늘은 데이터 엔지니어가 매일 마주하는 파일 포맷들 — CSV, JSON, Parquet, ORC, Avro — 을 비교하고, 그 차이의 본질인 컬럼형(columnar) vs 행형(row-based) 저장을 이해하며, 시간이 지나며 데이터 구조가 바뀌는 현실을 다루는 스키마 진화(schema evolution)를 본다.
포맷
구조
사람이 읽나
압축
주 용도
CSV
행형, 텍스트
O
약함
단순 교환, 레거시
JSON
행형, 텍스트
O
약함
API, 중첩 구조, 로그
Parquet
컬럼형, 바이너리
X
강함
분석(OLAP)의 표준
ORC
컬럼형, 바이너리
X
강함
Hive 생태계 분석
Avro
행형, 바이너리
X
중간
스트리밍, 스키마 진화
크게 세 부류다. 텍스트 기반(CSV/JSON)은 사람이 읽기 쉽고 호환성이 좋지만 비효율적이다. 컬럼형 바이너리(Parquet/ORC)는 분석 쿼리에 최적화돼 있다. 행형 바이너리(Avro)는 쓰기와 스키마 진화에 강해 스트리밍·이벤트에 쓰인다.
💡 관련 이론: 포맷 선택은 워크로드 따라간다. "쓰기가 잦고 한 레코드 전체를 자주 다루는가(write-heavy)" → 행형(Avro). "읽기 위주로 특정 컬럼만 집계하는가(read-heavy, analytical)" → 컬럼형(Parquet/ORC). 이 한 줄이 시험에서 포맷을 고르는 기준이다.
원본 테이블
id | name | amount
1 | Kim | 100
2 | Lee | 200
3 | Park | 300
행형(row-based) 저장 — CSV, Avro
[1,Kim,100][2,Lee,200][3,Park,300]
한 행을 통째로 모아둔다
컬럼형(columnar) 저장 — Parquet, ORC
[1,2,3][Kim,Lee,Park][100,200,300]
같은 컬럼끼리 모아둔다
SELECT SUM(amount) 같은 분석 쿼리를 예로 든다. 행형은 amount를 읽으려면 id·name까지 전부 스캔해야 한다. 컬럼형은 [100,200,300] 블록만 읽으면 끝이다. 필요한 컬럼만 읽으므로 I/O가 극적으로 줄어든다. 이를 컬럼 프루닝(column pruning)이라 한다.
게다가 같은 컬럼은 데이터 타입과 값이 비슷해 압축이 훨씬 잘 된다. amount 컬럼은 전부 숫자라 효율적으로 압축되지만, 행 단위로 섞인 타입은 그렇지 않다.
-- Athena 비용은 "스캔한 데이터량"에 비례한다-- CSV: amount만 필요해도 모든 컬럼을 스캔 → 비싸고 느림-- Parquet: amount 컬럼만 스캔 → 싸고 빠름SELECT region, SUM(amount)FROM orders -- Parquet이면 region, amount 두 컬럼만 읽음GROUP BY region;
반대로 OLTP처럼 "한 행 전체를 읽거나 쓰는" 작업은 행형이 유리하다. 컬럼형은 한 행을 재구성하려면 여러 컬럼 블록을 모아야 하기 때문이다.
컬럼형 포맷은 컬럼 프루닝 외에도 프레디킷 푸시다운(predicate pushdown)과 파티션 프루닝으로 빨라진다. Parquet은 데이터 블록마다 min/max 등 통계를 저장해, WHERE amount > 500 같은 조건에 안 맞는 블록을 아예 읽지 않고 건너뛴다. 컬럼 프루닝(가로로 덜 읽기) + 프레디킷 푸시다운(세로로 덜 읽기)의 결합이 OLAP 성능의 핵심이다.
AWS 분석 스택(Athena, Redshift Spectrum, Glue, EMR)에서 Parquet이 기본 권장 포맷이다. 이유를 정리하면:
컬럼 프루닝: 필요한 컬럼만 읽어 I/O·비용 절감 (Athena는 스캔량 과금)
강한 압축: 같은 데이터를 CSV 대비 훨씬 작게 저장 → 스토리지 비용↓
내장 통계: 블록별 min/max로 프레디킷 푸시다운
스키마 내장: 파일 자체가 스키마를 포함해 자기 기술적(self-describing)
# 흔한 최적화: 원시 CSV/JSON을 Parquet으로 변환 (Glue/Spark)df = spark.read.json("s3://raw/events/")df.write.partitionBy("year","month","day") \ .parquet("s3://processed/events/")# 이후 Athena 쿼리가 빨라지고 스캔 비용이 급감
ORC는 Parquet과 거의 동일한 컬럼형 장점을 가지며 Hive/Hadoop 생태계에서 특히 강하다. AWS에서는 둘 다 잘 지원되지만 Parquet이 더 널리 쓰인다.
Avro는 행형 바이너리인데도 중요하게 다뤄진다. 핵심 강점이 스키마 진화이기 때문이다. Avro는 데이터와 함께 스키마를 명시적으로 다루며, 읽을 때의 스키마(reader)와 쓸 때의 스키마(writer)가 달라도 호환되도록 설계됐다. 그래서 스키마가 자주 바뀌는 스트리밍·이벤트 파이프라인(Kafka 등)에서 표준처럼 쓰인다.
스키마 진화란 시간이 지나며 데이터 구조가 바뀌는 현실을 안전하게 다루는 것이다. 예를 들어 주문 이벤트에 나중에 coupon_code 필드가 추가될 수 있다. 이때 옛 데이터와 새 데이터를 모두 읽을 수 있어야 한다.
스키마 진화의 호환성 종류
하위 호환(backward) : 새 스키마로 옛 데이터를 읽을 수 있다
→ 필드 삭제, 기본값 있는 필드 추가
상위 호환(forward) : 옛 스키마로 새 데이터를 읽을 수 있다
→ 필드 추가
완전 호환(full) : 양방향 모두 가능
안전한 진화의 원칙: 필드를 추가할 땐 기본값(default)을 주고, 함부로 삭제·이름변경하지 않는다. 기본값이 있으면 그 필드가 없는 옛 데이터를 읽어도 기본값으로 채워져 깨지지 않는다.
스키마 진화를 조직 차원에서 강제하는 것이 스키마 레지스트리(Schema Registry)다. 프로듀서가 새 스키마를 등록할 때 기존과 호환되는지 검증해, 호환을 깨는 변경을 배포 전에 차단한다. AWS Glue Schema Registry가 이 역할을 하며, Kinesis·MSK와 통합된다. 데이터 계약(data contract)을 코드로 강제하는 거버넌스 장치다.
레지스트리는 새 스키마 버전을 등록할 때 정해 둔 호환 규칙으로 검사하고, 어기면 등록을 거부한다. AWS Glue Schema Registry 는 Avro, JSON(JSON Schema), Protobuf 를 지원하고 별도 요금이 없으며, Apache Kafka·Amazon MSK·Kinesis Data Streams·Managed Service for Apache Flink·Lambda 와 연동된다.
모드
보장하는 것
허용되는 변경
NONE
검사 없음
무엇이든 등록됨(개발용)
DISABLED
버전 추가 자체를 막음
없음
BACKWARD
새 스키마의 소비자가 직전 버전 데이터도 읽는다
필드 삭제, 선택(optional) 필드 추가
BACKWARD_ALL
위와 같되 모든 이전 버전 대상
같음
FORWARD
직전 스키마의 소비자가 새 데이터를 읽는다
필드 추가, 선택 필드 삭제
FORWARD_ALL
위와 같되 모든 이전 버전 대상
같음
FULL
직전 버전과 양방향 호환
선택 필드의 추가·삭제만
FULL_ALL
모든 이전 버전과 양방향 호환
같음
AWS 가 권장하는 기본 선택은 BACKWARD 다. 새 코드로 올린 소비자가 스트림에 남아 있는 옛 메시지도 읽을 수 있어야 하기 때문이다.
모드는 배포 순서를 정해 준다. BACKWARD 는 소비자를 먼저 올려도 안전하고(새 소비자가 옛 데이터를 읽음), FORWARD 는 프로듀서를 먼저 올려도 안전하다(옛 소비자가 새 데이터를 읽음). 어느 쪽을 먼저 배포할지 통제할 수 없으면 FULL 이다.
_ALL 이 붙지 않은 모드는 바로 앞 버전하고만 비교한다. 보존 기간이 긴 스트림을 처음부터 다시 읽을 일이 있으면 v1 로 쓴 메시지를 v5 소비자가 읽어야 하므로 _ALL 이 필요하다.
BACKWARD 에서 "필수 필드 추가"가 거부되는 이유: 옛 메시지에는 그 필드가 없는데 새 소비자는 반드시 있어야 한다고 믿기 때문이다. 선택 필드로 바꾸면 통과한다.
레지스트리를 쓰면 메시지마다 스키마 전체를 실어 보내지 않는다. 직렬화기가 스키마를 레지스트리에 등록(또는 조회)하고 메시지에는 스키마 버전 식별자만 붙인다. 소비자의 역직렬화기가 그 식별자로 스키마를 가져와 해석한다. Avro 메시지가 JSON 보다 작아지는 이유가 필드 이름을 매번 싣지 않기 때문이다.