Blog
pysparksparkconfigurationtuningaqedata-engineering

PySpark 설정 튜닝 종합 — 어떤 config를 언제 바꿔야 하는가

수많은 Spark 설정 중 실제로 효과 있는 것만 정리합니다. 익스큐터 사이징(코어·메모리·인스턴스), AQE, 셔플 파티션, 직렬화, 동적 할당, 그리고 "메모리부터 올리는" 안티패턴 대신 근거 기반으로 튜닝하는 순서를 다룹니다.

Data Dynamics2026년 6월 5일14 min read

잡이 느릴 때 여러분은 어떻게 하시나요? 많은 팀이 executor.memory 를 올려 보고, 그래도 안 되면 인스턴스를 늘립니다. 비용은 올라가는데 속도는 그대로인 경험, 한 번쯤 해보셨을 거예요. Spark 설정은 수백 개이지만 실제로 효과 있는 설정은 십여 개뿐이고, 무엇을 언제 바꿔야 하는지에는 명확한 순서가 있습니다.

이 글은 정말 중요한 Spark 설정만 골라, 무엇을 어떤 근거로 조정하는지 정리합니다. (개별 문제 — 스큐·OOM·작은 파일 — 의 상세 튜닝은 각 전용 글에서 다뤘으니, 여기서는 전체 그림과 우선순위를 봅니다.)

이 글에서 배우는 것

  • 추측 대신 측정에 기반한 Spark 튜닝 순서
  • AQE(Adaptive Query Execution)가 자동으로 해결해 주는 것들
  • 익스큐터 코어·메모리·인스턴스를 합리적으로 정하는 방법
  • 셔플 파티션 기본값 200의 함정과 대처법
  • 설정이 아니라 코드를 바꿔야 하는 상황 구분하기

1. 튜닝의 황금률 — 측정하고, 가장 큰 병목부터

❌ 추측: 느리다 → memory 올린다 → 안 된다 → 인스턴스 늘린다 → 비용만↑
✅ 근거: Spark UI 측정 → 병목 식별(스큐/셔플/OOM) → 해당 설정만 조정

의사가 진단 없이 처방을 내리지 않듯이, 설정을 바꾸기 전에 Spark UI 로 병목을 먼저 식별하세요(별도 글 "PySpark 느린 잡 디버깅"). 스큐인데 메모리를 올리거나, 셔플 문제인데 인스턴스를 늘리는 건 효과가 없습니다.

한 문장으로: 측정 없는 튜닝은 처방 없는 치료입니다.

2. 가장 먼저 — AQE 켜기

Spark 3.x 의 Adaptive Query Execution 은 런타임에 파티션 병합·스큐 조인·조인 전략을 자동 조정합니다. 코드 한 줄 추가 없이 적용 가능한, 가장 적은 노력으로 가장 큰 효과를 주는 설정입니다.

spark.conf.set("spark.sql.adaptive.enabled", "true")                      # 마스터 스위치
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")   # 작은 파티션 자동 병합
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")             # 스큐 조인 자동 분할
spark.conf.set("spark.sql.adaptive.advisoryPartitionSizeInBytes", "128MB")

최신 Spark 는 AQE 가 기본 활성이지만, 명시적으로 확인해 두는 것이 좋습니다. AQE 하나로 작은 파일·스큐·파티션 수 문제의 상당 부분이 자동 완화됩니다.

3. 익스큐터 사이징 — 가장 중요한 결정

자원 배분의 핵심은 세 값의 균형입니다: 익스큐터 코어, 메모리, 인스턴스 수. 피자를 나눌 때처럼, 조각 수(코어)와 한 조각 크기(메모리), 그리고 피자 판 수(인스턴스)를 함께 고민해야 합니다.

spark.conf.set("spark.executor.cores", "4")          # 익스큐터당 동시 태스크
spark.conf.set("spark.executor.memory", "8g")        # JVM 힙
spark.conf.set("spark.executor.memoryOverhead", "2g")# 힙 밖(셔플·네이티브·Python)
spark.conf.set("spark.executor.instances", "20")     # 익스큐터 수

코어 수 — 4~5가 스위트스폿

코어 너무 적음(1~2) → 익스큐터 오버헤드 대비 비효율
코어 너무 많음(>5)  → HDFS/스토리지 I/O 처리량 저하, GC 경합
→ 익스큐터당 4~5 코어가 일반적 권장

메모리 — 힙과 오버헤드 구분

PySpark 는 Python 워커가 JVM 힙 밖의 메모리를 별도로 사용하기 때문에, memoryOverhead 를 넉넉히 잡아야 "Container killed" 오류를 피할 수 있습니다(별도 글 "PySpark Executor OOM 정복").

설정의미지침
executor.cores동시 태스크4~5
executor.memoryJVM 힙태스크당 충분히, GC 고려
executor.memoryOverhead힙 밖힙의 ~25%, PySpark 면 더
executor.instances익스큐터 수병렬도 = cores × instances

흔한 실수: "큰 익스큐터 몇 개" vs "작은 익스큐터 많이". 너무 큰 익스큐터는 GC pause 가 길고, 너무 작으면 오버헤드가 큽니다. 중간 크기(45코어, 816GB) 다수가 보통 안전합니다.

4. 셔플 파티션 — 200 기본값의 함정

spark.sql.shuffle.partitions 의 기본값 200 은 거의 모든 워크로드에 맞지 않는 값입니다. 마치 신발 사이즈가 "모두 260"인 것과 같죠 — 데이터가 크면 파티션당 너무 커서 OOM, 작으면 빈 파티션만 200개입니다.

# AQE 가 켜져 있으면 런타임에 자동 조정되므로 기본값 신경 덜 써도 됨
spark.conf.set("spark.sql.adaptive.enabled", "true")
 
# AQE 없이 수동이라면: 파티션당 ~128MB 목표로
# num = 총 셔플 데이터 / 128MB
spark.conf.set("spark.sql.shuffle.partitions", "800")

AQE 의 coalescePartitions 가 켜져 있으면 이 값을 크게 잡아도(예: 2000) 런타임에 적정 수로 병합해주므로 안전합니다. AQE 시대에는 이 값을 일일이 맞추는 부담이 크게 줄었습니다.

5. 직렬화·압축

직렬화는 데이터를 네트워크로 주고받을 때 어떻게 포장하는지에 관한 설정입니다. 기본 Java 직렬화보다 Kryo 를 쓰면 짐을 더 작고 빠르게 포장할 수 있습니다.

# Kryo 직렬화 (Java 기본보다 빠르고 작음)
spark.conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
 
# 셔플 압축(기본 켜짐) — 네트워크·디스크 절약
spark.conf.set("spark.shuffle.compress", "true")
spark.conf.set("spark.shuffle.spill.compress", "true")

Kryo 는 거의 항상 이득입니다. 압축은 기본 활성이며 대부분 그대로 두면 됩니다.

6. 동적 할당 — 자원 효율

고정 익스큐터를 항상 켜두는 것은 빈 고속도로에 차선을 영구 예약하는 것과 같습니다. 동적 할당을 쓰면 부하에 따라 익스큐터 수가 자동으로 조절됩니다(특히 공유 클러스터·K8s 환경에서 효과적입니다).

spark.conf.set("spark.dynamicAllocation.enabled", "true")
spark.conf.set("spark.dynamicAllocation.shuffleTracking.enabled", "true")  # K8s 필수
spark.conf.set("spark.dynamicAllocation.minExecutors", "2")
spark.conf.set("spark.dynamicAllocation.maxExecutors", "50")
spark.conf.set("spark.dynamicAllocation.executorIdleTimeout", "60s")

(K8s 에서의 셔플 추적·스팟 이슈는 별도 글 "PySpark on Kubernetes" 참고.)

7. 브로드캐스트·조인

작은 테이블과 큰 테이블을 조인할 때, 작은 테이블을 모든 익스큐터에 복사해 두면 셔플 없이 로컬에서 처리할 수 있습니다. 이것이 브로드캐스트 조인입니다.

# 자동 브로드캐스트 임계값 (기본 10MB) — 작은 차원 조인 최적화
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "50MB")
# -1 로 끄면 broadcast 안 함 (의도적 비활성 시)

너무 크게 잡으면 큰 테이블이 broadcast 돼 OOM 위험이 있으니 주의하세요(별도 글 "Broadcast 변수와 대형 Lookup"). 50~100MB 정도가 보통 안전합니다.

8. 설정 우선순위 — 무엇부터 손대나

어디서 시작해야 할지 막막하다면, 아래 순서를 따라가면 됩니다.

1. AQE 켜기 (skew/coalesce/skewJoin)        → 가장 큰 효과, 가장 적은 노력
2. 익스큐터 사이징 (cores 4~5, 메모리+오버헤드)
3. shuffle.partitions (AQE 있으면 크게 둬도 OK)
4. memoryOverhead (PySpark Container killed 대응)
5. 직렬화(Kryo), 동적 할당
6. autoBroadcastJoinThreshold
   ───
   그래도 안 되면 → 코드/데이터 문제 (스큐·UDF·작은파일) — 설정으로 못 고침

⚠️ 설정 튜닝에는 한계가 있습니다. 스큐, 잘못된 조인, 비싼 UDF, 작은 파일은 설정으로 못 고칩니다 — 코드·데이터 레이아웃을 바꿔야 합니다. 설정을 다 만져봐도 안 되면, 문제는 설정이 아니라 코드입니다.

9. 워크로드별 프로파일

워크로드 성격에 따라 어느 설정을 먼저 살펴볼지 달라집니다. 아래 표를 참고하면 빠르게 우선순위를 잡을 수 있습니다.

워크로드강조 설정
대형 ETL 배치큰 익스큐터, shuffle.partitions↑, FTE
대화형/짧은 쿼리동적 할당, 작은 지연, broadcast
스트리밍트리거·백프레셔, 체크포인트, 적정 익스큐터
ML 학습메모리↑, 캐시, 적은 셔플
고동시성 공유동적 할당, 공정 스케줄러

10. 정리

영역핵심 설정효과
AQEadaptive.*자동 스큐·파티션·조인
사이징cores 4~5, 메모리, 오버헤드안정성·처리량
셔플shuffle.partitions(AQE면 덜 중요)파티션 크기
직렬화Kryo속도
동적 할당dynamicAllocation.*자원 효율
조인autoBroadcastJoinThreshold셔플 회피

Spark 설정 튜닝의 핵심은 "측정으로 병목을 찾고, 가장 효과 큰 것부터 조정하되, 설정의 한계를 인정하는 것"입니다. AQE 를 켜고 익스큐터를 합리적으로 사이징하는 것만으로 대부분의 잡이 안정화됩니다. 그래도 느리다면 — 설정을 더 만지기 전에 스큐·UDF·작은 파일 같은 코드·데이터 문제를 의심하세요. 좋은 설정은 좋은 코드를 빠르게 하지만, 나쁜 코드를 고쳐주지는 못합니다.

마치며 — 핵심 요약

  • 측정이 먼저입니다. Spark UI 에서 병목을 확인하기 전에 설정을 건드리지 마세요.
  • AQE 가 가장 강력한 한 방입니다. 켜는 것만으로 스큐·파티션·조인 문제의 상당 부분이 자동 해결됩니다.
  • 익스큐터는 중간 크기 다수가 안전합니다. 코어 45개, 메모리 816GB 를 여러 개 두는 패턴을 기본으로 삼으세요.
  • 셔플 파티션 200은 함정입니다. AQE 가 켜져 있다면 크게 잡아도 괜찮고, 없다면 데이터 크기에 맞게 조정하세요.
  • PySpark 는 memoryOverhead 를 꼭 챙겨야 합니다. Python 워커 메모리를 JVM 힙과 별도로 확보하지 않으면 "Container killed" 가 반복됩니다.
  • 설정의 한계를 아는 것도 실력입니다. 코드·데이터 레이아웃 문제는 설정으로 해결되지 않습니다 — 그 경계를 알면 시간 낭비를 크게 줄일 수 있습니다.

이 순서대로 하나씩 확인해 보면, 대부분의 Spark 잡은 생각보다 훨씬 빠르게 안정됩니다. 여러분의 클러스터를 응원합니다!


이 글은 Spark 3.5 기준으로 작성되었습니다. Spark 클러스터·잡의 종합 성능 튜닝이 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀