https://product.kyobobook.co.kr/detail/S000220221456
LUVIT EPL과 유튜브 데이터로 배우는 DuckDB | 이기준 - 교보문고
LUVIT EPL과 유튜브 데이터로 배우는 DuckDB | 복잡한 데이터 분석 흐름을 더 단순하게 만드는 DuckDB 최근 주목받고 있는 DuckDB를 활용해 SQL 기반 데이터 분석과 실전 프로젝트를 학습할 수 있도록 구
product.kyobobook.co.kr
Apache Spark를 AWS EMR에서 운영하던 환경을 DuckDB를 실행하는 단일 EC2 인스턴스로 전환하면서, 연간 36,000달러 이상의 클라우드 비용을 절감했고 ETL 파이프라인의 실행 속도는 11배 빨라졌습니다.
예산을 조금씩 갉아먹던 기존 환경
우리 데이터 팀은 S3에 저장된 300~800GB 규모의 압축 Parquet 데이터를 대상으로 매일 ETL 작업을 수행하고 있었습니다. 파이프라인은 문서상으로는 단순했습니다. S3에서 데이터를 읽고, 여러 개의 대용량 데이터셋을 조인한 뒤 집계와 정제 작업을 수행하고, 머신러닝용 피처를 생성한 다음 다시 S3에 저장하는 구조였습니다.
이를 처리하기 위해 AWS EMR에서 Spark 클러스터를 사용했습니다. 구성은 마스터 노드 1대와 워커 노드 4대였습니다. 문제는 성능이 아니었습니다. 진짜 문제는 비용과 비효율성이었습니다. 작업은 배치 시간대에만 실행되는데도 클러스터 시작 시간을 줄이기 위해 대부분의 시간을 계속 실행 상태로 유지했습니다. 그 편의성 때문에 AWS 비용은 매달 증가했습니다.
월간 비용은 컴퓨팅, S3 사용료, 모니터링, 로깅, 소규모 애드혹 쿼리 비용까지 포함해 약 4,280달러에 달했습니다. 여기에 Spark 설정, 성능 튜닝, 메모리 오류 디버깅에 투입되는 엔지니어링 시간도 상당했습니다.
놀라운 점은 데이터 규모가 그렇게 크지 않았다는 것입니다. 페타바이트급 데이터를 다루는 것이 아니라 매일 수백 GB 정도를 처리하고 있었기 때문에 Spark는 실제 문제에 비해 지나치게 무거운 도구처럼 느껴졌습니다.
왜 DuckDB를 고려하게 되었을까
DuckDB는 이전부터 관심 있게 지켜보던 도구였습니다. SQLite와 비슷한 인프로세스(in-process) 데이터베이스이지만 트랜잭션이 아니라 분석 작업에 최적화되어 있습니다. 서버도, 클러스터도, 복잡한 설정도 필요 없으며 Python 프로세스 안에서 직접 실행됩니다. pip으로 설치한 뒤 바로 SQL을 실행할 수 있습니다.
본격적으로 테스트하게 된 계기는 한 데이터 엔지니어링 Slack 그룹에서 본 글 때문이었습니다. 어떤 사용자가 Spark ETL 작업을 DuckDB로 교체한 뒤 단일 EC2 인스턴스에서 40배의 성능 향상을 얻었다고 했습니다. 너무 좋은 이야기처럼 들렸기 때문에, 합성 벤치마크가 아니라 실제 운영 환경의 워크로드로 직접 검증해 보기로 했습니다.
목표는 단순했습니다. DuckDB가 단일 머신에서 ETL 파이프라인의 80%만 처리할 수 있어도 인프라 비용을 크게 절감하고 Spark 클러스터 운영에 따른 복잡성을 대부분 제거할 수 있을 것이라 판단했습니다.
벤치마크: 실제 워크로드, 실제 결과
두 가지 환경을 나란히 구성했습니다.
Spark 환경
- 기존 AWS EMR 클러스터
- m5.xlarge 마스터 1대
- m5.2xlarge 워커 4대
- Spark 3.4
- PySpark
- S3의 Parquet 파일 사용
DuckDB 환경
- c6i.4xlarge EC2 인스턴스 1대
- 16 vCPU
- 32GB RAM
- Python 3.11
- DuckDB 0.10
- httpfs 확장을 통해 S3의 Parquet 파일을 직접 조회
실제 운영 중인 대표적인 세 가지 워크로드를 대상으로 테스트를 진행했습니다.
DuckDB가 분석 워크로드에서 이렇게 빠른 이유
이것은 마법이 아닙니다. 이 정도 데이터 규모에서 DuckDB가 Spark보다 뛰어난 성능을 보이는 데에는 분명한 아키텍처적 이유가 있습니다.
컬럼 기반 벡터화 실행(Columnar Vectorized Execution)
DuckDB는 SIMD 명령어를 활용해 데이터를 컬럼 단위의 배치(batch)로 처리합니다. 즉, 하나의 CPU 연산으로 여러 값을 동시에 처리할 수 있습니다. 반면 Spark의 행 기반(Row-based) RDD 모델은 각 단계마다 직렬화(serialization)와 셔플(shuffle) 오버헤드가 발생하며, 이러한 비용이 파이프라인 전반에 걸쳐 누적됩니다.
네트워크 셔플이 없습니다.
Spark에서는 분산 조인을 수행할 때 데이터를 네트워크를 통해 여러 노드로 이동시키는 셔플 과정이 필요합니다. 이 과정은 Spark 작업에서 가장 큰 병목 중 하나입니다. DuckDB는 모든 작업을 단일 머신의 메모리에서 수행하기 때문에 셔플 비용이 전혀 발생하지 않습니다.
시작 오버헤드가 거의 없습니다.
EMR에서 Spark 작업은 실제 코드가 실행되기 전에도 클러스터 시작만으로 일반적으로 3~8분이 소요됩니다. 반면 DuckDB는 밀리초(ms) 단위로 즉시 실행됩니다.
Parquet와 S3를 기본적으로 지원합니다.
DuckDB의 httpfs 확장은 S3에 저장된 Parquet 파일을 Predicate Pushdown과 Column Pruning을 적용하여 직접 읽습니다. 즉, 실제로 필요한 컬럼과 Row Group만 읽습니다. 관리형 쿼리 엔진과 동일한 수준의 효율성을 별도의 관리형 인프라 없이 얻을 수 있습니다.
단일 프로세스의 데이터 지역성(Locality)
데이터가 단일 머신의 메모리에 적재되거나 일부만 디스크로 스필(spill)되는 수준이라면, 메모리를 효율적으로 관리하는 단일 프로세스 엔진이 분산 시스템보다 거의 항상 유리합니다. 분산 처리는 하나의 머신으로 감당할 수 없는 규모가 되었을 때 비로소 가치가 있습니다. 테라바이트 미만의 워크로드에서는 그 한계에 도달하는 경우가 드뭅니다.
전환 이후의 비용
대부분의 파이프라인을 DuckDB로 이전하고 단일 c6i.4xlarge 인스턴스(무거운 작업은 c6i.8xlarge)를 사용하게 되면서 월간 인프라 비용은 크게 달라졌습니다.
항목이전 (Spark/EMR)이후 (DuckDB/EC2)
| 컴퓨팅 | 약 $2,800 | 약 $680 |
| S3 전송 및 요청 | 약 $620 | 약 $510 |
| 모니터링/로깅 | 약 $380 | 약 $90 |
| 기타 컴퓨팅 | 약 $480 | 약 $0 |
| 총합 | 약 $4,280 | 약 $1,280 |
월간 클라우드 비용은 약 4,280달러에서 1,280달러로 줄어 약 70%를 절감했습니다. 연간 기준으로는 36,000달러 이상의 인프라 예산을 절약할 수 있었습니다.
비용 절감뿐만 아니라 운영 부담도 크게 감소했습니다. 더 이상 관리해야 할 클러스터도 없고, Spark 버전 업그레이드를 조율할 필요도 없으며, YARN 리소스 매니저를 디버깅할 일도 없습니다. 파이프라인은 순수한 Python과 SQL 코드로 작성되었고, 팀의 어느 엔지니어라도 쉽게 읽고 수정하고 배포할 수 있게 되었습니다.
실제 마이그레이션 과정
이번 마이그레이션은 주말에 끝낼 수 있는 단기 프로젝트는 아니었습니다. 하지만 개별 작업을 옮기는 과정은 예상보다 훨씬 간단했습니다. 대부분의 PySpark 변환 로직은 DuckDB SQL로 그대로 옮길 수 있었고, 절차적인 변환이 필요한 경우에는 DuckDB와 Polars를 함께 사용하는 방식으로 해결했습니다.
기존의 PySpark 코드는 다음과 같았습니다.
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, sum, avg, count
spark = SparkSession.builder.appName("daily_aggregation").getOrCreate()
df = spark.read.parquet("s3://our-data-lake/transactions/")
result = (
df.filter(col("status") == "completed")
.groupBy("region", "product_category")
.agg(
count("transaction_id").alias("total_orders"),
sum("amount").alias("revenue"),
avg("amount").alias("avg_order_value")
)
)
result.write.parquet("s3://our-data-lake/aggregated/daily/")
spark.stop()
DuckDB에서는 다음과 같이 변경되었습니다.
import duckdb
con = duckdb.connect()
con.execute("INSTALL httpfs; LOAD httpfs;")
con.execute("SET s3_region='us-east-1';")
con.execute("""
COPY (
SELECT
region,
product_category,
COUNT(transaction_id) AS total_orders,
SUM(amount) AS revenue,
AVG(amount) AS avg_order_value
FROM 's3://our-data-lake/transactions/*.parquet'
WHERE status = 'completed'
GROUP BY region, product_category
)
TO 's3://our-data-lake/aggregated/daily/'
(FORMAT PARQUET, PARTITION_BY (region))
""")
DuckDB 버전은 코드가 더 짧고 읽기 쉬우며 실행 속도도 더 빠릅니다. SQL에 익숙한 엔지니어라면 이러한 마이그레이션은 실제로 매우 수월하게 진행할 수 있습니다.
변환 과정 중간에 보다 복잡한 Python 로직이 필요한 작업에서는 DuckDB와 Polars를 함께 사용했습니다. Apache Arrow 기반의 Zero-Copy 데이터 교환을 통해 두 엔진 사이에서 DataFrame을 주고받았습니다.
import duckdb
import polars as pl
con = duckdb.connect()
# DuckDB로 읽기 및 필터링
raw = con.execute("""
SELECT * FROM 's3://our-data-lake/events/*.parquet'
WHERE event_date = CURRENT_DATE - 1
""").pl() # Polars DataFrame 반환
# Polars에서 복잡한 변환 수행
cleaned = (
raw
.with_columns(
pl.col("user_agent")
.str.extract(r"(Mobile|Desktop)", 1)
.alias("device_type")
)
.filter(pl.col("session_duration") > 5)
.sort("event_time")
)
# DuckDB를 통해 결과 저장
con.execute("""
COPY (SELECT * FROM cleaned)
TO 's3://our-data-lake/processed/events/'
(FORMAT PARQUET, COMPRESSION ZSTD)
""")
DuckDB 쿼리 결과에서 사용하는 .pl() 메서드는 Apache Arrow를 통해 직렬화 과정 없이 Polars DataFrame을 반환합니다. 이러한 조합만으로도 기존 PySpark에서 사용하던 거의 모든 패턴을 구현할 수 있었습니다.
여전히 Spark가 필요한 경우
이 부분은 앞에서 설명한 내용만큼 중요합니다. DuckDB는 Spark를 완전히 대체하는 만능 솔루션이 아니며, 그렇다고 주장하는 것은 잘못된 판단으로 이어질 수 있습니다.
다음과 같은 경우에는 Spark를 계속 사용하는 것이 좋습니다.
데이터 규모가 실제로 단일 머신에서 합리적인 시간 안에 처리할 수 있는 수준을 넘어서는 경우입니다. 작업 하나당 여러 테라바이트의 데이터를 정기적으로 처리해야 하고 처리 시간 제약도 엄격하다면, 분산 컴퓨팅 엔진이 적합한 선택입니다. DuckDB는 디스크 스필(spill)을 통해 메모리보다 큰 데이터도 처리할 수 있지만 한계가 있습니다. 쿼리 복잡도에 따라 다르지만 단일 노드에서 약 500GB~1TB를 넘어서면 이러한 한계를 체감하게 됩니다.
팀이 이미 Spark 생태계에 깊이 투자하고 있는 경우입니다. Delta Lake, Databricks Unity Catalog, 실시간 파이프라인을 위한 Structured Streaming, Kafka와의 기본 통합 등은 Spark가 성숙한 생태계를 갖춘 분야입니다. 반면 DuckDB는 실시간 스트리밍을 위한 네이티브 기능을 제공하지 않습니다.
매우 긴 다단계 작업에서 세밀한 장애 복구(Fault Tolerance)가 필요한 경우입니다. Spark는 Lineage 기반의 장애 복구 기능을 제공하므로 일부 작업이 실패하더라도 전체 작업을 처음부터 다시 시작하지 않고 복구할 수 있습니다. DuckDB는 단일 프로세스로 실행되기 때문에 프로세스가 종료되면 작업 전체를 처음부터 다시 실행해야 합니다.
DuckDB가 적합한 경우
다음과 같은 환경이라면 DuckDB가 더 적합한 선택입니다.
배치 작업의 데이터 규모가 수 GB에서 수백 GB 정도인 경우입니다. 이 구간은 DuckDB가 Spark보다 일관되게 더 빠르고, 더 저렴하며, 더 단순한 구조를 제공하는 최적의 영역입니다.
파이프라인이 객체 스토리지에 저장된 Parquet 또는 CSV 파일을 읽고(Read), 변환하고(Transform), 다시 저장하는(Write) 형태가 대부분인 경우입니다. DuckDB는 Predicate Pushdown을 지원하는 뛰어난 파일 처리 성능을 제공합니다.
운영 복잡성을 크게 줄이고 싶고, 팀이 분산 컴퓨팅 인프라를 직접 관리하고 싶지 않은 경우입니다.
별도의 쿼리 엔진을 구축하지 않고도 로컬 또는 S3에 저장된 대용량 데이터에 대해 빠른 애드혹 분석을 수행해야 하는 경우입니다.
시작하기 전에 알아두면 좋은 팁
먼저 httpfs 확장을 설치하고 S3 인증 정보를 올바르게 설정해야 합니다. 처음 사용하는 사용자들이 가장 자주 겪는 문제는 S3 인증 오류입니다. DuckDB는 환경 변수와 EC2 Instance Profile을 포함한 AWS의 표준 Credential Chain을 그대로 사용할 수 있습니다.
import duckdb, boto3
con = duckdb.connect()
con.execute("INSTALL httpfs; LOAD httpfs;")
session = boto3.Session()
creds = session.get_credentials().get_frozen_credentials()
con.execute(f"SET s3_access_key_id='{creds.access_key}';")
con.execute(f"SET s3_secret_access_key='{creds.secret_key}';")
con.execute(f"SET s3_session_token='{creds.token}';")
con.execute("SET s3_region='us-east-1';")
대부분의 ETL 작업에서는 duckdb.connect(database=':memory:')를 사용하는 것이 좋습니다. 영구 데이터베이스 파일은 로컬 개발이나 중간 결과를 캐시할 때 유용하지만, S3에서 읽어 S3에 저장하는 파이프라인에서는 인메모리 연결이 더 단순하며 데이터베이스 파일에 대한 디스크 I/O도 발생하지 않습니다.
EXPLAIN ANALYZE를 이용해 쿼리 성능을 분석하는 것도 좋은 방법입니다. DuckDB는 우수한 쿼리 플래너를 제공하며, 실행 계획을 통해 특히 복잡한 조인에서 시간이 어디에 소비되는지 쉽게 확인할 수 있습니다.
print(con.execute("EXPLAIN ANALYZE SELECT ...").fetchall())
EC2 인스턴스는 CPU뿐 아니라 메모리 용량도 고려하여 선택해야 합니다. 300~500GB 규모의 워크로드라면 c6i.8xlarge(32 vCPU, 64GB RAM) 또는 r6i.4xlarge(16 vCPU, 128GB RAM)가 일반적으로 비용 대비 가장 좋은 성능을 제공합니다. 특히 조인 과정에서 큰 중간 결과가 생성되는 경우에는 메모리 최적화 인스턴스가 훨씬 유리합니다.
결론
Spark 클러스터가 잘못된 선택이었던 것은 아닙니다. 당시에는 분산 컴퓨팅이 가장 안전하고 확장성이 뛰어난 선택이었으며, 지금도 Spark는 충분히 검증된 강력한 시스템입니다.
하지만 우리의 사용 사례에서는 달랐습니다. Parquet 파일을 대상으로 하는 테라바이트 미만의 배치 ETL 작업을 수행하면서, 단일 머신으로도 충분히 처리할 수 있는 문제를 해결하기 위해 불필요한 분산 인프라를 운영하고 있었던 것입니다.
DuckDB는 이러한 상황을 바꾸어 주었습니다. 비용을 절감했고, 파이프라인을 단순화했으며, 운영 부담도 크게 줄였습니다. 코드 유지보수는 쉬워졌고, 엔지니어들은 클러스터 관리에 시간을 쓰는 대신 실제 데이터 문제를 해결하는 데 더 많은 시간을 투자할 수 있게 되었습니다.
만약 여러분의 Spark 클러스터가 하루 대부분의 시간을 유휴 상태로 보내고 있고, 처리하는 데이터 규모가 수백 GB 수준이라면 다른 접근 방식을 한번 시도해 볼 가치가 있습니다. 생각보다 훨씬 좋은 결과를 얻을 수도 있습니다.
Spark를 떠나 다른 도구를 사용해 본 경험이 있으신가요? 어떤 점이 잘 작동했고, 어느 시점부터 한계를 느끼셨는지 궁금합니다.
<출처: https://medium.com/stackademic/duckdb-replaced-my-spark-cluster-i-cut-cloud-costs-by-70-842e1a7791a5>
'EPL과 유튜브 데이터로 배우는 DuckDB' 카테고리의 다른 글
| DuckDB vs PostgreSQL: 아무도 예상하지 못했던 분석 혁명 (0) | 2026.06.27 |
|---|---|
| DuckDB : 왜 모든 데이터 엔지니어가 갑자기 DuckDB를 이야기하는가? (0) | 2026.06.25 |
| Pandas, Polars, DuckDB로 테스트 데이터 생성하기 (0) | 2026.06.22 |
| DuckDB: 2026년 데이터 분석가를 위한 가장 과소 평가된 도구 (0) | 2026.06.21 |
| DuckDB + Python: SQL로 CSV 파일 다루기 part 1 (0) | 2026.06.20 |
댓글