[Rust Wave] Day 3: 쿼리 엔진의 미래, DataFusion과 아키텍처의 모듈화
서론: 데이터베이스 개발의 민주화
과거에 고성능 쿼리 엔진을 만든다는 것은 Oracle이나 Databricks 같은 거대 기술 기업의 전유물이었다. SQL 파서, 쿼리 옵티마이저, 실행 계획 수립, 스토리지 엔진 연동까지, 밑바닥부터 개발해야 하는 컴포넌트의 난이도가 극도로 높았기 때문이다.
하지만 Rust 생태계는 Apache Arrow DataFusion을 통해 이 패러다임을 바꿨다. Day 1의 Polars와 Day 2의 Delta-RS가 “완성된 자동차”라면, DataFusion은 누구나 고성능 자동차를 조립할 수 있는 “모듈형 엔진 블록”이다.
2026년 현재, 수많은 신생 데이터베이스(Vector DB, Time-series DB)들이 바닥부터 엔진을 짜는 대신 DataFusion을 채택하고 있다. 오늘은 이 DataFusion의 아키텍처와, 이것이 어떻게 데이터 엔지니어링 도구들의 파편화를 막고 표준화를 이끄는지 분석한다.
1. DataFusion이란 무엇인가?
DataFusion은 Rust로 작성된, 매우 빠르고 확장 가능한(Extensible) 쿼리 실행 엔진이다. 중요한 점은 이것이 독립적인 서버(Server)라기보다는, 다른 애플리케이션에 내장(Embedded)되어 사용되는 라이브러리(Crate)라는 점이다.
1.1 “LLVM for Databases”
컴파일러 분야에 LLVM이 있다면, 데이터베이스 분야에는 DataFusion이 있다.
- LLVM: 프로그래밍 언어 개발자가 프론트엔드만 만들면, 중간 코드(IR) 최적화와 기계어 생성은 LLVM이 담당한다.
- DataFusion: 데이터베이스 개발자가 스토리지 레이어만 연결하면, SQL 파싱, 쿼리 최적화, 벡터화된 실행(Vectorized Execution)은 DataFusion이 담당한다.
이러한 특성 덕분에 엔지니어들은 쿼리 처리 로직을 재발명할 필요 없이, 자신의 비즈니스 로직(예: 시계열 처리, 벡터 검색 등)에만 집중할 수 있게 되었다.
2. 아키텍처: Arrow-Native Execution
DataFusion의 모든 실행 과정은 Apache Arrow 메모리 포맷을 중심으로 돌아간다.
2.1 실행 파이프라인 (Life of a Query)
- SQL / DataFrame API: 사용자의 입력을 받는다.
- Logical Plan: 입력을 논리적인 연산 트(Scan, Filter, Aggregate)로 변환한다. 이 단계에서는 데이터가 어디에 있는지, 어떻게 읽을지 고민하지 않는다.
- Optimizer: 논리적 계획을 최적화한다. (예: Constant Folding, Predicate Pushdown, Join Reordering)
- Physical Plan: 실제 실행 가능한 계획으로 변환한다. 여기서 “어떤 파일 포맷을 읽을지”, “어떤 병렬 처리 전략을 쓸지” 결정된다.
- Execution: Arrow 데이터를 스트리밍 방식으로 처리하며 결과를 반환한다.
2.2 Volcanic vs Vectorized
전통적인 DB(PostgreSQL, MySQL 등)는 한 번에 한 행(Row)씩 처리하는 Volcano 모델을 사용했다. 반면 DataFusion은 철저히 벡터화된 실행(Vectorized Execution)을 따른다.
- Batch Processing: 데이터를 RecordBatch(Arrow Array의 묶음) 단위로 처리한다.
- SIMD: Polars와 마찬가지로 최신 CPU의 병렬 연산 기능을 극대화한다.
3. 확장성: Everything is a Trait
DataFusion이 Spark Catalyst(Scala 기반 옵티마이저)보다 강력한 점은 Rust의 Trait 시스템을 활용한 극단적인 모듈화다. 개발자는 엔진의 거의 모든 부분을 자신의 입맛대로 갈아끼울 수 있다.
3.1 Custom Data Source (TableProvider)
DataFusion은 기본적으로 Parquet, CSV, JSON, Avro를 지원한다. 하지만 만약 사내 독점 포맷이나 REST API 결과를 쿼리하고 싶다면?
TableProvider트레이트(Trait)만 구현하면 된다. DataFusion은 데이터가 어디서 오는지 신경 쓰지 않고, 단지Scan메서드를 통해 Arrow 배치를 받아올 수만 있으면 된다.
3.2 Custom Optimizer (OptimizerRule)
기본 옵티마이저가 마음에 들지 않거나, 특정 도메인에 특화된 최적화가 필요한가?
- 사용자는 실행 파이프라인 중간에 자신만의
OptimizerRule을 주입할 수 있다. 예를 들어, 특정 지리 공간(Geospatial) 함수가 보이면 인덱스 스캔으로 바꿔치기하는 규칙을 직접 추가할 수 있다.
4. 활용 사례: Spark 가속화와 Vector DB
DataFusion의 위력은 이를 기반으로 만들어진 도구들에서 증명된다.
4.1 Apache Spark의 가속기: Comet
Comet은 Spark의 실행 엔진을 DataFusion으로 교체하는 프로젝트다.
- Spark 사용자는 코드를 전혀 바꿀 필요가 없다.
- Comet은 Spark의 Physical Plan을 가로채서(Intercept), JVM 대신 Rust 기반의 DataFusion 엔진에서 실행한다. 이를 통해 기존 Spark 대비 2~4배의 성능 향상을 이뤄냈다.
4.2 차세대 데이터베이스들
- LanceDB: 벡터 데이터베이스. 스토리지 포맷은 자체적인 Lance 포맷을 쓰지만, SQL 필터링과 벡터 연산의 실행 계획 수립은 DataFusion을 사용한다.
- Delta-RS: 어제 다룬 Delta-RS의 Python 바인딩 내부에서도 복잡한 필터링 조건을 처리할 때 DataFusion의 표현식(Expression) 엔진을 사용한다.
5. 코드 예시: 10줄로 만드는 SQL 엔진
Rust 코드 몇 줄이면 고성능 SQL 엔진이 탄생한다. 이것이 DataFusion이 가져온 혁신이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use datafusion::prelude::*;
#[tokio::main]
async fn main() -> datafusion::error::Result<()> {
// 1. 실행 컨텍스트 생성 (SparkSession과 유사)
let ctx = SessionContext::new();
// 2. Parquet 파일 등록 (Lazy Loading)
ctx.register_parquet(
"taxi_trips",
"s3://bucket/nyc_taxi.parquet",
ParquetReadOptions::default(),
).await?;
// 3. SQL 실행 (Arrow 포맷으로 병렬 처리됨)
let df = ctx.sql(
"SELECT passenger_count, MIN(fare_amount)
FROM taxi_trips
GROUP BY passenger_count"
).await?;
// 4. 결과 출력
df.show().await?;
Ok(())
}
6. 요약 및 시사점
| 특징 | Spark Catalyst | DataFusion |
|---|---|---|
| 언어 | Scala | Rust |
| 메모리 | JVM Heap / Off-heap | Apache Arrow (Native) |
| 확장성 | 어려움 (내부 API 의존) | 매우 쉬움 (Trait 기반) |
| 용도 | Spark 전용 | 범용 쿼리 엔진 라이브러리 |
| 생태계 | Hadoop/Spark 중심 | Rust, Python, WASM, Embedded |
결론적으로, DataFusion은 “데이터 처리의 모듈화”를 완성했다. 이제 데이터 엔지니어들은 거대한 모놀리식 플랫폼에 종속되지 않고, 필요한 부품(DataFusion, Arrow, Delta-RS)을 조립하여 자신만의 최적화된 데이터 파이프라인과 인프라를 구축할 수 있게 되었다.
배치 처리는 이렇게 Rust가 정복했다. 그렇다면 실시간 스트리밍(Streaming) 영역은 어떨까? 내일은 Flink의 독주를 위협하는 Rust 기반의 스트리밍 데이터베이스, RisingWave를 다룬다.