Blog
pysparksparkpandasmigrationpandas-on-sparkdata-engineering

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

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

Data Dynamics2026년 6월 5일15 min read

집에서 요리할 때는 냉장고를 열면 재료가 바로 손에 잡히고, 냄비에 뭔가를 넣으면 즉시 끓기 시작합니다. 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 엔지니어링 팀