스키마 대통합: Polars와 Iceberg 사이엔 언제나 PyArrow가 있다

Category
Python
Tags
Published
February 9, 2026
Last updated
Last updated February 9, 2026
데이터 엔지니어링을 하다 보면 늘 마주치는 고민이 있습니다. "Polars로 가공하고 Iceberg에 저장해야 하는데, 스키마 정의는 어디서 맞춰야 하지?"
각 라이브러리(Polars, PyIceberg, DuckDB 등)는 저마다의 스키마 정의 방식을 가지고 있습니다. 하지만 걱정할 필요 없습니다. 이 모든 혼란을 잠재우는 **절대적인 표준(Base Standard)**이 존재하기 때문입니다. 바로 **Apache Arrow(PyArrow)**입니다.
오늘은 PyArrow가 어떻게 이 생태계의 '공용어' 역할을 하는지, 그리고 각 라이브러리의 스키마 정의가 디테일하게 어떻게 다른지 코드로 비교해 보겠습니다.

1. 왜 PyArrow가 '기준(Base)'인가?

결론부터 말하면, 모든 데이터 이동은 Arrow를 통하기 때문입니다.
  • Polars: 내부 메모리 포맷으로 Arrow를 사용합니다.
  • PyIceberg: 데이터를 읽고 쓸 때 Arrow 포맷을 기본으로 사용합니다.
  • Zero-Copy: Arrow를 기준점(Base)으로 삼으면, 데이터를 변환할 때 메모리 복사가 발생하지 않습니다. (속도 저하 0)
즉, PyArrow 스키마만 확실히 잡아두면, Polars로의 변환이나 Iceberg로의 저장은 '공짜'나 다름없습니다.

2. 라이브러리별 스키마 정의 방식 비교 (Code Level)

세 라이브러리가 같은 데이터를 정의할 때, 코드 레벨에서 어떤 디테일 차이가 있는지 살펴봅시다.

① PyArrow: "가장 엄격하고 디테일한 표준"

가장 기초가 되는 정의 방식입니다. 데이터 타입의 물리적인 크기(bit width)부터 Null 허용 여부, 메타데이터까지 가장 상세하게 제어할 수 있습니다.
Python
import pyarrow as pa # PyArrow Schema: 엔지니어링의 정석 (Base) arrow_schema = pa.schema([ # 1. 필드명, 타입, Null 여부를 명시적으로 제어 pa.field("id", pa.int64(), nullable=False), # 2. 메타데이터 주입 가능 (문서화 용도 등) pa.field("name", pa.string(), metadata={"description": "User full name"}), # 3. 중첩된 구조(Nested)도 명확하게 정의 pa.field("properties", pa.struct([ pa.field("version", pa.int32()), pa.field("source", pa.string()) ])) ])

② Polars: "분석을 위한 실용주의"

Polars는 Arrow를 기반으로 하되, **'사용자 편의성'**에 초점을 맞춥니다. 복잡한 설정보다는 데이터 타입(Class) 위주로 간결하게 선언합니다.
Python
import polars as pl # Polars Schema: Arrow와 1:1 매핑되지만 훨씬 간결함 polars_schema = pl.Schema({ "id": pl.Int64, # Arrow의 int64로 자동 매핑 "name": pl.String, # Arrow의 string으로 자동 매핑 "properties": pl.Struct({ "version": pl.Int32, "source": pl.String }) })
특징: nullable 같은 제약조건보다는 **데이터의 형태(Shape)**와 연산 속도에 집중합니다.

③ PyIceberg: "저장과 진화를 위한 설계도"

가장 큰 차이점은 **field_id**입니다. Iceberg는 데이터가 쌓이고 스키마가 변하는(Evolution) 상황을 대비해야 하므로, 모든 컬럼에 고유 번호를 부여합니다.
Python
from pyiceberg.schema import Schema from pyiceberg.types import NestedField, IntegerType, StringType, StructType # PyIceberg Schema: 관리와 이력을 위한 ID 중심 iceberg_schema = Schema( # field_id=1 : 이 번호가 메타데이터 관리의 핵심! NestedField(field_id=1, name="id", field_type=IntegerType(), required=True), NestedField(field_id=2, name="name", field_type=StringType(), required=False), # 구조체 내부 필드에도 각각 ID가 부여됨 (3, 4, 5...) NestedField(field_id=3, name="properties", field_type=StructType( NestedField(field_id=4, name="version", field_type=IntegerType(), required=False), NestedField(field_id=5, name="source", field_type=StringType(), required=False) ), required=False) )

3. 상호 운용성: Arrow를 통한 대통합

이 세 가지 스키마가 서로 다르다고 겁먹을 필요가 없습니다. PyArrow가 가운데 버티고 있기 때문에 변환은 물 흐르듯 자연스럽습니다.

Case 1: Iceberg 데이터를 Polars로 분석하기

Python
# 1. Iceberg 테이블 스캔 (결과는 Arrow Table) arrow_table = iceberg_table.scan().to_arrow() # 2. Polars로 변환 (Zero-Copy!) df = pl.from_arrow(arrow_table)

Case 2: Polars 가공 데이터를 Iceberg 스키마로 맞추기

Python
# 1. Polars 데이터프레임을 Arrow로 변환 arrow_table = df.to_arrow() # 2. PyIceberg의 유틸리티를 사용해 Arrow 스키마를 Iceberg 스키마로 변환 from pyiceberg.io.pyarrow import pyarrow_to_schema # Arrow 스키마를 넣으면 Iceberg Schema 객체가 튀어나옴 (ID 자동 할당 등 처리) new_iceberg_schema = pyarrow_to_schema(arrow_table.schema)

🔚 결론

데이터 파이프라인을 설계할 때 "어느 라이브러리의 스키마를 써야 하지?"라고 고민된다면 정답은 간단합니다.
  1. 데이터 교환의 표준은 PyArrow입니다.
  1. 분석 코드를 짤 때는 편한 **Polars*를 쓰세요.
  1. 저장소(Data Lake)를 구축할 때는 **PyIceberg*의 ID 체계를 따르세요.
하지만 기억하세요. 이 모든 것의 바닥에는 PyArrow가 깔려 있습니다. PyArrow 스키마를 이해하는 것이 곧 모던 데이터 엔지니어링의 시작입니다.