PySpark 동적 파티션 덮어쓰기와 멱등 백필 — 재처리를 안전하게
특정 날짜만 다시 계산해 덮어쓰고 싶은데 전체 테이블이 날아가는 사고를 막는 법. dynamic partition overwrite 모드, 멱등(idempotent) 백필 설계, Iceberg/Delta 의 안전한 부분 덮어쓰기와 replaceWhere 패턴을 정리합니다.
데이터 파이프라인을 운영하다 보면 "지난 화요일 데이터가 잘못됐으니 그 날짜만 다시 계산해 덮어써라" 같은 요청이 끊임없이 들어옵니다. 그런데 순진하게 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(기본) | 대상 경로 전체 덮어씀 |
dynamic | DataFrame 에 있는 파티션만 덮어씀 |
핵심: 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 엔지니어링 팀