Apache Iceberg 완벽 가이드 — 차세대 Lakehouse 테이블 포맷의 모든 것
Apache Iceberg의 등장 배경, 메타데이터 아키텍처, 핵심 기능, 카탈로그 종류, 엔진별 사용법, 운영 자동화, 성능 최적화, 실무 도입 전략까지 한 편으로 정리한 종합 가이드.
데이터 웨어하우스를 쓰다 보면 이런 순간이 옵니다 — 어제 올린 데이터가 쿼리에서 보이지 않거나, 파티션 컬럼이 뭔지 외워야 하거나, 스키마를 바꾸려다 테이블을 통째로 다시 써야 했던 경험. Apache Iceberg는 바로 이런 불편함을 설계 수준에서 없애기 위해 만들어진 오픈 테이블 포맷입니다. 처음 접하는 데이터 엔지니어부터 도입을 고민 중인 아키텍트까지 모두를 위한 종합 가이드를 시작합니다.
이 글에서 배우는 것
- Iceberg가 왜 등장했는지, Hive 방식의 한계가 무엇인지
- 3계층 메타데이터 구조가 어떻게 동작하는지
- 스키마/파티션 진화, 시간 여행, 브랜치·태그 같은 핵심 기능
- Spark·Trino·Flink·PyIceberg에서 실제로 쓰는 방법
- 운영 자동화(컴팩션·스냅샷 만료·고아 정리)의 표준 패턴
1. Apache Iceberg란?
1.1 등장 배경 — Hive 테이블 포맷의 한계
2010년대 빅데이터 생태계의 사실상 표준은 Hive Metastore + 디렉토리 파티셔닝 이었습니다. 단순하고 직관적인 이 구조는 HDFS 시대에는 잘 작동했지만, 클라우드 객체 스토리지(S3, ADLS, GCS)가 표준이 되면서 한계가 드러났습니다. 아래 문제들이 특히 고통스럽게 느껴지셨다면, 여러분의 경험이 Iceberg를 낳은 원인이기도 하죠.
Hive 방식의 구조적 문제:
- 디렉토리 = 파티션 = 쿼리 조건:
WHERE event_date='2026-05-23'가 디렉토리 경로와 정확히 일치해야 Partition Pruning이 동작합니다. 사용자가WHERE ts > ...처럼 표현식을 쓰면 Pruning이 무력화됩니다. - 파티션 진화 불가: "일별 파티션을 시간별로 바꾸자"는 결정이 곧 전체 테이블 재작성을 의미합니다.
- 원자성 부재: S3에서 rename은 copy + delete로 구현되어, 잡 중간 실패 시 부분 가시성 문제가 발생합니다.
- List 비용: 수십만 파일이 있는 prefix에 대한 S3 list 호출은 비싸고 일관성도 약합니다.
- 읽기-쓰기 동시성 부재: 동일 파티션을 쓰는 동안 읽으면 부분 파일이 보일 수 있습니다.
이 한계를 해결하기 위해 Netflix에서 2017년에 시작한 프로젝트가 Apache Iceberg 입니다.
1.2 Iceberg의 핵심 가치
Iceberg는 객체 스토리지 위에 데이터베이스 수준의 트랜잭션과 진화(evolution) 의미론 을 부여하는 개방형 테이블 포맷 입니다.
| 가치 | 설명 |
|---|---|
| ACID 트랜잭션 | Snapshot 기반의 Serializable Isolation |
| Hidden Partitioning | 사용자가 파티션 컬럼을 알 필요 없이 자동 Pruning |
| Schema/Partition Evolution | 재작성 없이 스키마와 파티셔닝 변경 |
| Time Travel | 과거 Snapshot 조회·롤백 |
| Branch / Tag | Git처럼 테이블에 분기와 태그 부여 |
| 엔진 중립적 | Spark, Trino, Flink, Snowflake, BigQuery 등 다중 엔진 지원 |
1.3 Lakehouse 아키텍처에서의 위치
Iceberg는 "스토리지 포맷"이 아니라 "메타데이터 구조" 입니다. Parquet 같은 파일 포맷 위에 트랜잭션과 진화 의미론을 얹는 사양(spec)입니다.
1.4 주요 채택 사례
- Netflix: 원개발사. 수십 페타바이트 규모의 데이터 플랫폼에서 사용.
- Apple: 내부 데이터 플랫폼의 핵심 포맷.
- Airbnb, LinkedIn, Stripe, Pinterest, Adobe: 멀티 엔진 환경에서 채택.
- Snowflake, AWS, Google Cloud, Databricks: 외부 카탈로그·외부 테이블로 Iceberg를 1급 시민으로 지원.
2. Iceberg 아키텍처
2.1 3-Layer 메타데이터 구조
Iceberg는 메타데이터를 3개 계층으로 분리합니다. 마치 책의 목차 → 챕터 목록 → 실제 페이지처럼, 쿼리 엔진은 위에서 아래로 내려가며 필요한 파일만 골라냅니다.
2.2 디렉토리 구성 예시
warehouse/db/orders/
├── data/
│ ├── 00000-0-...parquet
│ └── 00001-0-...parquet
└── metadata/
├── v1.metadata.json
├── v2.metadata.json
├── snap-3051729675574597004-1-...avro # manifest list
└── 0c7a1f8c-...avro # manifest file
| 파일 | 역할 |
|---|---|
metadata.json | 테이블의 현재 상태: 스키마, 파티션 spec, sort order, 현재 snapshot 포인터 |
Manifest List (snap-*.avro) | 한 Snapshot에 속한 모든 manifest의 목록 + 파티션 통계 |
Manifest File (*.avro) | 한 묶음의 데이터 파일 목록 + 컬럼별 통계 (min/max, null count 등) |
Data File (*.parquet) | 실제 데이터 |
2.3 Snapshot 기반 트랜잭션 모델
모든 쓰기 작업은 새로운 Snapshot 을 생성합니다. Snapshot은 불변(immutable)이며, "그 시점의 테이블 상태"를 완전히 표현합니다.
쓰기는 Optimistic Concurrency Control(OCC) 로 직렬화됩니다. Commit 시점에 카탈로그가 atomic compare-and-swap으로 current snapshot pointer를 교체합니다. 충돌이 발생하면 retry합니다.
2.4 Predicate Pushdown의 새로운 차원
쿼리 엔진은 manifest의 컬럼 통계를 보고 파일을 열기 전에 필요 없는 파일을 잘라낼 수 있습니다.
Hive는 list → 모든 파일 오픈 → footer 읽기 였지만, Iceberg는 manifest 한 번 읽기 → 필요한 파일만 오픈 입니다.
3. Iceberg 핵심 기능
3.1 Schema Evolution
Iceberg는 모든 컬럼에 고유 ID 를 부여합니다. 이름이 아니라 ID로 매핑하므로, 컬럼 이름 변경/재배치도 안전합니다.
| 연산 | Hive | Iceberg |
|---|---|---|
| Add column | OK | OK |
| Drop column | 위험 | 안전 |
| Rename column | 위험 | 안전 |
| Reorder column | 위험 | 안전 |
| Type promote (int → long) | 부분 | 안전 |
ALTER TABLE orders ADD COLUMN customer_tier STRING;
ALTER TABLE orders RENAME COLUMN amt TO amount;
ALTER TABLE orders ALTER COLUMN amount TYPE BIGINT;3.2 Hidden Partitioning과 Partition Evolution
Hive에서는 사용자가 WHERE event_date='2026-05-23'처럼 파티션 컬럼을 직접 써야 했습니다. Iceberg는 파티션 변환(transform)을 메타데이터에 저장합니다.
CREATE TABLE orders (
id BIGINT,
ts TIMESTAMP,
amount DECIMAL(10,2)
) USING iceberg
PARTITIONED BY (days(ts)); -- ts에서 일자를 자동 추출
-- 사용자는 ts만 알면 됨
SELECT * FROM orders WHERE ts >= '2026-05-23';지원하는 transform: identity, bucket(N, col), truncate(N, col), year, month, day, hour.
Partition Evolution: 운영 중에 파티션 전략을 바꿀 수 있습니다.
ALTER TABLE orders DROP PARTITION FIELD days(ts);
ALTER TABLE orders ADD PARTITION FIELD hours(ts);기존 파일은 그대로 두고, 신규 쓰기부터 새 전략을 적용합니다.
3.3 Time Travel, Tag, Branch
-- Snapshot ID로 조회
SELECT * FROM orders VERSION AS OF 3051729675574597004;
-- 타임스탬프로 조회
SELECT * FROM orders TIMESTAMP AS OF '2026-05-20 00:00:00';
-- 태그 생성 (영구 보존)
ALTER TABLE orders CREATE TAG `release-2026-Q2`;
-- 브랜치 생성 (실험용)
ALTER TABLE orders CREATE BRANCH experimental;3.4 Row-Level 연산 — CoW vs MoR
| 모드 | 동작 | 쓰기 비용 | 읽기 비용 |
|---|---|---|---|
| Copy-on-Write (CoW) | 영향받은 파일 전체를 다시 씀 | 높음 | 낮음 |
| Merge-on-Read (MoR) | Delete file을 추가, 읽을 때 병합 | 낮음 | 높음 |
CDC처럼 잦은 UPDATE/DELETE에는 MoR, 분석 위주에는 CoW를 권장합니다.
V2 spec에서 도입된 Delete 종류:
- Position Delete: "파일 X의 N번째 row를 삭제"
- Equality Delete: "특정 컬럼이 특정 값인 row를 삭제"
V3에서는 Deletion Vector 가 도입되어 MoR의 효율이 크게 개선됩니다.
4. Iceberg Catalog
4.1 Catalog의 역할
Catalog는 "테이블 이름 → 현재 metadata.json 위치" 매핑을 보관합니다. 쓰기 시 atomic swap을 제공해야 하므로, Iceberg에서 Catalog는 데이터베이스의 트랜잭션 매니저에 해당합니다.
4.2 Catalog 종류
| Catalog | 특징 | 권장 환경 |
|---|---|---|
| Hive Metastore | 기존 HMS 재활용 | Hive 자산이 많은 온프레미스 |
| Hadoop (file-based) | 카탈로그 서버 없이 파일 락 | 개발/테스트만 |
| REST Catalog | 엔진 중립 표준 API | 신규 도입 시 1순위 |
| AWS Glue | AWS 통합 | AWS-native 환경 |
| Nessie | Git 같은 multi-table branching | Data versioning |
| Snowflake / Polaris | 관리형 | Snowflake와 Iceberg 공유 |
| Unity Catalog | Databricks 통합 | Databricks와 Iceberg 공유 |
| JDBC | 관계형 DB 사용 | 간단한 메타스토어 |
4.3 REST Catalog 표준의 의미
REST Catalog는 카탈로그를 언어/엔진 중립 HTTP API 로 정의한 표준입니다. Spark, Trino, Flink, PyIceberg가 모두 동일 API로 접근하므로, 카탈로그 구현체를 자유롭게 교체할 수 있습니다. 2026년 현재 Iceberg 생태계의 사실상 표준은 REST Catalog입니다.
5. Iceberg 사용하기
5.1 Spark
-- Spark 설정 (REST Catalog)
spark.sql.catalog.demo = org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.demo.type = rest
spark.sql.catalog.demo.uri = http://iceberg-rest:8181
spark.sql.catalog.demo.warehouse = s3://my-warehouseCREATE TABLE demo.db.orders (
id BIGINT,
customer_id BIGINT,
ts TIMESTAMP,
amount DECIMAL(10,2)
) USING iceberg
PARTITIONED BY (days(ts));
INSERT INTO demo.db.orders VALUES (1, 100, current_timestamp(), 99.99);
MERGE INTO demo.db.orders t
USING updates u
ON t.id = u.id
WHEN MATCHED THEN UPDATE SET amount = u.amount
WHEN NOT MATCHED THEN INSERT *;5.2 Trino
-- Trino catalog 설정 파일 (iceberg.properties)
connector.name = iceberg
iceberg.catalog.type = rest
iceberg.rest-catalog.uri = http://iceberg-rest:8181SELECT * FROM iceberg.db.orders WHERE ts >= DATE '2026-05-01';
-- 메타데이터 테이블
SELECT * FROM iceberg.db."orders$snapshots";
SELECT * FROM iceberg.db."orders$files";
SELECT * FROM iceberg.db."orders$partitions";5.3 Flink로 스트리밍 적재
CREATE TABLE orders_iceberg (
id BIGINT,
ts TIMESTAMP(3),
amount DECIMAL(10,2)
) WITH (
'connector' = 'iceberg',
'catalog-type' = 'rest',
'uri' = 'http://iceberg-rest:8181',
'warehouse' = 's3://my-warehouse'
);
INSERT INTO orders_iceberg
SELECT * FROM kafka_orders;5.4 PyIceberg
from pyiceberg.catalog import load_catalog
catalog = load_catalog(
"demo",
**{
"type": "rest",
"uri": "http://iceberg-rest:8181",
"warehouse": "s3://my-warehouse",
},
)
table = catalog.load_table("db.orders")
# Pandas로 직접 읽기
df = table.scan(
row_filter="ts >= '2026-05-01'",
selected_fields=("id", "amount"),
).to_pandas()6. 운영과 유지보수
6.1 Compaction — Small File 문제
Flink 스트리밍 적재처럼 커밋이 잦은 환경에서는 작은 파일이 눈덩이처럼 쌓입니다. 수십만 개의 수십 KB짜리 파일은 S3 list 비용과 plan 오버헤드를 폭증시켜 쿼리 성능을 급격히 떨어뜨리죠. 정기적인 컴팩션으로 파일을 합쳐줘야 합니다.
-- Spark Action
CALL demo.system.rewrite_data_files(
table => 'db.orders',
options => map('target-file-size-bytes', '536870912') -- 512MB
);6.2 Snapshot Expiration
Snapshot이 계속 쌓이면 메타데이터가 비대해지고 데이터 파일도 GC되지 않습니다.
CALL demo.system.expire_snapshots(
table => 'db.orders',
older_than => TIMESTAMP '2026-05-01 00:00:00',
retain_last => 10
);6.3 Orphan File 정리
실패한 쓰기 등으로 메타데이터에 참조되지 않는 고아 파일이 생길 수 있습니다.
CALL demo.system.remove_orphan_files(
table => 'db.orders',
older_than => TIMESTAMP '2026-05-01 00:00:00'
);6.4 Metadata Rewrite
Manifest가 너무 많아지면 plan 시간이 늘어납니다.
CALL demo.system.rewrite_manifests(table => 'db.orders');6.5 운영 자동화 표준 패턴
| 작업 | 주기 | 도구 |
|---|---|---|
rewrite_data_files | 시간/일 단위 | Spark Action, Airflow |
expire_snapshots | 일 단위 | Spark Action |
remove_orphan_files | 주 단위 | Spark Action |
rewrite_manifests | 필요 시 | Spark Action |
| 메트릭 수집 | 분 단위 | Catalog API + Prometheus |
위 작업들을 수동으로 챙기기는 어렵습니다. Airflow 같은 스케줄러로 자동화해 두면, 테이블이 스스로 건강을 유지하는 구조를 만들 수 있습니다. 운영 자동화 없이는 Iceberg는 빠르게 "메타데이터 늪"이 됩니다.
7. Iceberg Spec 진화 (V1 → V2 → V3)
| 버전 | 출시 | 핵심 기능 |
|---|---|---|
| V1 | 2019 | Snapshot, Schema/Partition Evolution, Hidden Partitioning |
| V2 | 2021 | Row-level Delete (Position/Equality Delete), MoR |
| V3 | 2025+ | Deletion Vector, Variant 타입, Geospatial, Default values, Row Lineage |
V3의 Deletion Vector는 Position Delete 파일 대신 압축 비트맵을 사용하여, MoR 워크로드의 읽기 성능을 대폭 개선합니다.
호환성 주의:
- 상위 버전 테이블을 하위 버전만 지원하는 엔진은 읽지 못합니다.
- V2 → V3 업그레이드는 일방향입니다.
- 모든 클라이언트(Spark, Trino, Flink) 버전을 미리 확인해야 합니다.
8. Delta Lake / Hudi와의 비교
| 항목 | Iceberg | Delta Lake | Hudi |
|---|---|---|---|
| 원개발사 | Netflix | Databricks | Uber |
| 트랜잭션 로그 | Manifest tree | JSON log | Timeline |
| Hidden Partitioning | OK | 부분 | 부분 |
| Partition Evolution | OK | 미지원 | 미지원 |
| Schema Evolution | 강함 | 강함 | 강함 |
| CoW / MoR | 둘 다 | CoW (DV 도입) | 둘 다 |
| Branch / Tag | OK | 미지원 | 미지원 |
| REST Catalog 표준 | OK | UC 종속 | 미지원 |
| 단일 엔진 친화 | 보통 | 매우 강함(Databricks) | 보통 |
| 멀티 엔진 친화 | 매우 강함 | 보통 (UniForm) | 보통 |
선택 기준:
- Databricks-only 환경 → Delta Lake
- 잦은 Upsert + 스트리밍 → Hudi
- 멀티 엔진, 장기 보존, 진화 의미론 → Iceberg
Delta Lake가 UniForm, Iceberg가 XTable 같은 상호운용 레이어를 도입하면서, 미래에는 포맷 선택이 점점 덜 중요해질 가능성이 큽니다.
9. 성능 최적화 베스트 프랙티스
9.1 파일 크기
너무 작은 파일(<100MB)은 plan 오버헤드, 너무 큰 파일(>2GB)은 병렬성 저하. 보통 256MB~1GB 권장.
ALTER TABLE orders SET TBLPROPERTIES (
'write.target-file-size-bytes' = '536870912'
);9.2 파티셔닝 전략
- 카디널리티가 너무 높은 컬럼으로 파티셔닝하지 마세요 (e.g.
user_id). - 시간 컬럼은 거의 항상
day또는hourtransform 사용. - 고-카디널리티 컬럼은
bucket(N, col)로 처리.
9.3 Sort Order / Z-Order
쿼리 패턴에 맞춰 정렬하면 manifest pruning 효과가 극대화됩니다.
ALTER TABLE orders WRITE ORDERED BY ts, customer_id;9.4 Manifest 분할
대형 테이블에서는 manifest를 파티션별로 분할해 plan 비용을 줄입니다. rewrite_manifests Action 활용.
9.5 안티 패턴
- Snapshot Expiration 미실행 → 메타데이터 폭증
- 컴팩션 미실행 → 수십만 작은 파일
- 너무 잦은 commit → manifest 폭증
- 파티션 컬럼 과다 → list 비용 증가
- Hadoop Catalog 운영 사용 → commit 충돌, 데이터 손실 위험
10. 실무 도입 가이드
10.1 기존 Hive 테이블 마이그레이션
두 가지 방식이 있습니다.
| 방식 | 동작 | 비용 | 롤백 |
|---|---|---|---|
Snapshot (snapshot 프로시저) | 원본 Hive 테이블 유지하면서 Iceberg shadow 생성 | 낮음 | 쉬움 |
Migrate (migrate 프로시저) | 원본을 in-place로 Iceberg로 변환 | 낮음 | 어려움 |
-- 방식 1: 검증용 shadow 생성
CALL demo.system.snapshot('hive_db.orders', 'demo.db.orders');
-- 방식 2: 검증 끝나면 완전 전환
CALL demo.system.migrate('hive_db.orders');10.2 CDC 파이프라인 구축
핵심 포인트:
- Flink Iceberg sink는
upsert모드로 Equality Delete 사용 - 분 단위 commit (너무 자주는 X)
- 야간 배치로 컴팩션 + snapshot expiration
10.3 거버넌스와 보안
- AWS Lake Formation: Glue Catalog + Iceberg에 행/열 수준 권한
- Apache Ranger: Hive/Trino + Iceberg에 정책 기반 인가
- Unity Catalog: Databricks 환경에서 Iceberg 외부 테이블 통제
10.4 비용 최적화
- 컴팩션 파일 크기 조정으로 list/get 호출 수 절감
- Snapshot 보존 기간을 워크로드별로 차등 (시계열 vs 마스터)
- S3 Intelligent-Tiering으로 오래된 Snapshot 데이터 자동 이동
- Manifest 캐시(Spark, Trino)로 plan 시간 단축
11. 마무리
11.1 언제 Iceberg를 선택해야 하는가
다음 중 2개 이상 해당된다면 Iceberg가 명확한 우위입니다.
- 2개 이상의 쿼리 엔진을 동시에 사용한다 (Spark + Trino, Flink + Snowflake 등)
- 장기 보존 데이터에 대한 시간 여행, 법적 정정이 필요하다
- 파티션 전략을 향후 변경할 가능성이 있다
- 실험·재현성을 위한 브랜치/태그가 필요하다
- 특정 벤더(Databricks 등)에 lock-in되고 싶지 않다
11.2 Iceberg 생태계의 미래
- REST Catalog 표준화: 카탈로그가 Lakehouse의 진정한 통제점(control plane) 으로 자리잡고 있습니다.
- V3 확산: Deletion Vector, Variant, Geospatial 지원으로 활용 범위 확대.
- 상호운용성: XTable, UniForm으로 Iceberg ↔ Delta ↔ Hudi 경계가 흐려집니다.
- 관리형 카탈로그 경쟁: Snowflake Polaris, Databricks Unity, AWS Glue, Tabular의 후계자들이 시장을 형성합니다.
11.3 학습 리소스
- Apache Iceberg 공식 문서
- Iceberg Spec v2 / v3
- 본 블로그의 관련 글:
Iceberg는 더 이상 "새로운 옵션"이 아닙니다. 멀티 엔진 Lakehouse의 사실상 표준 이며, 카탈로그 계층이 데이터 플랫폼의 통제점으로 부상하는 흐름의 중심에 있습니다. 도입 자체보다, 운영 자동화와 카탈로그 전략에 더 많은 고민을 투자하시기 바랍니다.