[Rust Wave] Day 2: Spark 없이 Delta Lake 다루기, Delta-RS와 Python 바인딩
서론: “작은 작업에 Spark는 너무 무겁다”
Delta Lake는 ACID 트랜잭션, 타임 트래블, 스키마 검증을 제공하며 모던 데이터 플랫폼의 표준 스토리지 포맷으로 자리 잡았다. 하지만 지금까지 Delta Lake를 다루기 위한 유일한 입장권은 Apache Spark였다.
단순히 S3에 있는 1GB짜리 Delta 테이블을 읽거나, 몇 개의 행(Row)을 추가하기 위해 JVM을 띄우고, Spark 세션을 초기화하고, 무거운 의존성(JARs)을 관리하는 것은 비효율적이다. 특히 AWS Lambda나 Fargate 같은 경량화된 컨테이너 환경에서 Spark의 느린 부팅 속도(Cold Start)와 높은 메모리 점유율은 치명적인 단점이다.
Rust로 작성된 Delta-RS는 Spark(JVM)에 대한 의존성을 완전히 제거했다. 이제 Python 프로세스 단 하나만으로 Delta Lake의 모든 기능을 네이티브 성능으로 제어할 수 있다.
1. 아키텍처의 변화: JVM Bypass
기존의 PySpark 방식과 Delta-RS(Python 바인딩) 방식의 아키텍처 차이는 명확하다.
1.1 기존 방식 (PySpark)
Python 코드는 단순히 JVM에 명령을 전달하는 래퍼(Wrapper)일 뿐이다.
- 경로: Python Script -> Py4J -> JVM (Spark Driver) -> Spark Executor -> Delta JAR -> File System.
- 문제점: 데이터가 Python과 JVM 사이를 오갈 때 직렬화/역직렬화 오버헤드가 발생하며, 디버깅 시 JVM 스택 트레이스를 분석해야 하는 복잡함이 있다.
1.2 새로운 방식 (Delta-RS)
delta-rs는 Delta Lake 프로토콜을 Rust로 직접 구현한 라이브러리다. Python 패키지인 deltalake는 이 Rust 코어에 대한 바인딩이다.
- 경로: Python Script -> Rust Binding (Native Code) -> File System.
- 이점: JVM이 전혀 필요 없다.
pip install deltalake만으로 설치가 끝나며, 실행 시 즉각적인(Sub-second) 응답 속도를 보여준다.
2. 핵심 기능과 코드: 읽기, 쓰기, 트랜잭션
Delta-RS는 단순히 파일을 읽는 것을 넘어, Delta Log(_delta_log)를 직접 파싱하고 트랜잭션을 생성(Commit)한다.
2.1 데이터 읽기 (Pandas/Arrow 변환)
Delta 테이블을 읽어 즉시 Pandas DataFrame이나 PyArrow Table로 변환할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
from deltalake import DeltaTable
# JVM 없이 S3에서 직접 메타데이터 로드
dt = DeltaTable("s3://my-bucket/delta-table/")
# 타임 트래블: 특정 버전의 데이터 로드
df = dt.load_version(10).to_pandas()
# 조건부 로드 (Partition Pruning 적용)
df_filtered = dt.to_pandas(filters=[("date", "=", "2026-02-03")])
2.2 데이터 쓰기 (ACID Transaction)
Spark 없이도 동시성 제어(Concurrency Control)가 보장되는 쓰기 작업이 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
import pandas as pd
from deltalake import write_deltalake
df = pd.DataFrame({"id": [1, 2], "value": ["a", "b"]})
# Append 모드로 쓰기 (트랜잭션 로그 생성 포함)
write_deltalake(
"s3://my-bucket/delta-table/",
df,
mode="append",
partition_by=["date"]
)
이 과정에서 _delta_log 디렉토리에 JSON 커밋 파일이 생성되며, Spark에서 조회해도 완벽하게 호환된다.
3. Polars와의 시너지: Zero-Copy Integration
Day 1에서 다룬 Polars와 Delta-RS의 결합은 로컬 데이터 엔지니어링의 정점이다. 두 라이브러리 모두 메모리 포맷으로 Apache Arrow를 사용하기 때문에, 데이터를 주고받을 때 복사(Copy)가 발생하지 않는다.
Polars는 최근 read_delta 기능을 내장(Native Support)하거나 deltalake 라이브러리와 연동하여 스캔 성능을 극대화했다.
1
2
3
4
5
6
7
8
9
10
11
12
import polars as pl
# 1. Delta Log 스캔 (Rust) -> 2. Arrow 포인터 전달 -> 3. Polars LazyFrame 생성
lf = pl.scan_delta("s3://my-bucket/large-table/")
# Polars의 Query Optimizer가 작동하여 필요한 데이터만 가져옴
result = (
lf.filter(pl.col("status") == "active")
.select(["id", "revenue"])
.collect()
)
이 파이프라인은 수 GB의 데이터를 처리할 때도 수 초 내에 완료되며, 메모리 사용량은 Spark 대비 1/10 수준이다.
4. 활용 사례 및 한계
Spark를 버리고 Delta-RS를 선택해야 하는 시점은 언제인가?
4.1 적합한 사용 사례 (Sweet Spot)
- AWS Lambda / Cloud Functions: 실행 시간과 메모리 제약이 있는 서버리스 환경에서 Delta 테이블을 조회하거나 업데이트해야 할 때.
- Airflow/Dagster Operators: 무거운 SparkSubmitOperator 대신 PythonOperator 내에서 가볍게 ETL을 수행할 때.
- 데이터 애플리케이션: Streamlit이나 FastAPI 백엔드에서 실시간으로 Delta Lake 데이터를 서빙해야 할 때.
4.2 한계 (When to use Spark)
- 대규모 셔플링 (Shuffling): 수 TB 데이터를 조인(Join)하거나 집계(Aggregation)해야 하는 경우, 분산 처리가 가능한 Spark가 여전히 유리하다. Delta-RS는 단일 노드(Single Node) 성능을 극대화하는 도구임을 명심해야 한다.
5. 요약
| 특징 | PySpark | Delta-RS (Python Binding) |
|---|---|---|
| 런타임 | JVM + Python | Native Rust |
| 시동 시간 | 수 초 ~ 수 분 (JVM Warm-up) | 즉시 (Milliseconds) |
| 메모리 | High (JVM Heap Overhead) | Low (Efficient Arrow handling) |
| 설치 | JDK, Spark Home 등 복잡 | pip install deltalake |
| 주 용도 | 대규모 분산 배치 처리 | 경량 ETL, 서버리스, 앱 백엔드 |
결론적으로, Delta-RS는 “Delta Lake = Spark”라는 공식을 깼다. 데이터 엔지니어는 이제 작업의 규모에 따라 도구를 선택할 수 있다. 무거운 트럭(Spark)이 필요한 곳도 있지만, 빠르고 민첩한 오토바이(Delta-RS)가 훨씬 효율적인 구간이 존재한다.
이제 우리는 Rust 기반으로 데이터를 메모리에 올리고(Polars), 저장소와 통신하는(Delta-RS) 방법을 알았다. 그렇다면 이 모든 것을 아우르는 쿼리 연산의 핵심 엔진은 무엇일까? 내일은 Rust 데이터 생태계의 심장인 DataFusion에 대해 알아본다.