Lakehouse Table Format — Understanding Apache Iceberg Through Its Metadata
One table, many engines — the spec is the contract and the catalog is the meeting point
한국어 원문으로 표시합니다.
한 줄 요약
Iceberg 표는 스펙대로 쓰인 metadata 와 데이터 파일이라서 Spark 가 만든 표에 파이썬이 쓰고 DuckDB 가 읽을 수 있다. 다만 이 약속은 모두가 같은 카탈로그에서 '지금' metadata 를 찾고, 같은 스펙 기능과 타입을 이해할 때만 지켜진다.
왜 여러 엔진인가
배치 변환은 Spark 가, 작은 적재와 점검 스크립트는 파이썬이, 분석가의 즉석 질의는 DuckDB 가 편하다. 예전에는 엔진마다 표를 따로 두고 복사했다. 복사본은 늘 어긋나고, 어느 쪽이 진짜인지 다투게 된다. Multi-Engine Support 문서는 Iceberg 를 어느 처리 엔진이든 쓸 수 있는 열린 표준이라고 소개한다. 표가 파일 형식이 아니라 스펙이 정한 metadata 로 정의되기 때문에 가능한 일이다.
같은 문서는 조건도 보여 준다. Spark·Flink 는 엔진 판마다 런타임 jar 가 따로 나오고, 지원 목록에 없는 판에는 쓸 수 없다. 이 코스의 실습 이미지가 저장소의 다른 Spark 코스(4.2)와 달리 Spark 4.1.3 을 쓰는 이유가 그것이다 — Iceberg 1.11.0 의 런타임은 4.1 까지다.
어떻게 동작하나 — 만나는 자리와 어긋나는 자리
카탈로그가 만나는 자리다. 실습 환경에서는 Spark 와 pyiceberg 가 같은 SQLite 카탈로그를 연다. 둘 다 조건부 교체로 커밋하니 서로의 커밋을 덮지 않는다. DuckDB 의 iceberg 확장은 두 방식을 준다. metadata 를 가리켜 직접 읽는 방식은 카탈로그가 필요 없고 읽기 전용이며, 쓰기까지 하려면 REST 카탈로그를 ATTACH 한다.
직접 읽기는 그 순간에 고정된다. metadata 파일은 한 번 쓰이면 바뀌지 않으므로 iceberg_scan('…/00003-….metadata.json') 은 언제 실행해도 그때의 표를 돌려준다. 누군가 그 뒤에 커밋해도 경고는 없다. DuckDB 문서는 파일 이름으로 '최신' 판을 추측하는 기능이 ACID 를 깰 수 있어 기본으로 꺼져 있다고 적는다. 지금을 읽으려면 매번 카탈로그에서 현재 경로를 다시 찾아야 한다.
타입이 어긋나는 자리. 스펙의 원시 타입은 시간대 없는 timestamp 와 UTC 로 저장하는 timestamptz 를 구별한다. Spark 의 TIMESTAMP 는 timestamptz 가 된다. 그래서 파이썬에서 시간대 없는 시각으로 같은 열에 쓰려 하면 pyiceberg 가 스키마가 맞지 않는다고 거절한다(실습 이미지에서 확인). 시각에 UTC 를 붙이는 것이 맞다. 날짜 경계도 UTC 로 잘린다는 것을 기억해 둔다.
기능이 어긋나는 자리. 형식 판은 옛 읽는 쪽이 새 기능을 바르게 읽지 못할 때 올라간다. 스펙은 엔진이 아직 구현하지 않은 기능을 피하려고 옛 판으로 계속 쓸 수 있다고 적는다. 판 3 의 deletion vector 나 새 타입을 쓰기 전에, 그 표를 읽는 모든 엔진이 지원하는지 확인해야 한다. 삭제 파일을 모르는 엔진은 지운 행을 돌려준다.
잘 맞는 자리. 열은 필드 ID 로 고르므로 한 엔진에서 열 이름을 바꾸면 다른 엔진도 새 이름으로 옛 파일의 값을 읽는다. 파티션 값은 매니페스트에 있으니 누가 썼든 같은 조건으로 건너뛴다.
누가 썼는가 — 스냅샷 요약
엔진이 여럿이면 '이 커밋은 어디서 왔나' 를 알아야 할 때가 온다. 스냅샷 요약이 단서다. 실습 이미지에서 보면 Spark 는 요약에 engine-name(spark)·engine-version·app-id 를 남기고, pyiceberg 0.12 는 남기지 않는다. 요약은 쓰는 쪽이 채우는 것이라 엔진마다 다르다는 점도 함께 기억해 둔다.
현장에서 만나는 모습
대시보드가 어제 숫자에 멈췄다. 누군가 BI 도구에 metadata.json 경로를 박아 두었다. 표는 매일 커밋되는데 그 도구는 첫날의 표를 읽고 있었다. 도구가 카탈로그를 거치게 하거나, 매번 현재 경로를 찾게 바꾼다.
파이썬 적재가 스키마 오류로 멈췄다. CSV 에서 읽은 시각이 시간대 없는 값이었다. 시간대를 붙이면 된다. 이 오류는 고맙게 받아들여야 한다 — 조용히 넣었다면 아홉 시간 어긋난 값이 섞였을 것이다.
새 엔진을 붙인다. 카탈로그를 지원하는가, 어떤 형식 판·삭제 형식을 읽는가, timestamptz 를 어떻게 다루는가를 먼저 확인한다. 셋 중 하나라도 어긋나면 결과가 조용히 틀린다.
실무에서 진짜 중요한 것
- 카탈로그는 하나, '지금' 은 매번 카탈로그에서. metadata 경로를 붙들면 낡는다.
- timestamp 와 timestamptz 를 구별한다. Spark 의 TIMESTAMP 는 timestamptz 다.
- 형식 판과 삭제 형식은 가장 느린 엔진에 맞춘다.
- 이름 바꾸기처럼 ID 로 풀리는 변경은 모든 엔진에 그대로 보인다.
다음 실습에서 할 것
Spark 가 날짜로 나눈 표를 만들어 일주일 치를 넣고, pyiceberg 가 시간대 있는 시각으로 하루를 더 쓴다. DuckDB 로 지역별 합계를, pyiceberg 로 조건 읽기를 한 뒤, 옛 metadata 경로를 적어 두고 Spark 가 하루를 더 커밋하게 해 옛 경로와 새 경로를 DuckDB 로 각각 읽어 본다. Spark 에서 열 이름을 바꿔 다른 두 엔진이 새 이름으로 읽는지 확인하고, 마지막으로 Spark 가 세 엔진이 쓴 행을 모두 모아 일별 표를 만든다.