Apache Iceberg

데이터 엔지니어 · AI 엔지니어를 위한 핵심 요약
Lakehouse Table Format Open Source ACID Schema Evolution

1. Apache Iceberg란?

Apache Iceberg는 대규모 분석 데이터셋을 위한 고성능 오픈 테이블 포맷입니다. 데이터 레이크 상에서 안정적이고 안전한 트랜잭션(ACID), 숨겨진 파티셔닝, 스키마 진화, 시간 여행(Time Travel) 등의 엔터프라이즈급 기능을 제공합니다.

Hive/Spark의 기존 테이블 포맷의 한계를 해결하며, 데이터 레이크하우스 아키텍처의 사실상 표준으로 자리잡았습니다.

ACID 트랜잭션

다중 동시 쓰기 작업에서도 데이터 무결성을 보장합니다. 스냅샷 격리(Snapshot Isolation)를 통해 읽기/쓰기 충돌을 방지하고, 롤백이 가능합니다.

스키마 & 파티션 진화

런타임 중단 없이 컬럼 추가/삭제/이름 변경, 데이터 타입 변경이 가능합니다. 파티션 스키마도 기존 데이터를 재작성하지 않고 변경할 수 있습니다.

고성능 쿼리

파일 수준 통계(통계, null 카운트 등)와 플래닝 최적화를 통해 불필요한 파일 스캔을 제거합니다. 벡터화된 읽기와 필터 푸시다운을 지원합니다.

2. 왜 써야 하는가?

기존 데이터 레이크 포맷(Hive, Delta Lake 등)과 비교한 Iceberg의 장점입니다.

기능 Apache Hive Delta Lake Apache Iceberg
ACID 트랜잭션 제한적 (파티션 수준) ⭕ (Databricks 런타임) ⭕ (오픈 소스)
Hidden Partitioning ❌ ❌ ⭕
스키마 진화 제한적 (컬럼 추가만) ⭕ ⭕ (가장 유연함)
시간 여행 ❌ ⭕ (로그 보존 기간 제한) ⭕ (스냅샷 기반, 유연한 보존)
벤더 종속성 낮음 높음 (Databricks 최적화) 낮음 (진정한 오픈 소스)
성능 최적화 기본적 좋음 탁월 (파일 통계, 플래닝)

핵심 선택 이유

Hidden Partitioning: 사용자는 파티션 키를 몰라도 효율적으로 필터링 가능합니다. 파티션 필드 변경 시 데이터 재작성 불필요.
멀티 엔진 지원: Spark, Flink, Trino, Hive, Presto 등 다양한 컴퓨팅 엔진에서 일관된 방식으로 작동합니다.
테이블 메타데이터 구조 JSON/Manifest/Data 파일의 계층적 구조로 메타데이터 확장성과 안정성을 보장합니다.

3. 핵심 개념

Iceberg의 아키텍처는 3계층 메타데이터로 구성되어 있으며, 이를 통해 효율적인 스냅샷 관리와 쿼리 플래닝이 가능합니다.

┌─────────────────────────────────────────────────────────────┐ │ Query Engine (Spark, Flink, Trino) │ └──────────────────────────────┬──────────────────────────────┘ │ ┌──────────────────────────────▼──────────────────────────────┐ │ Iceberg Catalog (Hive, Nessie, JDBC) │ └──────────────────────────────┬──────────────────────────────┘ │ (현재 스냅샷 ID 참조) ┌──────────────────────────────▼──────────────────────────────┐ │ Metadata Layer (JSON) │ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │ │ │ Metadata │ │ Snapshot │ │ Schema │ │ │ │ File │◄─┤ Info │ │ & Partition │ │ │ │ (Versioned) │ │ │ │ Spec │ │ │ └─────────────┘ └─────────────┘ └──────────────────┘ │ └──────────────────────────────┬──────────────────────────────┘ │ (Manifest 목록) ┌──────────────────────────────▼──────────────────────────────┐ │ Manifest Layer (Avro) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Manifest │ │ Manifest │ │ Manifest │ │ │ │ List │──► File │──► Entry │ │ │ │ │ │ │ │ (파일 경로, │ │ │ │ │ │ │ │ 통계) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └──────────────────────────────┬──────────────────────────────┘ │ (실제 데이터 파일) ┌──────────────────────────────▼──────────────────────────────┐ │ Data Layer (Parquet, ORC, Avro) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Data │ │ Data │ │ Data │ │ │ │ File 1 │ │ File 2 │ │ File N │ │ │ │ (Row Group, │ │ (Row Group, │ │ (Row Group, │ │ │ │ Statistics)│ │ Statistics)│ │ Statistics)│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘

주요 용어

4. 퀵스타트

로컬 환경에서 PySpark를 사용해 Iceberg 테이블을 생성하고 조작하는 기본 흐름입니다.

4.1 환경 설정 (PySpark)

# pip 설치
pip install pyspark==3.5.0 pyiceberg

# 또는 Spark 세션 빌드 시 패키지 추가 (권장)
pyspark --packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.5.0

4.2 Spark 세션 생성

from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("Iceberg Quickstart") \
    .config("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions") \
    .config("spark.sql.catalog.local", "org.apache.iceberg.spark.SparkCatalog") \
    .config("spark.sql.catalog.local.type", "hadoop") \
    .config("spark.sql.catalog.local.warehouse", "/tmp/iceberg-warehouse") \
    .getOrCreate()

4.3 테이블 생성 및 데이터 쓰기

# 테이블 생성 (CTAS)
spark.sql("""
CREATE TABLE local.db.users (
    id bigint,
    name string,
    created_at timestamp,
    country_code string
) USING iceberg
PARTITIONED BY (days(created_at), country_code)
""")

# 데이터 삽입
spark.sql("""
INSERT INTO local.db.users VALUES
(1, 'Alice', '2024-01-15 10:00:00', 'US'),
(2, 'Bob', '2024-01-15 11:30:00', 'KR'),
(3, 'Charlie', '2024-01-16 09:15:00', 'US')
""")

# MERGE INTO (Upsert) 예제
spark.sql("""
MERGE INTO local.db.users t
USING (SELECT 4 as id, 'David' as name, '2024-01-16 14:00:00' as created_at, 'JP' as country_code) s
ON t.id = s.id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *
""")

4.4 시간 여행 및 스냅샷 관리

# 스냅샷 조회
spark.sql("SELECT * FROM local.db.users.snapshots").show(truncate=False)

# 과거 데이터 조회 (시간 여행)
spark.sql("SELECT * FROM local.db.users VERSION AS OF 1234567890123456789").show()

# 롤백
spark.sql("CALL local.system.rollback_to_snapshot('db.users', 1234567890123456789)")

# 스냅샷 정리 (오래된 메타데이터 삭제)
spark.sql("CALL local.system.expire_snapshots('db.users', TIMESTAMP '2024-01-01 00:00:00')")

5. 핵심 설정 레퍼런스

실제 운영 환경에서 중요한 Iceberg 관련 설정들입니다.

카테고리 설정 키 기본값 설명
쓰기 최적화 write.target-file-size-bytes 536870912 (512MB) 파일 당 목표 크기. 너무 작으면 소스 파일 문제, 너무 크면 읽기 성능 저하.
write.format.default parquet 기본 파일 포맷 (parquet, orc, avro).
write.metadata.delete-after-commit.enabled true 커밋 후 이전 메타데이터 파일 삭제 여부.
읽기 최적화 read.split.open-file-cost 4MB 파일을 열기 위한 예상 비용. 조인 시 성능에 영향.
read.split.planning-lookback 10 스플릿 계획 시 고려할 이전 파일 수.
engine.hive.enabled false Hive 엔진 호환 모드 활성화.
메타데이터 관리 history.expire.max-snapshot-age-ms 5일 스냅샷 만료 최대 나이.
history.expire.min-snapshots-to-keep 1 유지할 최소 스냅샷 수.
commit.retry.num-retries 4 커밋 실패 시 재시도 횟수.
카탈로그 catalog-name.type hadoop 카탈로그 타입 (hadoop, hive, rest, nessie, glue).
catalog-name.warehouse (없음) 데이터 파일과 메타데이터가 저장될 루트 경로.

6. 실무 팁 & 트러블슈팅

팁 1: 파티션 설계

문제: 너무 많은 소규모 파티션(Over-partitioning)으로 인해 메타데이터 파일이 폭발적으로 증가하고 쿼리 계획 수립이 느려짐.
해결: 파티션 당 데이터 크기가 최소 1GB 이상이 되도록 설계. days(ts) 대신 months(ts) 또는 years(ts) 고려. 파티션 필드는 카디널리티가 높지 않은 컬럼 선택.

팁 2: 스냅샷 관리 자동화

문제: 스냅샷이 자동으로 정리되지 않아 스토리지 비용 증가 및 메타데이터 성능 저하.
해결: 정기적(예: 매일)으로 expire_snapshots 프로시저를 실행하는 워크플로우 구성. 보존 정책을 비즈니스 요구사항에 맞게 설정 (예: 30일).

-- Spark SQL
CALL local.system.expire_snapshots(
    table => 'db.my_table',
    older_than => TIMESTAMP '2024-02-01 00:00:00',
    retain_last => 10
)

주의: 동시 쓰기 충돌

증상: 두 개의 작업이 동시에 같은 테이블에 쓰기를 시도할 때 CommitFailedException 발생.
원인: 낙관적 동시성 제어로 인해 한 번에 하나의 쓰기만 커밋 가능.
해결: 쓰기 작업을 직렬화하거나, MERGE INTO를 사용하여 단일 트랜잭션으로 결합. 또는 카탈로그 수준의 잠금 메커니즘(Nessie) 사용 검토.

팁 3: 데이터 파일 최적화

문제: 작은 파일이 많아서(Small file problem) 읽기 성능 저하 및 메타데이터 비대화.
해결: rewrite_data_files 액션을 사용하여 파일을 병합하고 정렬.

-- Spark SQL
CALL local.system.rewrite_data_files(
    table => 'db.my_table',
    strategy => 'sort',
    sort_order => 'id DESC, date ASC'
)

-- 또는 Compaction 실행
CALL local.system.rewrite_data_files('db.my_table')

팁 4: 벤더 간 호환성

문제: Spark로 생성한 테이블을 Trino에서 읽을 때 스키마 불일치 오류.
해결: 항상 명시적 스키마 정의 사용. Spark의 using iceberg와 함께 DDL 사용 권장. 모든 엔진에서 동일한 Iceberg 버전과 런타임 라이브러리 사용 확인.

7. 참고 자료