Backfill 안전장치: data interval, catchup, replay window, 중복 방지
Backfill이 평범한 재실행과 다른 이유
"어제 파이프라인이 실패했으니 오늘 다시 실행하면 된다"는 생각은 대부분 맞지만, 그 전제 위에 보이지 않는 위험이 있다. backfill은 단순히 과거 날짜를 재처리하는 것이 아니라, 스케줄러가 그 구간에 대한 DagRun을 새로 생성하고 task를 실행하는 과정이다. 이 과정에서 중복 데이터가 생기거나, 서로 다른 구간이 겹치거나, pool 한도를 초과해 다른 파이프라인을 밀어내는 일이 벌어질 수 있다.
이 편에서는 Airflow가 시간과 데이터 구간을 어떻게 표현하는지부터 시작해, catchup과 backfill의 차이, 중복을 막는 쓰기 패턴, 그리고 Airflow 3.0의 scheduler-managed backfill까지 다룬다.
1. data_interval: Airflow가 시간 구간을 표현하는 방식
Airflow 2.2 이전에는 execution_date라는 개념이 있었다. 이 값은 DAG run의 실행 시각이 아니라 처리해야 하는 데이터 구간의 논리적 시작점이었다. 이름이 직관적이지 않아 많은 혼란을 일으켰고, Airflow 2.2에서 logical_date로 이름이 바뀌면서 동시에 data_interval_start와 data_interval_end가 도입됐다.
logical_date(= data_interval_end)는 DAG run이 "완료된 시점"이 아니라 "처리 대상 구간의 끝점"이다. 일별 DAG에서 logical_date = 2026-07-02라면 그 DAG run은 7월 1일 데이터를 처리한다. 이 구조를 이해하지 못하면 backfill 시 날짜 범위를 잘못 지정하는 실수가 발생한다.
1.1 template 변수로 경계를 명시한다
task 코드에서 현재 시각(datetime.now(), today())을 쓰는 순간 그 task는 backfill 안전성을 잃는다. 실행 시점에 따라 결과가 달라지기 때문이다. 항상 오케스트레이터가 보장하는 template 변수를 사용한다.
# 나쁜 패턴: 실행 시각 기준 조회 → backfill 시 잘못된 범위 처리
SELECT * FROM events WHERE created_at >= NOW() - INTERVAL 1 DAY
# 좋은 패턴: data_interval 기준 조회 → backfill에서도 동일하게 동작
SELECT * FROM events
WHERE created_at >= '{{ data_interval_start }}'
AND created_at < '{{ data_interval_end }}'Python 코드(PythonOperator 등)에서는 context["data_interval_start"]와 context["data_interval_end"]로 접근한다.
2. catchup vs backfill: 비슷해 보이지만 다르다
스케줄러가 자동으로 DagRun 생성
자동으로 메운다
max_active_runs 설정 필수
명시적으로 지정해서 실행
재처리 가능
중복 실행 여부 제어
2.1 catchup
catchup=True는 DAG가 처음 활성화되거나 재시작될 때 start_date부터 현재까지 건너뛴 구간의 DagRun을 자동으로 생성한다. 편리하지만 수백 개의 DagRun이 한꺼번에 생성될 수 있다. max_active_runs를 설정하지 않으면 pool, parallelism 한도를 초과해 다른 DAG까지 영향을 받는다.
대부분의 팀은 catchup=False를 기본으로 설정하고, Airflow 전역 설정 catchup_by_default=False를 통해 강제한다. 이 방식이 의도치 않은 대량 재처리를 막는 가장 확실한 방어선이다.
2.2 backfill 명령
명시적 재처리가 필요할 때는 airflow backfill create 명령을 사용한다.
# Airflow 3.x 방식
airflow backfill create \
--dag-id my_etl_dag \
--start-date 2026-06-01 \
--end-date 2026-06-30 \
--reprocessing-behavior failed \
--max-active-runs 5
# Airflow 2.x 방식 (레거시)
airflow dags backfill \
-s 2026-06-01 \
-e 2026-06-30 \
my_etl_dag--reprocessing-behavior 옵션이 중복 방지의 핵심이다.
| 값 | 동작 |
|---|---|
none | 이미 DagRun이 있으면 상태와 무관하게 새 run 미생성 (기본값) |
failed | 실패 상태의 run만 새로 생성 |
completed | 성공 또는 실패 run 모두 새로 생성 (전체 재처리) |
운영 실수로 가장 많이 일어나는 문제가 completed 옵션으로 이미 성공한 구간을 재처리하는 것이다. 이 경우 중복 데이터가 생기므로 task의 idempotency가 반드시 보장되어야 한다.
3. 중복을 막는 쓰기 패턴
backfill의 안전성은 결국 "같은 구간을 두 번 처리해도 결과가 한 번처럼 보이는가"에 달려 있다. 이를 보장하는 쓰기 패턴을 데이터 저장소별로 정리한다.
3.1 관계형 DB: DELETE + INSERT 또는 UPSERT
-- 패턴 1: 해당 파티션 삭제 후 재삽입
DELETE FROM orders_daily
WHERE dt = '{{ ds }}';
INSERT INTO orders_daily (dt, order_id, amount, status)
SELECT DATE(created_at), order_id, amount, status
FROM raw_orders
WHERE DATE(created_at) = '{{ ds }}';
-- 패턴 2: UPSERT (PostgreSQL)
INSERT INTO orders_daily (dt, order_id, amount, status)
SELECT DATE(created_at), order_id, amount, status
FROM raw_orders
WHERE DATE(created_at) = '{{ ds }}'
ON CONFLICT (dt, order_id) DO UPDATE SET
amount = EXCLUDED.amount,
status = EXCLUDED.status;DELETE + INSERT 패턴은 단순하고 명확하지만 트랜잭션으로 묶어야 부분 실패 시 데이터 손실이 없다. UPSERT는 트랜잭션이 필요 없지만 유일 키 설계가 선행되어야 한다.
3.2 분석 DB / Object Storage: 파티션 덮어쓰기
BigQuery, Hive, Spark + S3 계열에서는 파티션 전체를 교체하는 방식이 표준이다.
-- BigQuery: WRITE_TRUNCATE + partition filter
INSERT INTO `project.dataset.orders_daily`
PARTITION (dt = '{{ ds }}')
SELECT ...
FROM `project.dataset.raw_orders`
WHERE DATE(created_at) = '{{ ds }}'
-- SDK에서 write_disposition=WRITE_TRUNCATE 설정
-- Spark: 특정 파티션만 덮어쓰기
spark.conf.set("spark.sql.sources.partitionOverwriteMode", "dynamic")
df.write.mode("overwrite").partitionBy("dt").saveAsTable("orders_daily")
-- Delta Lake: replaceWhere로 범위 교체
df.write.format("delta").option("replaceWhere", "dt = '{{ ds }}'")
.mode("overwrite").save("/data/orders_daily")partitionOverwriteMode=dynamic은 쓰는 파티션만 교체하므로 다른 날짜 파티션에 영향을 주지 않는다. 이 설정을 빠뜨리면 테이블 전체가 삭제될 수 있다.
3.3 집계 테이블 갱신 시 주의점
집계 task가 기존 값에 더하는 방식이면 재실행 시 값이 두 배가 된다.
# 나쁜 패턴: 누적 합산 → 재실행 시 중복
UPDATE revenue_daily
SET total = total + {{ new_amount }}
WHERE dt = '{{ ds }}'
# 좋은 패턴: 매 실행마다 전체 재계산
INSERT INTO revenue_daily (dt, total)
SELECT '{{ ds }}', SUM(amount)
FROM orders_daily
WHERE dt = '{{ ds }}'
ON CONFLICT (dt) DO UPDATE SET total = EXCLUDED.total4. Replay window: 재처리 범위 설계
backfill을 안전하게 운영하려면 "어느 구간까지 재처리할 수 있는가"를 미리 설계해야 한다. 이를 replay window라고 부른다.
또는 스트리밍 소스
→ 소스 시스템 보존 정책 확인 필수
raw layer 보존 기간 = 재처리 가능 범위
순서대로 재처리 필요
비용과 재처리 시간이 트레이드오프
오래된 구간 재처리 불가능할 수 있음
4.1 Replay window를 결정하는 세 가지 제약
소스 보존 기간: Kafka topic retention이 7일이면 8일 전 이벤트는 재처리할 소스가 없다. S3 생명주기 정책이 30일이면 31일 전 raw 파일이 없다. replay window는 소스 시스템이 허용하는 기간을 초과할 수 없다.
스키마 호환성: 3개월 전 raw 데이터를 현재 파이프라인 코드로 재처리할 때 컬럼이 추가·변경됐다면 파싱 오류가 발생한다. 스키마 변경 이력과 backward compatibility 설계가 replay window에 영향을 준다.
downstream SLA 영향: 대규모 backfill은 pool을 소비해 실시간 파이프라인을 지연시킨다. max_active_runs를 낮게 설정하거나 별도 backfill pool을 만들어서 프로덕션 트래픽과 분리한다.
4.2 backfill 전용 pool 설계
# backfill 전용 pool 생성 (슬롯 3개로 제한)
airflow pools set backfill_pool 3 "backfill only – limits resource usage"
# DAG에서 backfill run 시에만 pool 지정
from airflow.models import DagRun
def get_pool(**context):
run_type = context["dag_run"].run_type
return "backfill_pool" if run_type == "backfill" else "default_pool"Airflow 3.0에서는 backfill이 스케줄러 관리 객체로 격상되어 UI에서 pool 설정을 직접 제어할 수 있다.
5. max_active_runs: 동시 DagRun 수 제어
catchup이나 backfill로 대량의 DagRun이 생성될 때, max_active_runs는 동시에 실행 중인 DagRun 수의 상한을 설정한다. 이 값이 너무 크면 pool을 독점하고 다른 DAG를 굶긴다. 너무 작으면 재처리가 오래 걸린다.
dag = DAG(
"daily_etl",
schedule="@daily",
catchup=False,
max_active_runs=3, # 최대 3개 DagRun 동시 실행
max_active_tasks=8, # 이 DAG의 최대 동시 task 수
...
)backfill 명령에서도 --max-active-runs 옵션으로 별도 제어할 수 있다. 프로덕션 클러스터에서 백필을 실행할 때는 동시 DagRun 수를 1~5로 제한하고, 점진적으로 늘려가면서 클러스터 부하를 확인하는 것이 안전하다.
6. Airflow 3.0의 scheduler-managed backfill
Airflow 3.0(AIP-78)에서 backfill이 first-class 스케줄러 개념으로 격상됐다. 이전에는 CLI 프로세스가 직접 DagRun을 생성하고 끝나면 프로세스가 종료되는 방식이었다면, 3.0부터는 스케줄러가 backfill 객체를 관리한다.
주요 변화는 다음과 같다.
- UI에서 backfill 생성, 모니터링, 일시정지, 취소가 가능하다.
- backfill이 non-blocking 방식으로 실행되어 여러 backfill을 동시에 큐에 넣을 수 있다.
- backfill run도 pool 한도를 존중한다 — 이전 버전에서는 backfill이 pool을 우회하는 문제가 있었다.
reprocessing_behavior옵션이 API와 UI에서도 설정 가능하다.
# Airflow 3.x CLI
airflow backfill create \
--dag-id orders_etl \
--start-date 2026-06-01 \
--end-date 2026-06-30 \
--reprocessing-behavior none \
--max-active-runs 5
# backfill 목록 확인
airflow backfill list --dag-id orders_etl
# backfill 취소
airflow backfill cancel <backfill_id>7. 실무 체크리스트: backfill 전에 확인할 것
backfill을 실행하기 전에 아래 항목을 점검한다.
| 항목 | 확인 내용 |
|---|---|
| idempotency | task 쓰기 로직이 DELETE+INSERT 또는 UPSERT인가? datetime.now() 사용 없는가? |
| 소스 데이터 존재 | 재처리 구간의 raw 데이터가 소스에 실제로 있는가? |
| 스키마 호환성 | 오래된 구간 데이터가 현재 파싱 코드와 호환되는가? |
| max_active_runs | 프로덕션 파이프라인에 영향을 주지 않을 동시 run 수인가? |
| pool 여유 | backfill이 점유할 pool 슬롯이 충분한가, 분리 pool이 필요한가? |
| downstream 파이프라인 | backfill 완료 후 downstream DAG도 재처리 필요한가? |
| reprocessing_behavior | 이미 성공한 run을 다시 실행해야 하는가, 실패 run만인가? |
마무리: 안전한 backfill은 설계에서 시작한다
backfill을 안전하게 만드는 것은 backfill 명령 옵션이 아니라 task 코드의 idempotency와 data_interval 사용에 달려 있다. 파이프라인을 처음 설계할 때부터 "이 task를 30일 전 날짜로 재실행하면 어떻게 되는가"라는 질문을 던지는 것이 가장 효과적인 예방이다. 그 질문에 자신 있게 "같은 결과가 나온다"고 답할 수 있는 파이프라인이 backfill 안전 파이프라인이다.
References
- Apache Airflow Documentation, "Backfill": https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/backfill.html
- Apache Airflow Documentation, "DAG Runs": https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dag-run.html
- Apache Airflow Wiki, "AIP-78 Scheduler-managed backfill": https://cwiki.apache.org/confluence/display/AIRFLOW/AIP-78+Scheduler-managed+backfill
- Astronomer Documentation, "Rerun Airflow DAGs and tasks": https://docs.astronomer.io/learn/rerunning-dags
- Astronomer Blog, "Intro to Airflow 3": https://www.astronomer.io/blog/intro-to-airflow-3/
- Medium (Chanon Krittapholchai), "Apache Airflow Useful Practices: Idempotent DAG": https://medium.com/@chanon.krittapholchai/apache-airflow-useful-practices-idempotent-dag-6d52b1594704
- ML4Devs, "Backfilling Historical Data With Idempotent Data Pipelines": https://www.ml4devs.com/what-is/backfilling-data/
- Galaxy, "Airflow Backfill without Duplicates": https://www.getgalaxy.io/learn/glossary/airflow-backfill-without-duplicates
- DataCamp, "Apache Airflow 3.0 Is Here: The Most Significant Release Yet": https://www.datacamp.com/blog/apache-airflow-3-0