Lakehouse Table Format — Understanding Apache Iceberg Through Its Metadata
Old files still read after a rename because of column IDs
한국어 원문으로 표시합니다.
한 줄 요약
Iceberg 는 열마다 바뀌지 않는 필드 ID 를 주고 데이터 파일에도 그 ID 를 적어 둔다. 읽을 때는 이름이 아니라 ID 로 짝을 맞추므로, 열 추가·삭제·이름 바꾸기·순서 바꾸기·형 넓히기가 모두 metadata 한 줄로 끝나고 옛 파일은 다시 쓰지 않는다.
왜 스키마 변경이 사고였나
데이터 레이크에서 스키마를 바꾸는 일은 오래 두려운 일이었다. 원인은 파일 속 열을 무엇으로 찾느냐에 있다.
이름으로 찾는 표(Hive 식 Parquet 표 대부분)에서 amount 를 amount_krw 로 바꾸면, 옛 파일에는 amount 라는 열만 있으니 새 이름으로 읽을 때 그 열이 통째로 null 이 된다. 더 나쁜 일도 있다. coupon 을 지웠다가 몇 달 뒤 다른 뜻의 coupon 을 새로 만들면, 옛 파일에 남은 옛 coupon 값이 되살아나 새 열에 섞여 나온다.
위치로 찾는 형식(CSV, 순서 기반 Hive 표)은 반대 방향으로 깨진다. 세 번째 열을 지우면 네 번째 열이 세 번째 자리로 당겨져 이름과 값이 어긋난다.
Evolution 문서가 바로 이 두 실패를 든다. 이름으로 추적하는 형식은 이름을 다시 쓰면 지운 열을 되살릴 수 있고, 위치로 추적하는 형식은 열을 지우면 다른 열에 쓰이는 이름이 바뀐다. 그래서 팀들은 스키마를 바꿀 때마다 표 전체를 다시 쓰거나, 아예 바꾸지 못하고 틀린 이름을 몇 년씩 안고 살았다.
어떻게 동작하나 — ID 로 짝을 맞춘다
표를 만들면 열마다 ID 가 붙는다(order_id 1, customer_id 2, …). 쓰는 엔진은 Parquet 파일의 열 정의에 그 ID 를 field_id 로 함께 적는다. 스키마는 metadata 에 목록으로 쌓이고 각자 schema-id 를 가지며, 스냅샷은 자기를 만들 때의 스키마 ID 를 기억한다.
열 투영 규칙은 간단하다. 데이터 파일의 열은 필드 ID 로 고른다. 표 스키마에 있는데 파일에 없는 ID 는 (이름 매핑·기본값이 없으면) null 로 채운다. 이 규칙 하나에서 다섯 가지 변경이 모두 안전해진다.
| 변경 | metadata 에서 일어나는 일 | 옛 파일을 읽으면 |
|---|---|---|
| 추가 | 새 ID 를 가진 필드가 생긴다 | 그 ID 가 없으니 null |
| 이름 바꾸기 | 같은 ID 의 이름만 바뀐다 | 파일 속 옛 이름과 ID 로 이어져 값이 그대로 |
| 삭제 | 지금 스키마에서 그 ID 가 빠진다 | 파일에 값이 남아도 읽지 않는다 |
| 순서 바꾸기 | 필드 순서만 바뀐다 | ID 로 고르니 값이 섞이지 않는다 |
| 넓히기 | 형이 넓은 쪽으로 바뀐다 | 좁은 형으로 쓰인 값을 넓혀 읽는다 |
지운 열과 같은 이름으로 다시 더한 열은 새 ID 를 받는다. 옛 파일에는 옛 ID 의 값만 있으니 새 열에는 아무것도 나타나지 않는다. 문서가 보장하는 "추가된 열은 다른 열의 기존 값을 절대 읽지 않는다" 가 이것이다.
형은 넓히기만 된다
스펙의 스키마 진화 절은 형식 판 1·2 에서 허용되는 형 바꾸기를 셋으로 못박는다 — int → long, float → double, decimal(P, S) → decimal(P', S)(P' > P, 정밀도만 넓히기). 판 3 에서는 date → timestamp 같은 몇 가지가 더해진다. 전부 값이 잘리지 않는 방향이다. long 을 int 로 되돌리는 것은 이미 파일에 있는 큰 값이 잘릴 수 있어 허용되지 않고, 엔진은 그 변경을 거절한다.
현장에서 만나는 모습
이름이 틀린 열을 고치기. amout 같은 오타 열을 몇 년 안고 사는 표가 흔하다. Iceberg 에서는 이름 바꾸기가 metadata 한 줄이라 그날 고칠 수 있다. 다만 파일을 직접 여는 소비자(필드 ID 를 모르는 스크립트)는 여전히 옛 이름을 본다는 것을 알려야 한다.
금액 열이 int 를 넘쳤다. 원 단위 금액이 21억을 넘는 날이 온다. int → long 넓히기는 파일을 다시 쓰지 않고 바로 된다. 반대로 '용량 줄이려고' long 을 int 로 바꾸려는 요청은 거절되는 것이 맞다.
열을 지웠다 다시 만들었더니 옛 값이 섞였다. 이름으로 읽던 옛 표에서 흔히 나던 사고다. Iceberg 로 옮긴 뒤에는 같은 이름으로 다시 만들어도 새 열은 비어서 시작한다.
실무에서 진짜 중요한 것
- ID 가 열의 정체성이고 이름은 표시일 뿐이다. 이름 바꾸기·순서 바꾸기는 언제든 안전하다.
- 스키마 변경은 데이터 파일을 다시 쓰지 않는다. 스냅샷도 늘지 않고 metadata 에 스키마가 하나 쌓일 뿐이다.
- 형은 넓히기만 된다. int → long, float → double, decimal 정밀도 넓히기.
- 지운 열의 ID 는 돌아오지 않는다. 같은 이름으로 다시 만든 열은 빈 새 열이다.
다음 실습에서 할 것
표를 만들어 3월 1일을 넣고, coupon 열을 더한 뒤 3월 2일을 넣는다. amount 를 amount_krw 로 바꾼 뒤 첫 커밋의 Parquet 파일 꼬리말을 pyarrow 로 직접 열어 옛 이름 amount 와 field_id 4 가 그대로 있는 것을 확인한다. amount_krw 를 BIGINT 로 넓히고 INT 로 되돌리는 것이 거절되는 오류 조건을 적고, coupon 을 지웠다 다시 더해 새 ID 와 비어 있는 값을 확인한 뒤, 스키마는 여럿 쌓였어도 스냅샷과 데이터 파일은 그대로인 것을 센다.