포스트

Airflow 해부 (4) 스케줄과 시간: logical date, catchup, backfill

실행 시각과 데이터 기준 시각이 다른 logical date 모델, 켜자마자 과거 실행이 쏟아지는 catchup, 과거 구간을 의도적으로 다시 채우는 backfill까지 Airflow 시간 개념을 정리합니다.

Airflow 해부 (4) 스케줄과 시간: logical date, catchup, backfill

Airflow 해부 시리즈의 4편입니다. 전체 목차는 0편에 있습니다.

logical date: 실행 시각과 데이터 시각은 다르다

@daily DAG가 7월 22일 자정에 실행됐을 때, 이 실행의 logical date는 7월 21일입니다. 처음 보면 버그 같지만 설계입니다.

Airflow의 스케줄 모델은 “구간이 끝난 뒤 그 구간을 처리한다”입니다. 7월 21일 하루치 데이터는 21일이 끝나는 시점(22일 자정)에야 완성되므로, 21일치를 처리하는 실행은 22일 자정에 돌고 그 실행의 데이터 기준 날짜(logical date)는 21일인 것입니다.

1
2
3
4
5
6
7/21 00:00 ────────── 7/22 00:00 ────────── 7/23 00:00
     │   21일치 데이터    │    22일치 데이터      │
     └── 구간 ───────────┘
                         ▲
                    실행 시각: 7/22 00:00
                    logical date: 7/21

따라서 task 안에서 “오늘 날짜”를 datetime.now()로 잡으면 안 됩니다. 처리할 데이터의 날짜는 logical date에서 가져옵니다.

1
2
3
@task
def extract(ds: str) -> str:          # ds: logical date의 YYYY-MM-DD 문자열
    return f"/data/raw/{ds}.parquet"  # 7/22 실행에서 ds는 "2026-07-21"

ds 같은 내장 파라미터를 함수 인자로 선언하면 Airflow가 채워줍니다. datetime.now()로 짠 DAG는 재실행할 때 그날 날짜로 돌아버려서, 과거 구간 재처리(backfill)가 불가능해집니다.

catchup: 켜자마자 쏟아지는 과거 실행

start_date가 7월 1일인 daily DAG를 7월 22일에 활성화하면, catchup이 켜진 상태에서는 7월 1일부터 21일까지 21개의 실행이 한꺼번에 쏟아집니다. “start_date부터 지금까지의 미실행 구간을 전부 채운다”가 catchup의 동작이기 때문입니다.

  • 과거 데이터를 소급 처리해야 하는 DAG라면 이것이 정확히 원하는 동작입니다
  • 그 외의 경우 대부분은 사고입니다. 갑자기 수십 개의 실행이 돌며 DB와 외부 API를 두드립니다

의도가 없다면 catchup=False를 명시하는 것을 기본값으로 삼습니다. 이 시리즈의 예제가 전부 catchup=False인 이유입니다.

backfill: 의도적인 과거 재처리

catchup이 “빠진 구간의 자동 채움”이라면, backfill은 “지정 구간의 의도적 재실행”입니다. 지난주 집계 로직에 버그가 있어서 그 구간만 다시 돌려야 할 때 씁니다.

1
2
airflow dags backfill demo_pipeline \
  --start-date 2026-07-14 --end-date 2026-07-20

이 명령이 성립하려면 두 가지 전제가 필요합니다.

  1. task가 logical date 기준으로 짜여 있어야 합니다. datetime.now()가 섞여 있으면 backfill이 과거가 아니라 오늘 데이터를 처리합니다
  2. task가 멱등해야 합니다. 이미 처리된 날짜를 다시 돌려도 중복 데이터가 쌓이지 않아야 합니다. “해당 날짜 파티션을 지우고 다시 쓴다”가 대표 패턴입니다

시간대

Airflow 내부 기준은 UTC입니다. start_date에 timezone 없는 datetime을 주면 UTC로 해석되어, KST 기준으로 생각한 스케줄과 9시간이 어긋납니다. 한국 기준 스케줄은 pendulum으로 시간대를 명시합니다.

1
2
3
4
5
6
7
import pendulum

@dag(
    schedule="0 3 * * *",   # KST 새벽 3시
    start_date=pendulum.datetime(2026, 7, 1, tz="Asia/Seoul"),
    catchup=False,
)

UI의 시각 표시도 기본이 UTC라서, “왜 새벽에 돈 DAG가 오후로 찍혀 있지”의 답은 대부분 시간대입니다.

이 편의 요약

  • 날짜는 datetime.now()가 아니라 logical date(ds)에서 가져옵니다
  • catchup=False를 기본값으로 명시하고, 과거 채움이 필요할 때만 켭니다
  • 과거 재처리는 backfill로 하고, 그 전제는 logical date 기준의 멱등한 task입니다
  • start_date는 시간대를 명시합니다

다음 글: Airflow 해부 (5) Operator, Connection, Sensor

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.