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