데이터 엔지니어 인터뷰 — Apache Iceberg

기본 개념

테이블 포맷이 왜 필요한가요

오브젝트 스토리지에 Parquet 파일만 쌓아두면 여러 문제가 생깁니다.

  • 파일 목록으로 테이블을 정의하므로 디렉터리 리스팅이 병목이 됩니다. S3에서 수만 개 파일을 나열하는 것은 느리고 비쌉니다
  • 원자성이 없습니다. 쓰는 도중에 읽으면 반쯤 쓰인 상태가 보입니다
  • 스키마 변경, 삭제·갱신이 어렵습니다
  • 동시 쓰기 시 충돌을 감지할 방법이 없습니다

Iceberg는 메타데이터로 파일 목록을 관리해 이 문제들을 해결합니다. 테이블이 “이 디렉터리 아래 파일들”이 아니라 “이 스냅샷이 가리키는 파일들”이 됩니다.

Iceberg의 메타데이터 구조를 설명해보세요

카탈로그
  └── metadata.json          현재 스냅샷 포인터, 스키마, 파티션 스펙 이력
        └── manifest list    스냅샷 하나가 참조하는 매니페스트 목록 (+ 파티션 범위 통계)
              └── manifest   데이터 파일 목록 (+ 파일별 컬럼 min/max, null count)
                    └── data files (Parquet/ORC/Avro)

쿼리 플래닝은 이 계층을 위에서 아래로 가지치기하며 내려갑니다. 매니페스트 리스트의 파티션 범위로 매니페스트를 걸러내고, 매니페스트의 컬럼 통계로 데이터 파일을 걸러냅니다. 파일을 열지 않고도 대부분을 건너뛸 수 있습니다.

스냅샷과 원자적 커밋은 어떻게 동작하나요

쓰기는 새 데이터 파일을 쓰고, 새 매니페스트와 매니페스트 리스트를 만든 뒤, 마지막에 카탈로그의 현재 metadata 포인터를 원자적으로 교체합니다. 이 교체가 성공해야 새 스냅샷이 보입니다.

읽는 쪽은 시작 시점의 스냅샷을 고정해서 읽으므로, 쓰기가 진행 중이어도 일관된 결과를 봅니다(snapshot isolation).

동시 쓰기는 낙관적 동시성 제어로 처리합니다. 커밋 시점에 기반 스냅샷이 바뀌었으면 실패하고 재시도합니다. 서로 다른 파티션을 건드리는 쓰기는 대개 재시도로 해결되지만, 같은 파일을 건드리면 충돌이 반복될 수 있습니다.

주요 기능

히든 파티셔닝(hidden partitioning)이란 무엇인가요

Hive 방식에서는 파티션 컬럼이 실제 컬럼으로 존재하고, 쿼리에서 그 컬럼을 명시해야만 프루닝이 됩니다.

-- Hive: dt 컬럼을 따로 만들어 두고 조건에 넣어야 함
WHERE dt = '2026-08-12' AND event_time >= '2026-08-12 00:00:00'

Iceberg는 변환식을 파티션 스펙에 저장합니다. days(event_time)으로 파티셔닝했다면, 사용자가 event_time으로 조건을 걸어도 엔진이 알아서 파티션을 프루닝합니다.

-- Iceberg: 원본 컬럼 조건만으로 프루닝됨
WHERE event_time >= '2026-08-12'

파티션 컬럼을 별도로 만들지 않아도 되고, 사용자가 파티션 구조를 몰라도 됩니다.

파티션 진화(partition evolution)는 무엇인가요

파티션 전략을 바꿔도 기존 데이터를 다시 쓰지 않아도 됩니다. 과거 데이터는 옛 스펙으로, 이후 데이터는 새 스펙으로 저장되고, 쿼리는 각각에 맞는 방식으로 프루닝합니다.

-- 일별에서 시간별로 변경. 과거 데이터 재작성 없음
ALTER TABLE events REPLACE PARTITION FIELD days(event_time) WITH hours(event_time);

Hive에서는 파티션 구조를 바꾸려면 테이블을 다시 만들고 전체를 재적재해야 했습니다.

스키마 진화는 어떻게 안전한가요

Iceberg는 컬럼을 이름이 아니라 ID로 추적합니다. 그래서 이름을 바꿔도 데이터가 그대로 매핑되고, 컬럼 순서를 바꿔도 안전합니다.

지원되는 변경은 추가, 삭제, 이름 변경, 순서 변경, 타입 확대(int → long 등)입니다. 삭제한 컬럼과 같은 이름으로 새 컬럼을 추가해도 다른 ID를 받으므로 옛 데이터가 되살아나지 않습니다.

Parquet 파일에 컬럼 이름으로 매핑하는 방식(Hive)에서는 이 보장이 없어서, 컬럼을 지웠다 같은 이름으로 추가하면 옛 값이 섞여 나오는 사고가 생깁니다.

타임 트래블은 어떻게 쓰나요

스냅샷이 남아 있는 한 과거 시점의 테이블을 그대로 읽을 수 있습니다.

-- 스냅샷 ID 로
SELECT * FROM events VERSION AS OF 3821550127947089612;

-- 시각으로
SELECT * FROM events TIMESTAMP AS OF '2026-08-11 00:00:00';

-- 스냅샷 목록 조회
SELECT * FROM events.snapshots;

잘못된 배치가 돌았을 때 이전 스냅샷으로 되돌리는 것도 가능합니다.

CALL catalog.system.rollback_to_snapshot('db.events', 3821550127947089612);

다만 스냅샷 만료 정책과 충돌합니다. 오래된 스냅샷을 정리하면 그 시점으로는 못 돌아갑니다.

삭제와 갱신

행 수준 삭제는 어떻게 구현되어 있나요

두 가지 방식이 있습니다.

Copy-on-Write (CoW) — 삭제 대상이 포함된 데이터 파일을 통째로 다시 씁니다. 읽기는 빠르지만(추가 처리 없음) 쓰기가 비쌉니다. 조회가 많고 갱신이 드문 테이블에 적합합니다.

Merge-on-Read (MoR) — 삭제 정보를 별도 delete 파일에 기록하고, 읽을 때 병합합니다. 쓰기는 빠르지만 읽을 때 비용이 붙습니다. CDC처럼 갱신이 잦은 테이블에 적합합니다.

delete 파일에도 두 종류가 있습니다. position delete는 “이 파일의 N번째 행”을 가리키고, equality delete는 “이 키를 가진 행”을 가리킵니다. 후자는 스트리밍 upsert에서 쓰이는데, 읽을 때 조인 비용이 커서 주기적 컴팩션이 필수입니다.

MERGE INTO는 어떻게 쓰나요

MERGE INTO target t
USING source s ON t.id = s.id
WHEN MATCHED AND s.op = 'D' THEN DELETE
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;

소스에 같은 키가 두 번 이상 있으면 실패합니다. CDC 피드를 그대로 MERGE에 넣으면 한 키의 여러 변경이 들어오므로, 미리 키별 최신 1건만 남겨야 합니다.

-- 키별 최신 변경만 추리기
WITH latest AS (
  SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY id ORDER BY ts DESC) rn FROM changes
  ) WHERE rn = 1
)
MERGE INTO target t USING latest s ON t.id = s.id ...

운영

유지보수 작업에는 어떤 것들이 있나요

Iceberg는 자동으로 정리하지 않습니다. 주기적으로 돌려야 하는 작업이 있습니다.

작업 내용
rewrite_data_files 작은 파일을 합침(컴팩션). MoR delete 파일도 병합
expire_snapshots 오래된 스냅샷과 그 스냅샷만 참조하던 파일 삭제
remove_orphan_files 실패한 쓰기가 남긴, 어떤 메타데이터도 참조하지 않는 파일 삭제
rewrite_manifests 매니페스트를 재구성해 플래닝 속도 개선
CALL catalog.system.rewrite_data_files(table => 'db.events');
CALL catalog.system.expire_snapshots(table => 'db.events', older_than => TIMESTAMP '2026-08-01 00:00:00');

remove_orphan_files동시에 쓰기가 진행 중이면 위험합니다. 아직 커밋되지 않은 파일을 고아로 판단할 수 있으므로 충분한 유예 시간을 둬야 합니다.

스토리지 비용이 계속 늘어납니다. 왜일까요

스냅샷이 만료되지 않아 옛 데이터 파일이 계속 남아 있는 경우가 대부분입니다. CoW 테이블이면 갱신할 때마다 파일이 통째로 새로 쓰이므로 누적이 빠릅니다.

expire_snapshots를 돌리되, 타임 트래블 요구사항과 균형을 맞춰야 합니다. 보통 7~30일 정도로 둡니다.

카탈로그는 어떤 것을 쓰나요

테이블의 현재 metadata 위치를 원자적으로 갱신해주는 주체가 필요합니다.

  • Hive Metastore — 기존 자산과 호환. 널리 쓰임
  • AWS Glue — AWS 환경에서 관리형
  • REST Catalog — 표준 인터페이스. 엔진 중립적이라 최근 선호됨
  • Nessie — 브랜치·태그 개념으로 데이터 버전 관리
  • JDBC / Hadoop — 단순하지만 운영 기능이 약함

Hadoop catalog(파일시스템 기반)는 S3에서 원자적 rename이 보장되지 않아 동시 쓰기에 안전하지 않습니다. 운영에는 권장되지 않습니다.

Iceberg, Delta Lake, Hudi를 비교해보세요

  Iceberg Delta Lake Hudi
출신 Netflix Databricks Uber
파티션 진화 지원 미지원 미지원
히든 파티셔닝 지원 미지원 미지원
엔진 중립성 높음 Spark 중심(개선 중) Spark/Flink 중심
강점 대규모 테이블, 스키마·파티션 진화 Spark 생태계 통합, 사용 편의 인덱스 기반 빠른 upsert

셋 다 ACID, 타임 트래블, 스키마 진화를 제공합니다. 선택은 대개 쓰는 엔진과 클라우드 환경에 따라 갈립니다.

언제 Iceberg가 과한 선택일까요

테이블이 작고, 갱신이 없고, 하루 한 번 전체를 덮어쓰는 정도라면 Parquet + 파티션 디렉터리로 충분합니다. Iceberg는 메타데이터 관리와 유지보수 잡이라는 운영 부담을 함께 가져옵니다.

갱신·삭제가 필요하거나, 파일 수가 많아 리스팅이 느려지거나, 동시 쓰기가 있거나, 스키마가 자주 바뀌는 상황에서 값을 합니다.