Blog
pysparksparkpandasmigrationpandas-on-sparkdata-engineering

pandas 에서 PySpark 로 — 단일 머신 한계를 넘는 스케일아웃

메모리에 안 들어가는 데이터를 만난 pandas 사용자를 위한 전환 가이드. lazy vs eager 실행 모델 차이, 흔히 빠지는 함정(반복문·인덱스·collect), pandas API on Spark 로 코드 거의 그대로 분산 처리하는 법까지 정리합니다.

Data Dynamics2026年6月5日15 min read
This post is not yet translated. The original Korean version is shown below.

집에서 요리할 때는 냉장고를 열면 재료가 바로 손에 잡히고, 냄비에 뭔가를 넣으면 즉시 끓기 시작합니다. pandas 가 딱 그런 느낌입니다 — 한 줄 쓰면 결과가 바로 나오죠. 그런데 수천 명 분량의 밥을 만들어야 하는 상황이 되면 집 부엌으론 불가능합니다. 대형 단체 급식 주방(PySpark)이 필요해집니다. 칼은 똑같이 생겼지만, 재료를 미리 손질해 두고 여러 조리대에서 동시에 작업하는 리듬 자체가 완전히 다릅니다.

pandas 로 잘 돌던 분석 코드가 데이터가 커지면서 MemoryError 를 내기 시작하면 PySpark 로의 전환을 고민하게 됩니다. 그런데 pandas 와 Spark 는 실행 모델이 근본적으로 달라서, 익숙한 pandas 습관을 그대로 옮기면 느려지거나 오히려 메모리가 터집니다.

이 글은 pandas 사용자가 PySpark 로 넘어올 때 반드시 이해해야 할 차이, 흔히 빠지는 함정, 그리고 코드를 거의 바꾸지 않고 분산 처리하는 길(pandas API on Spark)을 정리합니다.

이 글에서 배우는 것

  • pandas 의 즉시 실행(eager)과 PySpark 의 지연 실행(lazy)이 어떻게 다른지
  • iterrows, collect, toPandas 같은 pandas 습관이 Spark 에서 위험한 이유
  • 행 인덱스 없이 순서·위치를 다루는 Spark 식 방법
  • 기존 pandas 코드를 최소 변경으로 분산 처리하는 pandas API on Spark
  • Spark 를 쓰지 말아야 할 상황과 도구 선택 기준

1. 가장 큰 차이 — Lazy vs Eager

pandas 와 PySpark 를 가르는 가장 근본적인 차이는 "언제 계산하느냐"입니다. pandas 는 즉시 실행(eager) 합니다 — 한 줄 쓰면 그 자리에서 계산하고 결과를 메모리에 들고 있습니다. Spark 는 지연 실행(lazy) 합니다 — 변환(transformation)을 쌓아두기만 하다가, 액션(action)을 만나야 한 번에 최적화해서 실행합니다. 이 차이 하나가 아래 나오는 거의 모든 함정의 뿌리입니다.

# pandas: 각 줄이 즉시 실행
df2 = df[df.amount > 100]      # 바로 필터링됨
df3 = df2.groupby("user").sum() # 바로 집계됨
 
# PySpark: 변환은 계획만, 액션에서 실행
df2 = df.filter(F.col("amount") > 100)    # 아직 실행 안 됨
df3 = df2.groupBy("user").sum()            # 여전히 계획만
df3.show()                                  # ← 여기서 비로소 전체 실행
pandas (eager)PySpark (lazy)
실행 시점줄마다 즉시액션에서 한 번에
최적화없음Catalyst 가 전체 계획 최적화
디버깅중간 결과 즉시 확인액션 전엔 확인 불가

이 차이를 이해하면 "왜 에러가 엉뚱한 줄에서 나는지"를 알 수 있습니다. 실제 오류는 액션이 호출되는 시점에 전체 계획이 한꺼번에 실행되면서 터지기 때문에, 에러 줄과 문제 원인이 멀리 떨어져 있는 경우가 많습니다.

한 문장으로: pandas 는 줄마다 즉시 계산하고, PySpark 는 .show(), .count() 같은 액션이 있어야 비로소 실행됩니다.

2. 함정 ① 반복문 — 절대 행을 순회하지 마라

pandas 에 익숙한 분들이 Spark 로 넘어올 때 가장 먼저 저지르는 실수가 행을 순회하는 것입니다. pandas 의 iterrows, apply 같은 행 순회 습관이 Spark 에서는 최악의 패턴입니다.

# pandas (이미 느리지만 동작)
for idx, row in df.iterrows():
    df.at[idx, "grade"] = compute(row)
 
# PySpark 에서 이걸 흉내내면? → collect 로 드라이버 메모리 폭발 + 분산 무력화
# 올바른 방법: 컬럼 연산(벡터화)
df = df.withColumn("grade",
    F.when(F.col("score") >= 90, "A").otherwise("B"))

Spark 의 힘은 컬럼 단위 분산 연산입니다. 행을 순회하는 순간 수십 개 executor 가 동시에 처리하는 분산의 이점이 사라집니다. pandas 의 apply(axis=1) 도 마찬가지입니다 — 컬럼 표현식이나, 불가피한 경우 pandas_udf 로 바꿔야 합니다(별도 글 "PySpark UDF가 느린 이유와 Pandas UDF").

⚠️ for row in df.rdd.toLocalIterator() 같은 RDD 순회도 같은 함정입니다. 데이터를 드라이버로 끌어온 뒤 순회하는 패턴은 분산 처리를 완전히 포기하는 것과 같습니다.

3. 함정 ② collect / toPandas — 드라이버로 다 끌어오기

# 위험: 분산 데이터를 단일 드라이버 메모리로 → 큰 데이터면 OOM
result = spark_df.toPandas()
rows = spark_df.collect()
 
# 안전: 분산 저장하거나, 작게 줄인 뒤에만
spark_df.write.parquet("out")              # 익스큐터가 분산 저장
sample = spark_df.limit(1000).toPandas()   # 필요한 만큼만
agg = spark_df.groupBy("k").count().toPandas()  # 집계 후 작은 결과만

pandas 로 "돌아오고" 싶은 충동이 가장 위험합니다. 분산 데이터 전체를 단일 드라이버 메모리로 끌어오는 순간, 그동안 분산 처리가 가져다준 이점이 한 번에 무너집니다. 결과가 크면 절대 드라이버로 모으지 말고, 분산 저장하거나 집계 후 작은 결과만 가져오세요(별도 글 "PySpark Executor OOM 정복"의 드라이버 OOM 참고).

4. 함정 ③ 인덱스 — Spark 에는 행 인덱스가 없다

pandas 를 쓰면 인덱스가 항상 존재하고, iloc[5] 로 다섯 번째 행에 바로 접근할 수 있습니다. Spark 에는 이 개념 자체가 없습니다. 데이터가 여러 노드에 분산되어 있으므로 "몇 번째 행"이라는 개념이 성립하지 않거든요. 순서·위치 기반 접근(df.iloc[5], df.loc[idx])이 불가능합니다.

# pandas: 위치/인덱스 접근
df.iloc[0]
df.set_index("id")
 
# PySpark: 순서가 필요하면 명시적으로 정렬 + window
from pyspark.sql.window import Window
df = df.withColumn("row_num",
    F.row_number().over(Window.orderBy("created_at")))

Spark 데이터는 분산되어 순서가 없습니다. 순서가 필요하면 명시적으로 정렬하고, 행 번호가 필요하면 window 함수를 써야 합니다. 처음엔 번거롭게 느껴지지만, 이 방식이 수백 GB 데이터에서도 동일하게 동작하는 이유이기도 합니다.

5. 함정 ④ 데이터 타입과 NULL

# pandas: NaN, 동적 타입
# PySpark: 명시적 스키마, null
 
# pandas 의 object 컬럼, mixed type → Spark 는 스키마가 엄격
# CSV 읽을 때 스키마 추론에 의존하지 말고 명시
schema = "id long, amount double, name string"
df = spark.read.schema(schema).csv("path", header=True)

6. API 매핑 — 손에 익은 연산들

작업pandasPySpark
필터df[df.x > 1]df.filter(F.col("x") > 1)
컬럼 추가df["y"] = ...df.withColumn("y", ...)
집계df.groupby("k").sum()df.groupBy("k").sum()
조인pd.merge(a, b, on="k")a.join(b, "k")
정렬df.sort_values("x")df.orderBy("x")
컬럼명 변경df.rename(...)df.withColumnRenamed(...)
결측 처리df.fillna(0)df.fillna(0) / F.coalesce
고유값df.x.unique()df.select("x").distinct()
행 수len(df)df.count() (액션!)

7. 가장 쉬운 길 — pandas API on Spark

기존 pandas 코드 자산이 많은데 Spark 로 옮겨야 하는 상황이라면, API 를 전부 다시 배우지 않아도 됩니다. pandas API on Spark(구 Koalas, pyspark.pandas)를 쓰면 pandas 와 거의 같은 API 를 Spark 위에서 그대로 실행할 수 있습니다.

import pyspark.pandas as ps
 
# pandas 코드와 거의 동일!
psdf = ps.read_parquet("hdfs://.../big_data")
result = (psdf[psdf.amount > 100]
    .groupby("user")["amount"]
    .sum()
    .sort_values(ascending=False))
 
# 진짜 pandas 가 필요한 작은 결과만 변환
small = result.head(100).to_pandas()
순수 PySparkpandas API on Spark
코드 변경많음(API 다름)거의 없음
친숙함새 API 학습pandas 그대로
세밀 제어높음다소 추상화됨
적합신규/성능 criticalpandas 코드 이식

전환 전략: 기존 pandas 코드가 많다면 pandas API on Spark 로 먼저 이식해 동작시키고, 성능이 중요한 핫스팟만 순수 PySpark 로 다시 쓰는 점진적 접근이 현실적입니다.

8. 작은 데이터엔 Spark 가 과하다

지금까지 "pandas 를 어떻게 PySpark 로 바꾸냐"를 이야기했지만, 반대 방향의 함정도 있습니다. 항상 Spark 가 답은 아닙니다. 데이터가 작으면 Spark 가 오히려 느립니다. 셔플·JVM·스케줄링 오버헤드가 실제 연산 시간보다 크기 때문입니다.

데이터 규모권장
수 GB 이하, 단일 머신 가능pandas (또는 Polars/DuckDB)
단일 머신 메모리 초과PySpark
pandas 코드 많은데 커짐pandas API on Spark

"큰 데이터 = Spark"가 항상 옳은 건 아닙니다. 단일 머신에 들어가는 데이터라면 pandas, Polars, DuckDB 가 더 빠르고 훨씬 간단합니다. 도구는 문제 크기에 맞게 고르는 것이 중요합니다.

9. 전환 체크리스트

  • lazy 실행 이해 — 액션 전엔 실행 안 됨
  • iterrows/apply(axis=1) → 컬럼 연산/pandas_udf
  • toPandas/collect 금지 — 분산 저장 또는 작게
  • 인덱스 의존 제거 → 정렬 + window
  • 스키마 명시 (추론 의존 줄이기)
  • 큰 pandas 코드는 pandas API on Spark 로 이식
  • 작은 데이터는 굳이 Spark 안 쓰기

10. 정리

차이/함정pandas 습관PySpark 정답
실행eagerlazy(액션에서 실행)
행 처리iterrows/apply컬럼 벡터 연산
결과 수집다 메모리분산 저장/limit
순서인덱스정렬 + window
이식pandas API on Spark

pandas 에서 PySpark 로의 전환은 "API 를 바꾸는 일"이 아니라 "사고방식을 바꾸는 일" 입니다. 즉시 실행·행 순회·전체 메모리 보유라는 pandas 의 전제가 분산 환경에서는 모두 반대가 됩니다. lazy 실행과 컬럼 연산을 이해하고, toPandas 의 유혹을 참으며, 기존 코드는 pandas API on Spark 로 점진적으로 옮기는 것 — 이 길이 단일 머신의 한계를 가장 매끄럽게 넘는 방법입니다.

마치며 — 핵심 요약

  • lazy vs eager: PySpark 는 액션(.show(), .count(), .write 등)이 호출될 때 비로소 실행됩니다. 에러가 예상치 못한 곳에서 나면 바로 이 이유입니다.
  • 행 순회 금지: iterrows, apply(axis=1), RDD 순회는 분산의 이점을 없앱니다. 컬럼 표현식(withColumn, F.when)이나 pandas_udf 로 대체하세요.
  • toPandas / collect 절제: 분산 데이터 전체를 드라이버로 가져오면 OOM 입니다. 집계 후 작은 결과만, 또는 분산 저장을 쓰세요.
  • 인덱스 없음: Spark 에서 행 순서가 필요하면 명시적 정렬 + row_number() window 함수를 사용하세요.
  • 코드 이식 지름길: 기존 pandas 코드가 많다면 pyspark.pandas (pandas API on Spark)로 먼저 이식하고, 성능 핫스팟만 순수 PySpark 로 재작성하는 점진적 접근이 현실적입니다.
  • 도구 선택: 데이터가 단일 머신 메모리에 들어간다면 pandas/Polars/DuckDB 가 더 빠릅니다. Spark 는 그 한계를 넘어설 때 꺼내는 도구입니다.

집 부엌에서 단체 급식 주방으로 넘어오는 과정이 처음엔 낯설어도, 원리를 이해하고 나면 수백 GB 데이터도 자신 있게 다룰 수 있게 됩니다. 여러분의 전환을 응원합니다!


이 글은 Spark 3.5 기준으로 작성되었습니다. pandas 기반 분석의 스케일아웃이나 데이터 파이프라인 전환이 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀