LLM WikiAccess-protected knowledge portal
← 스터디 홈
2편 · 약 23분

Apache Iceberg 핵심: snapshot, manifest, partition evolution

왜 Iceberg인가

앞 장에서 파일 레이크의 한계를 살펴봤다. ACID 없음, 업데이트 어려움, 스키마 강제 없음, 느린 메타데이터 조회 — 이 문제들을 해결하기 위해 Apache Iceberg는 파일 위에 계층형 메타데이터 트리를 올린다. 이 장에서는 그 내부 구조를 실제로 파악해본다: 스냅샷이 어떻게 불변성을 만들어내는지, 매니페스트가 어떻게 쿼리 속도를 높이는지, 파티션 스펙을 바꿀 때 데이터를 다시 쓰지 않아도 되는 이유가 무엇인지.


1. 메타데이터 계층 구조 전체

Iceberg 테이블의 모든 접근은 카탈로그(catalog)에서 시작한다. 카탈로그는 테이블 이름과 현재 metadata 파일 경로를 매핑하는 단일 포인터다. AWS Glue, Nessie, Polaris, Hive Metastore, REST Catalog 등이 여기에 속한다.

Catalog
table name →
metadata.json 경로
atomic pointer swap
Metadata File
JSON
스키마·파티션 스펙 이력
스냅샷 목록
current-snapshot-id
Snapshot
snapshot-id, timestamp
manifest-list 경로
operation: append/overwrite/delete
Manifest List
Avro 파일
manifest 파일 목록
파티션별 min/max 요약
added/deleted 파일 수
Manifest File
Avro 파일
데이터 파일 목록
컬럼별 lower/upper bound
null_value_counts
Data Files
Parquet / ORC / Avro
실제 행 데이터
Apache Iceberg 전체 메타데이터 계층

각 계층은 서로를 가리키는 포인터로 연결된다. 한 계층을 바꾸면 상위 계층의 포인터만 새 파일로 교체되고, 하위 파일들은 그대로 재사용된다. 이 불변성(immutability) 설계가 동시성과 time travel을 가능하게 하는 기반이다.


2. Snapshot: 불변의 테이블 뷰

스냅샷(snapshot)은 특정 커밋 시점의 테이블 전체 상태를 나타내는 불변(immutable) 레코드다. 쓰기가 발생할 때마다 새 스냅샷이 생성되고, 이전 스냅샷은 그대로 남는다.

스냅샷이 보유하는 핵심 필드:

필드내용
snapshot-id고유 64비트 정수
timestamp-ms커밋 시각(밀리초)
manifest-list이 스냅샷에 속한 Manifest List 파일 경로
operationappend / overwrite / delete / replace
parent-snapshot-id직전 스냅샷 ID(이력 추적)
summary추가된 파일 수, 삭제된 파일 수, 행 수 변화량

카탈로그는 항상 current-snapshot-id를 가리킨다. 새로운 커밋은 새 metadata 파일을 작성하고 카탈로그의 포인터를 atomic하게 교체한다 — 이 원자적 교체가 ACID의 출발점이다.

s3://my-table/metadata/
├── v1.metadata.json          # 초기 생성
├── v2.metadata.json          # 첫 번째 쓰기 후
├── v3.metadata.json          # 두 번째 쓰기 후 (현재)
├── snap-8157992549605115800-1.avro   # snapshot v2의 manifest list
└── snap-9012345678901234567-1.avro   # snapshot v3의 manifest list

Time Travel

스냅샷이 보존되므로 과거 시점 조회가 가능하다:

-- Spark: 스냅샷 ID 지정
SELECT * FROM prod.orders
  VERSION AS OF 8157992549605115800;

-- Spark: 타임스탬프 지정 (해당 시각 직전의 가장 최신 스냅샷)
SELECT * FROM prod.orders
  TIMESTAMP AS OF '2026-06-15 00:00:00';

-- Trino
SELECT * FROM orders
  FOR SYSTEM_TIME AS OF TIMESTAMP '2026-06-15 00:00:00 UTC';

3. Manifest List: 스냅샷과 Manifest 사이의 인덱스

Manifest List는 Avro 파일로, 해당 스냅샷을 구성하는 Manifest 파일들의 목록을 담는다. 단순한 경로 목록이 아니라 각 Manifest에 대한 요약 통계를 함께 저장한다.

Manifest List 한 행(= Manifest 하나)의 주요 필드:

필드내용
manifest_pathManifest 파일 경로
partition_spec_id이 Manifest가 사용한 파티션 스펙 ID
added_rows_count이 Manifest로 새로 추가된 행 수
existing_rows_count이전 스냅샷에서 이어온 행 수
deleted_rows_count삭제 표시된 행 수
partitions파티션별 lower_bound / upper_bound 배열

마지막 partitions 필드가 핵심이다. 쿼리 엔진은 Manifest List만 읽어도 어떤 Manifest를 건너뛸 수 있는지 판단한다. Manifest 수가 수백 개라도, 쿼리 조건에 맞는 파티션 범위가 없는 Manifest는 아예 열지 않는다.


4. Manifest File: 컬럼 통계와 File Skipping

Manifest File도 Avro 파일이다. 각 행은 데이터 파일(Parquet/ORC 등) 하나를 나타내고, 그 파일에 대한 상세 메타데이터를 담는다.

데이터 파일 항목의 주요 필드:

필드내용
file_path데이터 파일 경로
file_formatPARQUET / ORC / AVRO
record_count파일 내 전체 행 수
file_size_in_bytes파일 크기
column_sizes컬럼별 압축 후 바이트 크기
value_counts컬럼별 전체 값 개수
null_value_counts컬럼별 null 개수
lower_bounds컬럼별 최솟값 (field ID → bytes 매핑)
upper_bounds컬럼별 최댓값 (field ID → bytes 매핑)
statusADDED / EXISTING / DELETED

File Skipping의 작동 방식

SELECT * FROM orders
WHERE order_date = '2026-06-30' AND customer_id > 100000;

쿼리 플래너가 Manifest File을 읽을 때 각 데이터 파일의 lower_bounds['order_date'], upper_bounds['order_date']를 확인한다. 해당 파일에 2026-06-30 데이터가 없으면(upper_bound < '2026-06-30') 그 파일을 완전히 건너뛴다. customer_id에 대해서도 마찬가지다. 파일을 열어 footer를 파싱하는 비용 없이, 메타데이터만으로 pruning이 이루어진다.

이 최적화를 데이터 파일 스킵(data file skipping) 또는 min/max pruning 이라고 부른다. Manifest List → Manifest File → Data File 세 단계에서 단계별로 불필요한 파일을 줄여나간다.


5. Hidden Partitioning: 쿼리에 파티션 컬럼을 쓰지 않아도 되는 이유

기존 Hive 파티셔닝에서는 파티션 컬럼이 물리적 폴더 경로에 반영된다:

# Hive 방식: 폴더 구조가 파티션
s3://events/year=2026/month=06/day=30/part-000.parquet

이 방식에서 WHERE event_time >= '2026-06-30' 쿼리가 파티션 pruning을 받으려면 반드시 WHERE year=2026 AND month=06 AND day=30으로 써야 한다. 논리 컬럼(event_time)과 파티션 컬럼(year, month, day)이 달라서 사용자가 직접 변환해야 한다.

Iceberg의 Hidden Partitioning은 파티션 값을 폴더 경로가 아닌 Manifest 메타데이터에 저장한다. 파티션 스펙은 소스 컬럼과 변환 함수(transform)로 정의된다.

제공하는 Transform 함수

Transform예시결과
year(col)year(event_time)2026
month(col)month(event_time)2026-06
day(col)day(event_time)2026-06-30
hour(col)hour(event_time)2026-06-30-14
bucket[N](col)bucket[32](user_id)0~31 해시 버킷
truncate[W](col)truncate[1000](amount)1000 단위로 내림
identity(col)identity(region)값 그대로

사용자가 WHERE event_time >= '2026-06-30'을 쓰면 Iceberg 플래너가 자동으로 day(event_time) 파티션 조건으로 변환해서 해당 파티션 외의 Manifest를 건너뛴다. 사용자가 파티셔닝 방식을 신경 쓰지 않아도 pruning이 동작한다.


6. Partition Evolution: 데이터를 다시 쓰지 않고 파티셔닝을 바꾼다

데이터가 쌓이면서 파티셔닝 전략을 바꾸고 싶어지는 경우가 생긴다. 처음에 월(month) 단위로 파티셔닝했지만 데이터가 많아져서 일(day) 단위로 바꿔야 할 때, Hive라면 테이블 전체를 다시 써야 한다.

Iceberg는 다르다. ALTER TABLE 한 줄이면 된다:

-- 파티션 스펙 진화: month → day
ALTER TABLE events ADD PARTITION FIELD day(event_time);
ALTER TABLE events DROP PARTITION FIELD month(event_time);

이 명령은 metadata 파일 하나만 바꾼다. 데이터 파일은 하나도 이동하거나 재작성하지 않는다.

Mixed-Spec Table: 어떻게 동작하는가

spec_id = 0 (기존: month 파티션)
Manifest A
2026-01 ~ 2026-05 데이터
month(event_time) 기준 통계
Manifest B
2026-06-01 ~ 2026-06-15 데이터
month(event_time) 기준 통계
spec_id = 1 (신규: day 파티션)
Manifest C
2026-06-16 ~ 2026-06-30 데이터
day(event_time) 기준 통계
Manifest D
2026-07-01~ 신규 데이터
day(event_time) 기준 통계
WHERE event_time = '2026-06-20'
spec_id=0 Manifests → month 범위로 pruning → Manifest B 후보
spec_id=1 Manifests → day 범위로 pruning → Manifest C 후보
두 결과를 합산해 반환 — 사용자에게는 단일 쿼리
Partition Evolution — Mixed-Spec Table 구조와 쿼리 처리

Manifest File의 partition_spec_id 필드가 어느 스펙을 사용했는지 기록한다. 쿼리 플래너는 Manifest를 spec_id별로 그룹화하고, 각 그룹에 맞는 pruning 논리를 적용한 뒤 결과를 합친다.

운영 참고: Evolution 후 이전 데이터도 compaction 시 새 스펙으로 자동 마이그레이션된다. 강제로 바꾸려면 CALL catalog.system.rewrite_data_files(table => 'schema.table')를 사용한다.


7. Schema Evolution: 컬럼 ID 기반 추적

Iceberg는 컬럼을 이름이 아닌 내부 정수 ID로 추적한다. 컬럼 이름이 바뀌어도 파일 포맷 수준에서는 ID로 매핑하므로 기존 파일이 깨지지 않는다.

지원하는 schema evolution 연산:

연산영향
컬럼 추가 (ADD COLUMN)이전 파일에서는 null로 반환
컬럼 삭제 (DROP COLUMN)이전 파일의 데이터는 무시, 신규 파일에서 제외
컬럼 이름 변경 (RENAME COLUMN)ID는 유지, 이름만 변경 — 기존 파일 영향 없음
타입 확장 (int → long)암묵적으로 안전한 업캐스팅만 허용
컬럼 재정렬물리적 순서 변경 없이 논리 순서만 변경

모든 schema change는 새 metadata 파일을 기록하는 metadata-only 연산이다. 실제 데이터 파일을 건드리지 않는다.


8. OCC: 동시 쓰기 충돌 해결

Iceberg는 Optimistic Concurrency Control(OCC)를 사용한다. 두 쓰기 작업이 동시에 발생할 때:

  1. 두 작업 모두 현재 스냅샷을 기준으로 데이터 파일을 쓴다.
  2. 둘 다 새 metadata 파일을 생성한다.
  3. 카탈로그에 포인터를 교체할 때 먼저 성공한 쪽이 이긴다.
  4. 늦은 쪽은 CommitFailedException을 받고 최신 스냅샷 기준으로 재시도한다.

충돌이 없는 경우(서로 다른 파티션에 append)는 재시도 없이 항상 성공한다. 충돌 재시도가 빈번하다면 파티셔닝 전략을 검토해야 한다는 신호다.


마무리: Iceberg를 운영할 때 알아야 할 것

Iceberg의 구조는 우아하지만 새로운 운영 과제를 만든다.

  • 메타데이터 증가: 매 커밋마다 새 metadata.json, snapshot, manifest가 생긴다. expire_snapshots를 주기적으로 실행해 오래된 스냅샷과 고아(orphan) 파일을 정리해야 한다.
  • Compaction 필요: 스트리밍 쓰기로 누적된 작은 파일은 rewrite_data_files 프로시저로 주기적으로 합친다.
  • Catalog 안정성: Catalog가 단일 장애점이다. REST Catalog나 Nessie라면 그 가용성 자체가 테이블 접근의 전제 조건이다.
  • Partition evolution 후 Compaction: 이전 스펙 Manifest가 많이 남아있으면 플래너가 두 스펙 모두 처리해야 해서 planning이 느려질 수 있다. Compaction을 실행하면 이전 스펙 파일들이 새 스펙으로 다시 쓰여 Manifest가 단순해진다.

References

  • Apache Iceberg, Official Table Specification, https://iceberg.apache.org/spec/
  • Apache Iceberg, Partitioning Concepts, https://iceberg.apache.org/docs/latest/partitioning/
  • Apache Iceberg, Evolution Guide, https://iceberg.apache.org/docs/latest/evolution/
  • OLake, "Apache Iceberg Metadata Explained: Snapshots & Manifests," https://olake.io/blog/2025/10/03/iceberg-metadata/
  • Dremio, "Apache Iceberg Partition Evolution: Change Your Partitioning Strategy Without Rewriting Data," https://www.dremio.com/blog/apache-iceberg-partition-evolution-change-your-partitioning-strategy-without-rewriting-data/
  • Iceberglakehouse, "Apache Iceberg Schema Evolution and Hidden Partitioning Explained," https://iceberglakehouse.com/apache-iceberg-schema-evolution/
  • Iceberglakehouse, "Partition Evolution in Apache Iceberg," https://iceberglakehouse.com/iceberg/iceberg-partition-evolution/
  • Alex Merced, "Understanding the Apache Iceberg Manifest File," https://dev.to/alexmercedcoder/understanding-the-apache-iceberg-manifest-file-581d
  • Dremio, "A Hands-On Look at the Structure of an Apache Iceberg Table," https://www.dremio.com/blog/a-hands-on-look-at-the-structure-of-an-apache-iceberg-table/
  • Sendoa Moronta, "Apache Iceberg Metadata Internals: How Manifests, Snapshots and Metadata Trees Enable Lineage at Scale," https://medium.com/@sendoamoronta/apache-iceberg-metadata-internals-how-manifests-snapshots-and-metadata-trees-enable-lineage-at-8715d0afd4c2
  • Conduktor, "Iceberg Table Architecture: Metadata and Snapshots," https://www.conduktor.io/glossary/iceberg-table-architecture-metadata-and-snapshots