단일 노드 데이터 엔지니어링: DuckDB, DataFusion, Polars, LakeSail
출처: Dev.to
지난 10년 동안 데이터 엔지니어링은 분산 클러스터와 동의어였다. 데이터셋이 몇 기가바이트를 초과하면 표준 관행은 AWS EMR이나 Databricks에서 Apache Spark 클러스터를 띄우는 것이었다. 이 분산 패러다임은 운영 복잡성을 크게 늘렸다: JVM 설정 관리, 실행자 할당, 셔플 파티션 튜닝, 그리고 네트워크 소켓과 언어 런타임을 가로질러 데이터를 이동시키는 데 드는 막대한 “직렬화 비용” 등이다.
최근 데이터 엔지니어링 환경은 단일 노드 부흥을 맞이하고 있다. 분산 클러스터로 확장하는 대신, 팀들은 단일 머신을 고성능으로 활용하고 있다. 최신 노트북은 12코어 이상, 멀티 기가바이트·초 수준의 읽기 속도를 지원하는 NVMe SSD, 그리고 최대 128 GB RAM을 기본 탑재한다. 클라우드 제공업체도 수백 코어와 테라바이트 수준의 메모리를 가진 단일 가상 머신을 Kubernetes나 Spark 클러스터 비용의 일부만으로 제공한다.
이러한 물리적 하드웨어 변화는 이야기의 절반에 불과하다. 진정한 촉매제는 Apache Arrow, 벡터화 실행, 아웃‑오브‑코어 메모리 관리 위에 구축된 새로운 세대 데이터 기술이다. DuckDB, Apache Arrow DataFusion, Polars, LakeSail 같은 도구들은 단일 노트북이나 VM에서 수백 기가바이트, 심지어 테라바이트 규모의 데이터를 처리할 수 있게 해준다. 이제 복잡한 분석 파이프라인을 분산 JVM 런타임의 오버헤드 없이 로컬 혹은 단일 노드에서 실행할 수 있다.
메모리 상에서 데이터가 어떻게 구조화되는지를 이해해야
단일 노드 데이터 엔지니어링이 수백 대의 클러스터 노드가 필요했던 데이터셋을 처리할 수 있는 이유는 데이터가 메모리에 어떻게 배치되는가에 있다.
전통적인 트랜잭션 워크로드(OLTP)를 위해 설계된 데이터베이스와 실행 엔진은 행 지향(row‑oriented) 레이아웃을 사용한다. 즉, 하나의 레코드에 속한 모든 필드를 메모리 상에 연속적으로 저장한다: [User_ID, Age, Name] → 다음 레코드. 분석 쿼리(OLAP)에서 특정 컬럼만 필요할 경우(예: 사용자 평균 연령 계산) 행 지향 엔진은 전체 레코드 구조를 스캔해야 한다. 이 과정에서 이름이나 ID 같은 필요 없는 데이터까지 CPU의 L1/L2 캐시로 로드되어 캐시 오염과 메모리 대역폭 낭비가 발생한다.
컬럼 지향 쿼리 엔진은 이 비효율을 컬럼 단위로 데이터를 연속 배치함으로써 해결한다: [Age, Age, Age]는 하나의 버퍼에, [Name, Name, Name]은 다른 버퍼에 저장된다. CPU는 쿼리에서 요구하는 컬럼만 읽는다.
행 지향 레이아웃 (OLTP)
┌──────────────────────────────┬──────────────────────────────┐
│ ID 1 │ Age 1 │ Name 1 │ ID 2 │ Age 2 │ Name 2 │
└──────────────────────────────┴──────────────────────────────┘
컬럼 지향 레이아웃 (Arrow/OLAP)
┌──────────┬──────────┐ ┌──────────┬──────────┐ ┌──────────┬──────────┐
│ ID 1 │ ID 2 │ │ Age 1 │ Age 2 │ │ Name 1 │ Name 2 │
└──────────┴──────────┘ └──────────┴──────────┘ └──────────┴──────────┘
Apache Arrow는 이러한 컬럼형 메모리 레이아웃을 표준화한다. 언어에 구애받지 않는 오픈소스 사양을 정의함으로써, Arrow는 공유 메모리 포맷을 제공하고 과거 데이터 파이프라인을 지연시켰던 직렬화 비용을 원천 차단한다.
전통적인 아키텍처에서는 Python 스크립트와 Java 혹은 C++ 엔진 사이에 데이터를 주고받을 때 JSON이나 Protobuf 같은 바이트 스트림으로 직렬화하고, 반대쪽에서 다시 역직렬화해야 했다. 이 과정은 전체 쿼리 실행 시간의 80 %까지 차지하기도 했다.
Arrow는 제로‑카피 IPC(Inter‑Process Communication) 를 가능하게 한다. Arrow가 메모리를 표현하는 방식이 Python, Rust, C++ 모두 동일하기 때문에, 서로 다른 프로세스가 동일 물리 메모리 버퍼를 mmap 으로 공유할 수 있다. 엔진은 데이터셋을 Python에 넘겨 머신러닝이나 시각화에 활용할 때 메모리 포인터만 교환하면 된다. 바이트 복사가 전혀 일어나지 않으며 직렬화도 발생하지 않는다.
또한 Arrow의 연속 메모리 정렬은 현대 CPU 캐시 라인과 일치하므로, SIMD(Single Instruction, Multiple Data) 명령어 집합(예: Intel/AMD의 AVX‑512, ARM의 Neon) 활용이 자연스럽다. SIMD는 하나의 명령어(예: 필터 비교, 산술 덧셈)를 데이터 벡터 전체에 동시에 적용한다. 이 하드웨어 수준 병렬성은 데이터 처리를 메모리·CPU 병목이 아닌, 프로세서 자체에서 효율적으로 수행되는 작업으로 바꾼다.
DuckDB – 단일 노드 SQL 분석의 표준 엔진
DuckDB는 인‑프로세스 분석 데이터베이스로 설계돼, Python 인터프리터나 CLI 바이너리와 같은 호스트 프로세스 내부에서 직접 실행된다. 따라서 PostgreSQL이나 Snowflake와 같은 클라이언트‑서버 DB가 겪는 네트워크 소켓 지연과 IPC 오버헤드가 사라진다.
DuckDB의 실행 엔진은 벡터화된 쿼리 실행 모델을 사용한다. 레코드 하나씩 처리하는 Volcano iterator 모델이나, 전체 컬럼을 한 번에 처리해 L1/L2 캐시를 초과하는 컬럼‑단위 모델과 달리, DuckDB는 작은 캐시 친화적 벡터(보통 2048 요소) 단위로 데이터를 다룬다.
Volcano Model: [Row 1] ──► [Operator] ──► [Row 2] ──► [Operator]
Column-at-a-time: [Entire Column (10M rows)] ──► [Operator] (Overflows Cache)
Vectorized Model: [Vector of 2048 rows] ──► [L1/L2 CPU Cache] ──► [Operator]
벡터가 CPU 캐시 안에 들어갈 정도로 작게 유지되기 때문에, DuckDB는 메모리 대역폭 병목을 최소화한다. CPU는 SIMD 명령어로 벡터를 처리해 파이프라인을 지속적으로 채운다.
아웃‑오브‑코어 실행
데이터셋이 물리 RAM을 초과하면 DuckDB는 아웃‑오브‑코어 실행을 수행한다. 메모리 사용량이 사용자가 정의한 한도에 도달하면, 버퍼 매니저가 중간 결과(해시 조인 테이블, 정렬 버퍼, 집계 상태 등)를 자동으로 임시 디스크 파일에 spill 한다. 이 스필링 메커니즘은 블록 기반 버퍼 풀을 사용해 데이터를 페이지‑폴트 방식으로 디스크에 기록하므로, RAM보다 몇 배 큰 데이터셋도 쿼리할 수 있다.
최신 v1.5.3 릴리스 (2026‑05) – 단일 노드 활용도 확대
- Quack Remote Protocol: 코어 확장으로 Quack 프로토콜을 제공한다. 필요 시 DuckDB를 클라이언트‑서버 형태로 실행해 원격 연결 및 원격 쿼리 오케스트레이션을 지원하면서도 엔진 자체의 단순성을 유지한다.
- Ecosystem & Format Updates: Iceberg 확장이
MERGE INTO연산을 지원하도록 업그레이드돼, 로컬 DuckDB 세션에서 Iceberg 테이블에 복잡한 델타 업데이트를 직접 수행할 수 있다. - AWS Security & IRSA: IAM Roles for Service Accounts (IRSA)를 네이티브 지원해, 컨테이너화된 단일 노드 파이프라인에서 S3 접근을 보다 안전하게 설정한다.
- Static Linking: Linux 배포판이 jemalloc을 정적으로 링크하도록 변경돼, 대규모 아웃‑오브‑코어 스필링 시 메모리 할당 속도가 빨라지고 단편화가 감소한다.
아래 Python 스크립트는 DuckDB 메모리 제한을 설정하고, 새 AWS 확장 기능을 이용해 S