Hadoop, Spark, Dask — 어떤 분산 처리 프레임워크를 언제 써야 하나
Hadoop MapReduce, Apache Spark, Dask의 실행 모델·자원 관리·장애 허용 방식을 비교하고, 워크로드별 선택 기준과 운영 시 주의점을 정리한다.
2026-08-12 · 최초 발행 2025-10-14
같은 클러스터 위에서 돌아가는 "분산 처리"라는 말 안에, 사실은 서로 다른 세 개의 게임이 들어 있다. Hadoop MapReduce는 Map → Shuffle → Reduce라는 고정된 파이프라인을 디스크 기반으로 밀어붙이고, Apache Spark는 DAG를 그려서 메모리에 최대한 올려놓고 돌리며, Dask는 파이썬 함수를 동적 태스크 그래프로 쪼개 가벼운 스케줄러로 흩뿌린다. 셋 다 "대량 데이터를 여러 노드에 나눠 처리한다"는 목표는 같지만, 그 목표에 도달하는 방식이 다르기 때문에 워크로드에 따라 성능·운영 부담·개발 생산성이 크게 갈린다.
실행 모델: 디스크, 메모리, 동적 그래프
MapReduce는 단계적 파이프라인과 강제 셔플, 디스크 기반 중간 결과 저장을 전제로 설계됐다. 각 단계 사이에 중간 결과를 디스크에 내려쓰기 때문에 느리지만, 그만큼 예측 가능하고 장애에 강하다. 대량 정렬·조인처럼 처음부터 끝까지 한 번만 훑고 지나가는 배치 작업에는 이 구조가 잘 맞는다.
Spark는 DAG(Directed Acyclic Graph) 최적화와 메모리 캐시를 중심에 둔다. 메모리가 부족하면 디스크로 스필하긴 하지만, 기본 전제 자체가 "가능하면 메모리에서 끝낸다"는 쪽이다. 같은 데이터를 여러 번 훑는 반복 연산(머신러닝 학습, 대화형 SQL 분석)에서 MapReduce와의 격차가 가장 크게 벌어지는 지점으로, 반복·SQL 워크로드를 Spark로 옮기면 MapReduce 대비 처리량이 310배 늘고 지연이 3080% 줄어드는 사례가 보고된다(데이터·클러스터 구성에 따라 상이).
Dask는 접근이 다르다. 동적 태스크 그래프와 파인그레인 병렬화를 통해, Pandas·NumPy·Scikit-learn 같은 기존 파이썬 코드를 거의 그대로 분산 실행으로 옮길 수 있게 설계됐다. 대규모 클러스터 운영보다는 "이미 익숙한 파이썬 워크플로우를 노트북 한 대에서 여러 대로 확장한다"는 문제에 최적화되어 있다.
자원 관리와 스케줄링
MapReduce는 YARN 위에서 정적 컨테이너 할당을 기본으로 하고, 느린 태스크는 speculative execution(같은 태스크를 다른 노드에서 중복 실행해 먼저 끝나는 결과를 채택하는 방식)으로 완화한다. Spark는 스탠드얼론·YARN·Kubernetes를 모두 지원하며 동적 할당과 스테이지·태스크 단위 최적화를 제공하지만, 셔플 서비스나 외부 셔플 관리가 별도로 필요해진다. Dask는 dask-scheduler + dask-worker 구조로 훨씬 가볍게 시작할 수 있고 적응형 스케일링도 지원하지만, 태스크 수가 수백만 단위로 늘어나면 그래프 자체의 최적화 비용이 문제가 되기 시작한다.
스토리지 포맷과 셔플이 성능을 가른다
세 프레임워크 모두 Parquet·ORC·Avro 같은 컬럼형 포맷을 쓰는 편이 유리하다. 프레디케이트 푸시다운과 압축으로 I/O 자체를 줄일 수 있기 때문이다 — 포맷·압축 최적화만으로 데이터 스캔 비용을 30~70% 줄인 사례도 있다. HDFS는 데이터 지역성을 활용할 수 있는 대신 탄력적 확장이 어렵고, 오브젝트 스토리지(S3/GCS 등)는 확장은 쉽지만 일관성 모델이 다르다 — 클라우드 오브젝트 스토리지를 쓸 때는 최종적 일관성 이슈가 발생할 수 있으므로 커밋 프로토콜과 버킷 설정을 별도로 확인해야 한다.
셔플은 세 프레임워크 모두에서 병목이 되기 쉬운 지점이다. 키 기반 파티셔닝을 쓰다 보면 특정 키에 데이터가 몰리는 스큐가 발생하는데, 이때 살팅(키에 임의 접미사를 붙여 분산시키는 기법)이나 커스텀 파티셔너로 완화한다. Spark는 AQE(Adaptive Query Execution)와 브로드캐스트 조인으로, Dask는 파티션 수 조정과 repartition으로 각각 대응한다.
장애 허용, 그리고 그 대가
MapReduce는 태스크 재시도와 중간 결과의 디스크 내구성에 기대는 구조라 장애 복구가 단순하고 예측 가능하다. Spark는 RDD 라인리지(lineage) 기반으로 손실된 파티션을 재계산하는 방식을 쓰는데, 라인리지가 길어지면 체크포인트를 걸어 끊어줘야 하고, 상태를 유지하는 스트리밍 작업은 별도의 체크포인트 전략이 필요하다. 또한 메모리 중심 설계이다 보니 OOM이나 GC 튜닝 이슈를 피하기 어렵다. Dask는 태스크 재시도와 워커 재스케줄링, 부분 재계산(partial recompute)으로 대응하지만, 장기 실행 작업이라면 언제 persist하고 언제 체크포인트를 걸지 별도로 설계해야 한다.
어떤 워크로드에 뭘 쓰나
야간 배치처럼 정형 로그를 정해진 시간에 안정적으로 집계해야 하는 환경, 대량 정렬·조인이 반복되는 예측 가능한 처리라면 MapReduce가 여전히 합리적인 선택이다. 반면 반복 연산이 많은 분석, SQL 중심 쿼리, 스트리밍까지 하나의 엔진으로 통합하고 싶다면 Spark 쪽이 유리하다. Structured Streaming의 exactly-once 싱크(Delta·Kafka 연동), MLlib과 MLflow 연계, Delta·Iceberg 같은 레이크하우스 포맷과의 결합이 이 강점을 뒷받침한다.
Dask는 팀의 무게중심이 파이썬 생태계에 있을 때, 특히 수십수백 GB 규모의 중형 데이터에서 Pandas 코드를 그대로 확장하고 싶을 때 강점이 있다 — 단일 노드 대비 520배 속도 향상을 기대할 수 있다. Dask-ML이나 XGBoost와의 연동으로 분산 학습·하이퍼파라미터 탐색도 가능하지만, 초대규모 데이터로 갈수록 Spark 생태계의 성숙도를 따라가긴 어렵다.
인프라 관점에서 보면 온프레미스에 HDFS를 이미 갖춘 환경은 MapReduce·Spark 둘 다 데이터 지역성을 살릴 수 있어 유리하고, 클라우드 오브젝트 스토리지 중심이라면 Spark·Dask 쪽이 더 자연스럽다 — 스팟/프리엠터블 인스턴스와 오토스케일을 함께 적용하면 인프라 비용을 20~50% 절감할 수 있다는 보고도 있다. 조직 관점에서는 여러 팀이 SQL과 표준 데이터 플랫폼을 공유해야 한다면 Spark 생태계가 우세하고, 연구·데이터사이언스 팀이 기존 파이썬 자산을 재사용하려 한다면 Dask가 우세하다.
처리 흐름 비교
세 프레임워크가 같은 입력에서 출발해 어디서 갈라지는지를 도식으로 보면 다음과 같다.
코드로 보는 접근 방식
같은 성격의 작업(단어 세기, 집계 쿼리)을 세 프레임워크로 각각 짜보면 API 철학의 차이가 바로 드러난다.
Hadoop Streaming으로 짠 WordCount (Hadoop 3.x, Python 3.x)
# mapper.py
#!/usr/bin/env python3
import sys
for line in sys.stdin:
for w in line.strip().split():
print(f"{w}\t1")
# reducer.py
#!/usr/bin/env python3
import sys
from itertools import groupby
import operator
for word, group in groupby(sorted((l.strip().split("\t")[0], int(l.strip().split("\t")[1])) for l in sys.stdin), operator.itemgetter(0)):
print(f"{word}\t{sum(v for _, v in group)}")
# 실행
hadoop jar $HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \
-input /data/input.txt \
-output /data/out_wc \
-mapper mapper.py \
-reducer reducer.py \
-file mapper.py -file reducer.py
PySpark DataFrame 집계 (Spark 3.5+, Python 3.10+, HDFS 또는 S3 경로)
# app.py
from pyspark.sql import SparkSession, functions as F
spark = SparkSession.builder.appName("parquet-agg").getOrCreate()
df = spark.read.parquet("s3a://bucket/events/")
res = (df.filter(F.col("ts") >= "2025-01-01")
.groupBy("country")
.agg(F.countDistinct("user_id").alias("dau"),
F.sum("amount").alias("revenue")))
res.coalesce(1).write.mode("overwrite").parquet("s3a://bucket/out/agg/")
spark.stop()
spark-submit \
--packages org.apache.hadoop:hadoop-aws:3.3.4 \
--conf spark.hadoop.fs.s3a.aws.credentials.provider=com.amazonaws.auth.InstanceProfileCredentialsProvider \
app.py
Dask 분산 DataFrame (Python 3.10+, dask[distributed], fsspec, s3fs)
# dask_app.py
import dask.dataframe as dd
from dask.distributed import Client
client = Client() # 로컬 멀티프로세스
df = dd.read_parquet("s3://bucket/events/", storage_options={"anon": False})
df = df[df.ts >= "2025-01-01"]
g = df.groupby("country").user_id.nunique().compute()
print(g.head())
client.close()
pip install "dask[distributed]" s3fs
python dask_app.py
운영에서 실제로 부딪히는 것들
파일 크기와 포맷은 생각보다 큰 변수다. Parquet+Snappy/ZSTD를 쓰고 목표 파일 크기를 128MB~1GB 정도로 유지하면 스몰 파일 문제를 예방할 수 있다. 셔플·스큐는 Spark의 AQE·브로드캐스트 조인, Dask의 파티션 재조정으로 완화하되, 근본적으로는 데이터 분포 자체를 파악하고 있어야 한다. Spark는 executor 메모리·오버헤드·GC 튜닝과 Kryo 직렬화가, Dask는 persist/checkpoint 시점과 워커 메모리 한계·spill-to-disk 설정이 각각 운영 부담으로 따라온다. 보안 측면에서는 Kerberos/SASL, Ranger/Atlas 같은 접근 제어 도구와 S3 IAM 역할 기반 접근, 저장/전송 암호화가 필요하고, 관측성 측면에서는 Spark UI/History Server나 Dask Dashboard를 중앙 로깅·메트릭(Prometheus/Grafana)과 엮어야 장애 대응이 가능해진다.
비교 요약
| 프레임워크 | 성능 특성 | 확장성 | 일관성 | 안정성 | 운영 편의 |
|---|---|---|---|---|---|
| Hadoop MapReduce | 디스크 중심, 반복 연산 성능 낮음 | 수천 노드까지 검증 | HDFS 강한 일관성 | 태스크 재시도/내구성 우수 | 설정 복잡, 개발 생산성 낮음 |
| Apache Spark | 메모리 중심, 반복/SQL 고성능 | 대규모 확장, 셔플 최적화 필요 | 라인리지+체크포인트 조합 | GC/OOM 튜닝 요구 | 풍부한 API, 배포 다양성 |
| Dask | 파이썬 워크로드에 효율적, 오버헤드 작은 태스크에 유리 | 중대규모까지 적합, 초대규모 시 그래프 비용 고려 | 태스크 그래프 기반 재실행 | 워커 수 변동에 유연 | 파이썬 친화, 개발 생산성 높음 |
기존 파이프라인을 옮길 때는 워크로드 분류와 SLO 정의부터 시작해서, 파일 포맷 표준화와 파티셔닝 재설계, 프레임워크 선택을 위한 PoC, 파이프라인 리팩터링과 옵저버빌리티 구축, 마지막으로 점진적 전환 순으로 진행하는 편이 안전하다. 프레임워크를 바꾸는 일 자체보다, 그 과정에서 파일 포맷과 셔플 전략을 함께 정리하지 않으면 마이그레이션 효과가 기대만큼 나지 않는 경우가 많다.