Blog
pysparksparkpartitionbackfillidempotenticebergdelta

PySpark 동적 파티션 덮어쓰기와 멱등 백필 — 재처리를 안전하게

특정 날짜만 다시 계산해 덮어쓰고 싶은데 전체 테이블이 날아가는 사고를 막는 법. dynamic partition overwrite 모드, 멱등(idempotent) 백필 설계, Iceberg/Delta 의 안전한 부분 덮어쓰기와 replaceWhere 패턴을 정리합니다.

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

데이터 파이프라인을 운영하다 보면 "지난 화요일 데이터가 잘못됐으니 그 날짜만 다시 계산해 덮어써라" 같은 요청이 끊임없이 들어옵니다. 그런데 순진하게 mode("overwrite") 로 쓰면 그 날짜만이 아니라 테이블 전체가 날아갑니다. 반대로 덮어쓰기를 피하려다 같은 데이터를 두 번 적재해 중복을 만들기도 합니다.

이 글은 특정 파티션만 안전하게 덮어쓰는 법, 재실행해도 결과가 같은 멱등 백필(idempotent backfill) 설계, 그리고 Iceberg/Delta 의 안전한 부분 덮어쓰기 패턴을 정리합니다.

1. 사고의 시작 — overwrite 가 전체를 지운다

# 위험: dt=2026-06-03 만 다시 쓰려 했는데...
(reprocessed_one_day
    .write
    .mode("overwrite")            # ← 파티션 테이블 전체를 덮어씀! 💥
    .partitionBy("dt")
    .parquet("/warehouse/events"))

기본 overwrite대상 경로 전체를 지우고 새로 씁니다. 하루치만 가진 DataFrame 을 overwrite 하면, 나머지 364일이 사라집니다. 이 사고는 실제로 매우 흔합니다.

2. 해법 ① Dynamic Partition Overwrite

Spark 에는 "들어온 데이터에 존재하는 파티션만 덮어쓰는" 동적 모드가 있습니다.

spark.conf.set("spark.sql.sources.partitionOverwriteMode", "dynamic")
 
(reprocessed_one_day              # dt=2026-06-03 만 포함
    .write
    .mode("overwrite")
    .partitionBy("dt")
    .parquet("/warehouse/events"))
# → dt=2026-06-03 파티션만 교체, 나머지 날짜는 그대로
모드동작
static(기본)대상 경로 전체 덮어씀
dynamicDataFrame 에 있는 파티션만 덮어씀

핵심: dynamic 모드에서는 DataFrame 에 등장하는 파티션 값만 교체됩니다. 5일치를 쓰면 그 5일만 교체되고 나머지는 보존됩니다. 백필의 기본 도구입니다.

dynamic 모드의 한계: 파티션 컬럼이 있어야 하고, "파티션 단위"로만 교체됩니다. 파티션 내 일부 행만 바꾸는 건 안 됩니다.

3. 해법 ② Iceberg / Delta — replaceWhere / 조건부 덮어쓰기

Lakehouse 포맷은 더 안전하고 유연합니다. 조건(predicate)으로 덮어쓸 범위를 명시하므로 사고 위험이 낮습니다.

Delta replaceWhere

(reprocessed
    .write
    .format("delta")
    .mode("overwrite")
    .option("replaceWhere", "dt >= '2026-06-01' AND dt <= '2026-06-03'")
    .save("/warehouse/events"))
# → 조건에 맞는 데이터만 원자적으로 교체

Iceberg overwritePartitions / dynamic

# Iceberg: DataFrameWriterV2 의 overwritePartitions (동적)
(reprocessed
    .writeTo("analytics.events")
    .overwritePartitions())       # 들어온 파티션만 교체
 
# 또는 조건부 덮어쓰기 (SQL)
spark.sql("""
  DELETE FROM analytics.events WHERE dt = DATE '2026-06-03'
""")  # 그 후 append, 또는 MERGE 사용
방식안전성입도
dynamic overwrite중(파티션 단위)파티션
Delta replaceWhere높음(조건 명시)조건 범위
Iceberg overwritePartitions높음(원자적)파티션
MERGE높음(행 단위)

Lakehouse 의 장점: 덮어쓰기가 원자적(atomic) 입니다. 중간에 실패해도 깨진 부분 상태가 남지 않습니다. 또 조건을 명시하니 "전체가 날아가는" 사고가 구조적으로 어렵습니다.

4. 멱등 백필 — 재실행해도 같은 결과

백필의 본질은 "같은 날짜를 여러 번 재처리해도 결과가 동일해야 한다"는 멱등성입니다. 재시도·중복 실행은 운영에서 필연적이기 때문입니다.

멱등하지 않은 파이프라인:
  append 로 적재 → 재실행하면 같은 데이터가 두 번 쌓임 (중복!)
 
멱등한 파이프라인:
  "해당 파티션을 교체" → 몇 번 실행해도 결과 동일

멱등 백필의 두 가지 패턴:

패턴 A — 파티션 교체(overwrite)

def backfill_day(dt):
    result = compute_for_day(dt)       # dt 하루치 재계산
    (result.write
        .format("delta").mode("overwrite")
        .option("replaceWhere", f"dt = '{dt}'")   # 그 날짜만 교체
        .save(TABLE))
    # 몇 번 실행해도 dt 파티션은 같은 결과로 교체 → 멱등

패턴 B — MERGE (행 단위 멱등)

# 키 기준 upsert, updated_at 비교로 멱등 (별도 글 "PySpark 대규모 중복 제거와 SCD2")
spark.sql("""
  MERGE INTO analytics.events t USING updates s
  ON t.id = s.id
  WHEN MATCHED AND s.updated_at > t.updated_at THEN UPDATE SET *
  WHEN NOT MATCHED THEN INSERT *
""")
패턴적합
파티션 교체파티션 전체를 다시 계산 가능할 때(일배치)
MERGE증분·키 기반, 일부 행만 변경

5. 백필 오케스트레이션

대량 기간 백필(예: 과거 1년)은 날짜별로 나눠 실행합니다.

from datetime import date, timedelta
 
def date_range(start, end):
    d = start
    while d <= end:
        yield d
        d += timedelta(days=1)
 
for d in date_range(date(2025, 1, 1), date(2025, 12, 31)):
    backfill_day(d.isoformat())   # 각 날짜는 멱등 → 실패 시 그 날만 재실행

운영 팁:

  • 날짜를 독립 단위로: 각 날짜 백필이 멱등·독립이면, 실패한 날짜만 재실행하면 됩니다.
  • Airflow 등 스케줄러에서 날짜별 태스크로 병렬화하되, 동시 쓰기 충돌(같은 테이블)에 주의.
  • 데이터 양이 크면 한 번에 전체 기간을 한 잡으로 하지 말고 청크로.

6. 흔한 함정

함정결과회피
static overwrite 로 부분 쓰기전체 테이블 소실dynamic 모드 / replaceWhere
append 로 백필중복 누적overwrite/MERGE
파티션 컬럼 없이 dynamic동작 안 함partitionBy 필수
동시 백필 충돌커밋 충돌·깨짐테이블 포맷 트랜잭션, 직렬화
덮어쓰기 후 작은 파일누적정기 컴팩션(별도 글)

static overwrite 사고를 구조적으로 막으려면, Lakehouse 의 replaceWhere/overwritePartitions 를 기본 도구로 삼으세요. 조건을 명시하게 강제되어 "전체 삭제" 실수가 어렵습니다.

7. 정리

도구핵심
dynamic partition overwrite들어온 파티션만 교체
Delta replaceWhere조건 범위만 원자적 교체
Iceberg overwritePartitions파티션 원자적 교체
MERGE행 단위 멱등 upsert
백필 설계날짜 독립·멱등 단위로

파티션 덮어쓰기와 백필의 핵심은 두 가지입니다. 첫째, static overwrite 의 "전체 삭제" 사고를 인지하고 dynamic 모드나 조건부 덮어쓰기를 쓸 것. 둘째, 백필을 멱등하게 설계해 재실행·중복 실행에도 결과가 흔들리지 않게 할 것. Iceberg/Delta 의 원자적·조건부 덮어쓰기를 기본 도구로 삼으면, "어제 데이터만 다시 계산"이라는 일상적 요청이 더 이상 위험한 작업이 아니게 됩니다.


이 글은 Spark 3.5 + Iceberg/Delta 기준으로 작성되었습니다. 백필·재처리 파이프라인의 안전한 설계가 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀