レイクハウスのテーブル形式 — Apache Iceberg をメタデータで理解する
テーブルはディレクトリではなくファイル一覧 — metadata.json からデータファイルまで
한국어 원문으로 표시합니다.
한 줄 요약
Iceberg 표는 디렉터리를 훑어서 알아내는 것이 아니라 metadata.json → 매니페스트 리스트 → 매니페스트 → 데이터 파일로 이어지는 목록 나무로 정의되고, 커밋은 새 나무를 써 둔 뒤 카탈로그의 포인터 한 칸을 원자적으로 바꾸는 일이다.
왜 디렉터리로는 안 되었나
Hive 식 표에서 '표' 는 디렉터리다. orders/order_date=2026-03-01/ 아래에 있는 파일이 곧 그날의 자료이고, 읽는 엔진은 디렉터리를 나열해 파일을 모은다. 이 방식은 단순하지만 세 가지 문제를 안고 있다.
첫째, 원자성이 없다. 쓰는 잡이 파일 열 개 중 다섯 개를 올렸을 때 읽는 잡이 디렉터리를 나열하면 반쪽짜리 결과를 본다. 둘째, 나열이 비싸다. Reliability 문서가 지적하듯 Hive 표는 메타스토어(파티션)와 파일 시스템(파일) 두 곳으로 상태를 추적해서, 잡을 계획할 때마다 파티션 수에 비례하는 목록 조회가 필요하다. 객체 저장소에서는 이 조회가 느리고, 예전에는 결과가 늦게 일관되기까지 했다. 셋째, 무엇이 표인지 합의가 없다. 누군가 실수로 디렉터리에 파일 하나를 떨어뜨리면 그것도 표의 일부가 된다.
어떻게 동작하나 — 네 층의 나무
스펙의 개요는 첫 문장에서 방향을 바꾼다. 이 형식은 디렉터리가 아니라 파일 하나하나를 추적한다. 쓰는 쪽은 파일을 어디에든 써 두고, 명시적인 커밋으로만 표에 더한다.
| 층 | 형식 | 담는 것 |
|---|---|---|
| metadata.json | JSON | 스키마 목록, 파티션 스펙 목록, 속성, 스냅샷 목록, 현재 스냅샷 ID, refs |
| 매니페스트 리스트 | Avro | 스냅샷 하나를 이루는 매니페스트들과 각각의 파티션 요약·파일 수 |
| 매니페스트 | Avro | 데이터(또는 삭제) 파일마다 한 줄: 경로, 파티션 값, 행 수, 열별 하한·상한 |
| 데이터 파일 | Parquet 등 | 실제 행 |
스냅샷은 snapshot-id, 부모를 가리키는 parent-snapshot-id, 커밋 순서를 나타내는 sequence-number, 그리고 자기 manifest-list 경로를 가진다. 요약(summary)의 operation 은 append·replace·overwrite·delete 넷 중 하나이고, 추가한 행 수·파일 수 같은 숫자가 함께 적힌다. 스냅샷의 데이터는 그 매니페스트들에 적힌 살아 있는 파일의 합집합이다.
매니페스트의 한 줄(엔트리)에는 status 가 있다 — 0 EXISTING, 1 ADDED, 2 DELETED. 스캔 계획은 DELETED 항목을 쓰지 않는다. 그리고 두 단계로 건너뛴다. 매니페스트 리스트의 파티션 요약으로 매니페스트를 통째로 건너뛰고, 매니페스트의 열 통계로 파일을 건너뛴다. 디렉터리 나열은 어디에도 없다.
커밋은 무엇을 하나
두 번째 커밋을 생각해 보자. 새 데이터 파일을 쓰고, 그 파일 하나를 담은 새 매니페스트를 쓰고, 첫 커밋의 매니페스트와 새 매니페스트를 함께 가리키는 새 매니페스트 리스트를 쓰고, 스냅샷 하나를 더한 새 metadata.json 을 쓴다. 스펙은 매니페스트가 스냅샷 사이에 재사용된다고 적는다 — 옛 매니페스트는 고쳐 쓰지 않고 가리키기만 한다. 그래서 커밋 비용은 표 크기가 아니라 바뀐 양에 비례한다.
마지막으로 카탈로그에 "현재 metadata 는 이 파일" 이라는 포인터를 원자적으로 바꾼다. 그 순간 이후에 표를 여는 사람은 새 상태를, 그 전에 연 사람은 옛 상태를 끝까지 본다. 반쪽 상태는 없다. 시퀀스 번호는 커밋마다 하나씩 늘고, 경쟁에 져서 다시 커밋할 때는 매니페스트 리스트만 새로 쓰면 되도록 설계되어 있다.
현장에서 만나는 모습
'어제 넣은 자료가 안 보인다'. 파일은 버킷에 있는데 행이 안 보이면 거의 언제나 그 파일이 지금 스냅샷의 매니페스트에 없는 것이다 — 쓰기 잡이 파일만 쓰고 커밋 직전에 죽었다. Iceberg 에서는 이것이 정상 동작이다. 커밋되지 않은 파일은 표의 일부가 아니다.
'디렉터리를 지웠는데 표가 멀쩡하다' 혹은 그 반대. 데이터 파일 경로는 매니페스트에 전체 경로로 적혀 있다. 디렉터리 구조는 관례일 뿐 의미가 없다. 손으로 파일을 지우면 표가 깨지고(매니페스트가 없는 파일을 가리킴), 손으로 파일을 넣으면 아무 일도 일어나지 않는다.
계획이 느리다. 스트리밍으로 1분마다 커밋한 표는 매니페스트가 수천 개가 된다. 쿼리 계획이 느려지는 원인이 데이터가 아니라 메타데이터 층에 있을 수 있다는 것을 알아야 압축과 매니페스트 정리로 이어진다.
실무에서 진짜 중요한 것
- 표의 상태는 metadata.json 하나로 정해진다. 그 파일이 가리키지 않는 것은 표가 아니다.
- 커밋은 새 나무를 쓰고 포인터를 바꾸는 일이다. 옛 나무는 그대로 남아 타임트래블의 재료가 된다.
- 매니페스트는 재사용된다. 두 번째 스냅샷이 첫 매니페스트를 그대로 가리키는 것을 눈으로 확인해 두면 뒤의 압축·만료가 쉽게 이해된다.
- 읽기는 목록과 통계로 건너뛴다. 디렉터리 나열이 없으니 파티션이 많아도 계획 비용이 폭발하지 않는다.
다음 실습에서 할 것
Spark 로 표를 만들고 하루치 주문을 두 번 커밋한다. 그다음 도구 없이 나무를 따라 내려간다. SQLite 카탈로그의 한 줄에서 현재·이전 metadata 경로를 읽고, jq 로 metadata.json 의 스냅샷 목록(부모·시퀀스 번호·매니페스트 리스트)을 뽑고, Avro 로 된 매니페스트 리스트를 풀어 두 번째 스냅샷이 첫 커밋의 매니페스트를 그대로 가리키는지 확인하고, 매니페스트를 풀어 살아 있는 데이터 파일과 행 수를 적는다. 채점기는 적어 낸 값을 실제 메타데이터와 견준다.