Apache Top-level 프로젝트인 Apache Airflow (아파치 에어플로우)는 이제 많은 곳에서 사랑을 받고 있는 오픈소스 워크플로우 오케스트레이션 엔진이 되었습니다.
다른 오케스트레이션 엔진과는 다르게 파이썬으로 쉽게 코드를 작성할 수 있고, 작업 현황을 한눈에 확인할 수 있는 UI가 제공되기 때문에 많은 분들이 활용하고 계신데요, Airflow를 처음 접하시는 분들에게 가장 이해하기 어려운 개념 중 하나가 바로 execution_date입니다.
모든 문제의 근원 execution_date
execution_date는 그 이름과는 다르게, Task가 실제로 실행되는 시간이 아니라 '해당 Task가 실행하고자 하는 Time Window의 시작 지점' 을 의미합니다. 도대체 무슨 소리냐고요? 제가 쓰면서도 무슨 말인지 모르겠습니다. 이해를 돕기 위해 아주 간단한 예시를 들어보겠습니다.
“Time Window는 연속적으로 들어오는 데이터를 일정 시간 단위로 나누어 처리하는 방식으로, Tumbling, Sliding, Session Window 등의 형태가 있으며, 주로 스트리밍 데이터 처리와 시계열 분석에 사용됩니다.”
Tumbling : 겹치지 않고 간격이 일정한 윈도우입니다.
Sliding Window : 일정 시간 간격으로 이동하지만, 윈도우끼리 서로 겹칠 수 있는 방식입니다.
Session Window : 고정된 시간이 아니라 사용자의 활동(Activity)에 따라 크기가 변하는 가변적 윈도우입니다.
"데이터를 처리할 때 틈 없이 딱딱 끊어서 보면 Tumbling, 겹쳐가며 흐르듯 보면 Sliding, 사용자 흐름에 따라 가변적으로 보면 Session 윈도우라고 이해했습니다."
다음과 같이 2021년 1월 1일부터 daily로 스케줄되는 DAG를 정의해 봅니다. Airflow는 Python 코드를 이용해서 DAG를 작성하기 때문에 유연하고 확장성 있게 작업을 정의할 수 있습니다.
혹시나 Airflow의 개념에 아직 익숙하지 않으신 분들은 Airflow 공식 문서에 기본 개념이 잘 설명되어 있으니 정독해 보시기를 추천드립니다.
# 2021-01-01 부터 하루에 한번 실행되는 DAG
# 빠른 이해를 위해 시간과 관계없는 다른 파라메터들과 타임존은 모두 생략했습니다
DAG(
dag_id='test_dag',
# '@daily', '0 0 * * *' or datetime.timedelta(days=1)
schedule_interval='@daily',
start_date=datetime.datetime(2021, 1, 1),
# end_date=datetime.datetime(2021, 12, 31),
...
)
이 DAG의 스케줄을 그림으로 표현해 보면 아래와 같습니다.

DAG의 시작시점(start_date)인 2021-01-01 새벽 0시부터 1일 간격(schedule_interval)으로 task가 실행됩니다. 여기까지는 누구나 쉽게 이해할 수 있는 부분이죠. 부푼 마음을 안고 1월 1일 자정까지 기다려 봅니다. 첫번째 작업이 실행됐을까요? 정답은 아니오 입니다.
Airflow를 처음 접하시는 분들이 하는 가장 흔한 실수 중 하나입니다. 실제 첫번째 작업은 2021년 1월 2일 자정에 시작됩니다. 분명 start_date를 1월 1일로 했는데 대체 왜 그런걸까요? 이 규칙을 이해하기 위해서는 먼저 *타임 윈도우(Time Window)*의 개념을 이해해야 합니다.
전통적인 배치 데이터 처리 방식은 작업의 대상이 되는 데이터의 기간이 있고, 그 대상 기간이 지나고 나서 해당 기간에 발생한 데이터를 처리하게 됩니다. 하루에 한번 실행되는 작업이라면 하루가 끝나고 날짜가 바뀔 때 비로소 작업을 실행하게 되는 것이죠.
더 쉬운 예시를 들어 보겠습니다. 쇼핑몰을 운영하고 있다고 해 볼까요. 2021년 1월 3일의 일일 매출을 계산하고 싶다면, 3일이 끝나고 하루가 넘어가는 1월 4일 자정(0시)에 01/03 00:00:00 ~ 01/03 23:59:59에 발생한 매출을 계산해야 합니다. 여기서 작업 대상이 되는 데이터는 1월 3일자 데이터(=execution_date)가 되고, 실제 작업이 실행되는 시간은 1월 4일 0시가 됩니다.
다시 이전에 예시로 돌아와 볼까요. 위의 매출 계산에서 이해한 방식으로 2021년 1월 3일자 Task가 실행되는 방법을 그림으로 표현해 보면 이렇습니다.

즉, execution_date는 해당 Task가 처리해야 할 데이터 기간의 시작 시점인 2021년 1월 3일을 의미합니다. execution_date라는 이름을 직역하면 실행 날짜가 되기 때문에 그 의미와 실제 의미가 달라 자꾸 혼란이 발생하는 것이죠.
이제 start_date가 2021-01-01임에도 불구하고 1월 1일 자정에 작업이 실행되지 않은 이유가 이해되셨나요?
헷갈리지만 기본적인 개념을 살펴봤습니다. 언뜻 보면 개념만 잘 이해한다면 여러가지 작업들을 스케줄링 하는 데 크게 문제가 되지 않을 것 같은데요, 실제로도 그렇습니다. 그렇다면 이런 방식으로 표현되지 않는 작업은 어떤 것들이 있을까요?
1. 정해진 시점의 데이터를 기록하는 스냅샷(Snapshot)을 찍고 싶어요
스냅샷 데이터는 작업이 실행되는 그 시점의 데이터를 기록하는 것이기 때문에, 일반적으로 정해진 시간에 스냅샷을 찍고 스냅샷을 찍을 당시의 시간으로 기록해 두게 됩니다. 만약 하루에 한번 스냅샷을 기록하는 작업이라면 schedule_interval을 1일로 지정하고, 스냅샷 시간을 작업 당시의 시간 혹은 execution_date + timedelta(days=1)로 기록해야 합니다.
2. 스케줄 간격과 처리해야 하는 데이터의 기간이 달라요
매일 자정에 현재 시점부터 과거 7일치의 데이터를 가지고 계산하는 작업 같은 경우는 어떨까요? 대표적인 예로 사용자가 방문한 후 7일 이내에 다시 방문하는지를 알아보는 D7 리텐션(Retention) 같은 것이 있습니다. (일반적으로 'Rolling window'라고 부릅니다.)
현재 방법으로는 하루에 한번 작업을 실행하기 위해 schedule_interval을 1일로 지정하고, execution_date와 상관없이 과거 7일 데이터를 보도록 해야 합니다. (execution_date - timedelta(days=6))
두 가지 경우만 살펴봤는데도 벌써 execution_date 가 제 역할을 못 하고 있네요. 이 외에도 현재 schedule_interval은 Cron expression과 timedelta만 사용할 수 있기 때문에 '공휴일을 제외한 영업일에만 작업' 과 같이 복잡한 형태의 스케줄을 정의할 수 없다는 문제도 있습니다.
schedule_interval 의 변신
Proposal 문서: Airflow Improvement Proposal: AIP-39 Richer scheduler_interval 배포 버전: Airflow 2.2.0
AIP-39 Richer scheduler_interval - Airflow - Apache Software Foundation
페이지 … Airflow Home Airflow Improvement Proposals Completed AIP-39 Richer scheduler_interval Ash Berlin-Taylor님이 작성, 10월 11, 2021에 최종 변경 Motivation Currently a DAG can be scheduled by either providing a cron expression (or one of
cwiki.apache.org
What's new in Apache Airflow 2.2.0
We're proud to announce that Apache Airflow 2.2.0 has been released.
airflow.apache.org
AIP-39는 현재 투표 중인 Proposal로 그 내용이 아직 확정되지 않았기 때문에 구체적인 구현체의 이름들은 변경될 수 있으며, 최종 확정안에 따라 해당 게시글을 업데이트 할 예정입니다. (현재 투표 상황으로 Proposal 자체는 문제없이 통과 될 예정입니다.)
2021.12.02 AIP-39가 확정되어 드디어 Airflow 2.2에서 배포되었습니다. 관련 내용에 따라 글을 최신으로 업데이트했습니다.
앞서 말씀드린 모든 문제는 스케줄 간격과 타임 윈도우라는 각기 다른 2가지의 개념이 섞여 있기 때문에 발생했습니다.
지금의 Airflow 구조에서는 시작 시점과 스케줄 간격만을 정할 수 있기 때문에, 작업이 언제 실행될 지를 결정하는 스케줄과 작업의 대상이 되는 데이터의 Time Window를 분리해서 정의할 수 없습니다. schedule_interval이 스케줄 간격과 타임 윈도우 2가지의 역할을 동시에 하고 있기 때문입니다.
이 문제를 해결하기 위해 PMC 멤버인 Ash Berlin-Taylor와 최근 Airflow 개발에 많이 기여하고 있는 Astronomer의 James Timmins의 주도로 AIP-39 Richer scheduler_interval이 제안되었습니다. AIP-39의 핵심은 바로 작업의 스케줄 간격과 작업의 대상이 되는 타임 윈도우를 분리 하는 것입니다. 지금부터 AIP-39의 내용을 자세히 살펴보겠습니다.
execution_date의 종말
가장 큰 변화는 혼란의 중심에 있는 execution_date을 없애고 좀 더 명확히 타임 윈도우의 개념을 표현하기 위해 data_interval이라는 개념을 도입합니다. 타임 윈도우의 시작은 data_interval_start, 타임 윈도우의 끝은 data_interval_end가 됩니다.
또, 실제로 작업이 실행되는 시점을 표현하기 위해 기존에 오해의 여지가 많았던 변수인 execution_date라는 표현은 이제 사용하지 않고, 새로운 이름인 logical_date로 표현합니다. (하위 호환성 유지를 위해 기존에 사용되던 execution_date는 계속 유지합니다.)
즉, 작업이 실행되는 시점은 logical_date로 표현하고, 해당 시점에 실행되는 작업이 참고해야 할 데이터의 날짜 범위는 data_interval_start와 data_interval_end로 각각 나누어 표현할 수 있게 된 것입니다.
이해를 돕기 위해 위에서 예시로 들었던 그림에 변경되는 변수들을 맨 아래에 표시해봤습니다.

기존에 사용되던 매크로/변수들과 새로 변경되는 변수들을 표로 정리하면 다음과 같습니다.
| 기존 이름 | 새로운 이름 |
| execution_date | data_interval_start, logical_date |
| next_execution_date | data_interval_end |
| tomorrow_ds | deprecate |
| tomorrow_ds_nodash | deprecate |
| yesterday_ds | deprecate |
| yesterday_ds_nodash | deprecate |
| prev_execution_date | prev_logical_date |
| next_execution_date | next_logical_date |
그 외에도 execution_date는 Airflow 스케줄링의 핵심 개념이기 때문에, Airflow Metadata DB에서도 이미 폭넓게 사용되고 있었습니다. 이에 따라 변경되는 DB테이블 구조의 변경 등 더 자세한 사항을 확인하려면 AIP-39 문서를 참고해 주세요.
Timetable 개념 도입
앞서 현재 schedule_interval은 크론 표현식(Cron expression)과 timedelta로만 표현할 수 있기 때문에 '공휴일을 제외한 영업일에만 작업' 과 같이 스케줄 중간중간에 구멍이 뚫려 있는 복잡한 형태의 스케줄을 정의할 수 없다고 말씀드렸습니다.
AIP-39에서는 이런 문제를 해결하기 위해 복잡한 시간을 표현할 수 있는 Timetable(https://airflow.apache.org/docs/apache-airflow/stable/concepts/timetable.html)이라는 새로운 클래스가 도입되었습니다. 여기서 모든 내용을 다룰 수는 없기에 간단하게 살펴보겠습니다. 더 자세한 내용은 공식 문서에서 확인할 수 있습니다.
참고로, 크론 표현식은 여전히 널리 쓰이기 때문에 사라지지 않고 계속 유지될 예정입니다. 사용자들은 상황에 맞추어 schedule_interval에 크론 표현식 혹은 Timetable 객체를 사용하면 됩니다.
class Timetable(Protocol):
def next_dagrun_info(
self,
*,
last_automated_data_interval: Optional[DataInterval],
restriction: TimeRestriction,
) -> Optional[DagRunInfo]:
"""
스케줄러가 이 함수를 호출해 가장 마지막에 처리된 `data_interval`를 확인하고,
이에 따라 Dag가 실행되어야 하는 다음 스케줄(`DagRun`)을 리턴합니다.
:param last_automated_data_interval: 이미 실행된 스케줄(`DagRun`) 중
가장 마지막에 실행된 스케줄에서 처리된 `data_interval` (manual run 제외)
:param restriction: DAG 스케줄을 결정할 때 고려해야 할 제약사항을 담고 있습니다.
DAG를 정의할 때 설정하는 `start_date`, `end_date`, `catchup` 등의
정보가 담겨 있습니다.
"""
먼저 스케줄 시점을 결정할 Timetable 의 추상 클래스를 살펴봅니다. 스케줄러는 일정한 간격으로 Timetable의 next_dagrun_info를 찔러서 Dag의 다음 스케줄이 필요한지 확인합니다. 이 함수에서 마지막으로 실행된 Dag 스케줄(DagRun)에서 처리된 데이터의 범위(date_interval)을 보고, 바로 다음에 실행되어야 할 Dag 스케줄(DagRunInfo)을 리턴하면 스케줄러가 그 시간에 Dag를 실행하게 됩니다.
기존과는 달리 1. Dag의 마지막 실행 날짜(DagRun)가 아닌 해당 Dag에서 처리한 데이터의 범위(date_interval)를 기반으로 다음 Dag가 실행될 스케줄을 결정하게 되고, 2. 스케줄 시점을 파이썬 코드를 이용해서 작성할 수 있기 때문에 DAG의 스케줄 시점을 입맛에 맞게 자유롭게 설정할 수 있습니다.
class DagRunInfo(NamedTuple):
run_date: DateTime
"""
Dag가 실행되어야 하는 스케줄(DagRun) 중 가장 빠른 시점을 나타냅니다.
만약 None이라면 Dag의 실행 스케줄이 아직 결정되지 않았다는 것을 의미합니다.
"""
data_interval: DataInterval
"""
Dag가 실행될 때 처리해야 하는 데이터의 기간을 표현합니다. ``[start, end)``
"""
다음으로 Dag의 스케줄 정보를 나타내는 DagRunInfo 클래스를 살펴봅니다. 스케줄러가 Timetable에 다음 실행 스케줄을 물어보면 이 클래스가 리턴됩니다. run_date는 Dag가 실행되어야 할 다음 스케줄을 알려주고, data_interval은 해당 스케줄에서 처리해야 할 데이터 기간의 범위를 표현합니다.
DAG가 실행되어야 하는 시점(run_date)과 처리해야 하는 데이터의 범위(data_interval)가 명확하게 분리되어 있는 것을 확인할 수 있습니다.
그렇다면 새로운 인터페이스가 좋은 것은 알겠는데, 기존과 다르게 뭘 할 수 있는걸까요? 앞서 들어보았던 예시로 살펴보겠습니다.
1. 정해진 시점의 데이터를 기록하는 스냅샷을 찍고 싶어요
스냅샷을 찍을 때는 특정 시점의 데이터를 기록하는 것이기 때문에 data_interval의 개념이 필요 없습니다. 이 경우 data_interval_start, data_interval_end 그리고 run_date 모두 같은 날짜가 됩니다.
2. 스케줄 간격과 처리해야 하는 데이터의 기간이 달라요
'하루에 한번, 자정에 가장 최근 7일 데이터를 처리'해야 하는 경우 run_date는 매일 자정으로 설정하고 data_interval을 7일로 설정합니다. 이제 스케줄 시점과 데이터 처리 기간을 따로 명시할 수 있기 때문에, 불필요한 계산 없이 작업 시점과 범위를 명확하게 표현할 수 있게 되었습니다.
또 다른 활용 예는 '하루에 한번 전날의 데이터를 자정이 아니라 새벽 2시에 처리'해야 하는 경우입니다. 예를 들면 서드파티에서 데이터를 가져오는데, 하루가 끝나고 데이터가 처리되는 데 약간의 시간이 걸려서 자정에 바로 데이터가 들어오는 것이 아니라 약간의 딜레이가 있어 새벽 2시 쯤에 들어오는 것이죠.
물론 기존에는 TimeSensor를 이용해 2시간을 기다렸다가 실행하거나, 크론 표현식을 0 2 * * * 와 같이 설정해, 작업이 자정이 아니라 새벽 2시에 시작되게 하는 방법을 사용했습니다. 하지만 전자는 스케줄 자원이 낭비되고, 후자는 execution_date가 2시로 설정돼 실제 데이터의 시간과 맞지 않는 문제가 있었습니다.
3. '공휴일을 제외한 영업일'과 같은 복잡한 스케줄을 하고 싶어요
Timetable을 구현할 때 휴일 정보를 제공하는 확인하는 API 등을 이용해 복잡한 스케줄을 자유롭게 구성할 수 있습니다. 다만, 스케줄을 판단하는 로직이 너무 오래 걸릴 경우 스케줄러의 퍼포먼스에 영향을 줄 수 있다는 점을 고려해야 합니다. 2.0 버전부터 지원되는 스케줄러 HA(고가용성)를 구성하면 이런 위험에서 조금 더 자유로울 수 있습니다.
Airflow 스케줄링의 Execution Date(2.2 이후 Logical, Data Interval)를 사용할 때 헷갈리는 부분을 정리합니다.
용어 정리
Airflow에는 스케줄링을 위한 여러 가지 날짜 개념이 존재하며,
대표적으로 start_date, end_date, execution_date 등이 있다.
Airflow에서는 일반적으로는 이해하는 실행 날짜에 대한 개념과 조금 다른 의미를 가진다.
start_date
job을 처음 예약하는 시간
start_date가 미래일 경우 Airflow DAG가 실행되지 않는다
end_date
마지막 job이 예약되는 시간
execution_date
execution_date는 그 이름과는 다르게, Task가 실제로 실행되는 시간이 아니라 해당 Task가 실행하고자 하는 Time Window의 시작 지점 을 의미
Execution Date 동작 방식
from datetime import datetime
from airflow import DAG
with DAG(
dag_id="execution_date_example",
start_date=datetime(2025, 11, 1),
end_date=datetime(2025, 11, 10),
catchup=False,
schedule="0 2 * * *",
) as dag:
즉 start_date 가 2025–11–01이며 매일 오전 2시에 실행되는 작업이 있다면,
execution_date가 2025–11–01T02:00:00 인 DAG는 2025–11–02T02:00:00에 실행된다.
| DAG가 예약된 시간 (schedule) | DAG가 실제로 실행된 시간 | Execution Date |
| ---------------------- | ------------------- | ------------------- |
| 2025-11-01 02:00:00 | 2025-11-02 02:00:00 | 2025-11-01 02:00:00 |
| 2025-11-02 02:00:00 | 2025-11-03 02:00:00 | 2025-11-02 02:00:00 |
| 2025-11-03 02:00:00 | 2025-11-04 02:00:00 | 2025-11-03 02:00:00 |
| 2025-11-04 02:00:00 | 2025-11-05 02:00:00 | 2025-11-04 02:00:00 |
| 2025-11-05 02:00:00 | 2025-11-06 02:00:00 | 2025-11-05 02:00:00 |
| 2025-11-06 02:00:00 | 2025-11-07 02:00:00 | 2025-11-06 02:00:00 |
| 2025-11-07 02:00:00 | 2025-11-08 02:00:00 | 2025-11-07 02:00:00 |
| 2025-11-08 02:00:00 | 2025-11-09 02:00:00 | 2025-11-08 02:00:00 |
| 2025-11-09 02:00:00 | 2025-11-10 02:00:00 | 2025-11-09 02:00:00 |
| 2025-11-10 02:00:00 | 2025-11-11 02:00:00 | 2025-11-10 02:00:00 |

전통적인 배치 데이터 처리 방식은 작업의 대상이 되는 데이터의 기간이 있고, 그 대상 기간이 지나고 나서 해당 기간에 발생한 데이터를 처리하게 됩니다.
그렇기 때문에 위 그림으로 보면 25-04–09T02:00:00 ~ 25-04–10T02:00:00 까지 생성된 데이터를 execution_date(25–04–09T02:00:00)를 기준으로 처리합니다.
Backfill 또는 Manual 로 실행된 DAG에서 Execution Date동작 방식
airflow dags trigger <dag_id> --exec-date <?>
- 2025-11-01T02:00:00
- 2025-11-02T02:00:00
정답은 1번이며, 직접 날짜를 지정(--exec-date)할 경우 기존에 방식처럼 하나 앞의 datetime을 사용하는게 아닌 직접 지정한 값을 사용한다.
$ airflow dags trigger execution_date_example --exec-date 2025-11-01T02:00:00
# [2025-11-04T09:55:41.391+0000] {__init__.py:43} INFO - Loaded API auth backend: airflow.api.auth.backend.basic_auth
# [2025-11-04T09:55:41.392+0000] {__init__.py:43} INFO - Loaded API auth backend: airflow.api.auth.backend.session
# | | | data_interval_ | data_interval_ | | external_trigg | last_schedulin | | | |
# conf | dag_id | dag_run_id | start | end | end_date | er | g_decision | logical_date | run_type | start_date | state
# =====+================+================+================+================+==========+================+================+===============+==========+============+=======
# {} | execution_date | manual__2025-1 | 2025-10-31 | 2025-11-01 | None | True | None | 2025-11-01 | manual | None | queued
# | _example | 1-01T02:00:00+ | 02:00:00+00:00 | 02:00:00+00:00 | | | | 02:00:00+00:0 | | |
# | | 00:00 | | | | | | 0 | | |
logical_date(execution_date): 2025–11–01T02:00:00
Backfill에서 execution_date가 설정되는 기준
airflow dags backfill <dag_id> -s <date> -e <date>
- -s <date>: Backfill 대상인 execution_date의 시작일
- -e <date>: Backfill 대상인 execution_date의 종료일
여기서도 trigger 명령어와 동일하게 지정한 날짜를 execution_date로 사용한다
Backfill 기간 지정 시 주의사항
만약 매일 2시에 실행되는 DAG를
airflow dags backfill <dag_id> -s 2025–11–01 -e 2025–11–03 명령어를 사용하여 Backfill할 경우 다음과 같은 Backfill 작업이 실행된다.
참고로 시간을 지정하지 않으면 모두 00으로 채워진다
- 2025–11-01T00:00:00 ~ 2025–11-03T00:00:00 사이의 execution_date를 찾음
→ [2025–11-01T02:00:00, 2025–11-02T02:00:00] - 해당 날짜를 execution_date로 가지는 DAG들이 Backfill 된다.
from datetime import datetime
from airflow import DAG
from airflow.operators.python import PythonOperator
with DAG(
dag_id="execution_date_example",
start_date=datetime(2025, 11, 1),
end_date=datetime(2025, 11, 10),
catchup=False,
schedule="0 2 * * *",
) as dag:
execution_task = PythonOperator(
task_id='print_execution_date',
python_callable=lambda **kwargs: print(f"""
Now DateTime is: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
Execution date is: {kwargs['ti'].execution_date}"""),
)
$ airflow dags backfill execution_date_example -s 2025-11-01 -e 2025-11-03
[2025-11-04T09:57:00.237+0000] {dagbag.py:545} INFO - Filling up the DagBag from /opt/airflow/dags
[2025-11-04T09:57:00.637+0000] {executor_loader.py:235} INFO - Loaded executor: CeleryExecutor
... INFO - [backfill progress] | finished run 0 of 2 | ...
...
... INFO - execution_date=2025-11-02 02:00:00+00:00, run_id=backfill__2025-11-02T02:00:00+00:00, run_start_date=2025-11-04 09:57:00.679021+00:00, run_end_date=2025-11-04 09:57:10.658985+00:00, run_duration=9.979964, state=success, external_trigger=False, run_type=backfill, data_interval_start=2025-11-02 02:00:00+00:00, data_interval_end=2025-11-03 02:00:00+00:00, dag_hash=None
...
... INFO - execution_date=2025-11-01 02:00:00+00:00, run_id=manual__2025-11-01T02:00:00+00:00, run_start_date=2025-11-04 09:57:00.671024+00:00, run_end_date=2025-11-04 09:57:10.660169+00:00, run_duration=9.989145, state=success, external_trigger=True, run_type=backfill, data_interval_start=2025-10-31 02:00:00+00:00, data_interval_end=2025-11-01 02:00:00+00:00, dag_hash=709734cea349e98cdc2be5c5df0d25bd
... INFO - [backfill progress] | finished run 2 of 2 | ...
Clear Task로 실행한 Task
Web UI에서 직접 DAG 실행 시 실행한 시간을 execution_date로 가지며,
이 때 clear_task를 통해 재실행하더라도 이전에 지정된 execution_date로 재실행 된다
Logical Date, data_interval_start, data_interval_end
Airflow 2.2 버전 이 후 부터 execution_date 대신에 logical_date, date_interval_start, date_interval_end로 변경됨에 따라 위 내용에 대해서 어떻게 동작하는지 설명합니다.
- logical_date: execution_date와 동일한 값이며, 위와 동일하게 동작합니다
- date_interval_start: interval의 시작 시점입니다
- date_interval_end: interval의 종료 시점입니다
how it works
위와 같이 manual 및 backfill 작업에서 logical_date는 동일한 방식으로 동작합니다. 그럼 date_interval_start, end는 어떻게 설정되는지 알아보겠습니다.
with DAG(
dag_id="best_practices_example_dag",
start_date=datetime(2024, 1, 1),
schedule="0 11 * * *", # 매일 11시에 작동하는 DAG
catchup=False,
) as dag:
위에서 설명한 개념을 이해하면 쉽게 이해할 수 있습니다.
- manual_trigger: 2025–11–09 10:56:50: 09일 11시 전의 trigger
아직 11–09일 11시 데이터가 모두 끝나지 않은 시점이기 때문에 다음과 같이 start, end가 설정됩니다.
- start: 2025–11–07 11:00:00+00:00
- end: 2025–11–08 11:00:00+00:00 - manual_trigger: 2025–11–09 11:01:50: 09일 11시 이후의 trigger
이 경우 11월 9일 11시 데이터가 모두 끝난 시점이기 때문에 다음과 같이 start, end가 설정됩니다.
- start: 2025–11–08 11:00:00+00:00
- end: 2025–11–09 11:00:00+00:00
Airflow 3.x
Airflow 3.x 이상부터는 create_cron_data_intervals 을 True로 설정해야 위와 같이 동작합니다.
# airflow.cfg
...
create_cron_data_intervals = True
...
결론
스케줄링된 작업은 Time Window를 기반으로 해당 Time Window가 시작되는 지점을 execution_date로 가지는 DAG을 해당 Time Window가 끝나는 지점에 실행하며,
trigger, Web UI에서 실행, Backfill로 실행되는 DAG는 지정한 시간을 execution_date로 사용하는 DAG를 실행한다.
Backfill & Catchup
Backfill 기능이란?
한 마디로 한다면 "Airflow에서 이전에 실행되지 않은 작업(태스크)을 자동으로 실행하는 과정"
- DAG(Directed Acyclic Graph)에서 이전의 실행 날짜나 특정 기간에 대한 작업을 다시 수행, backfill을 실행할 때 시작 날짜와 종료 날짜를 지정하여 특정 기간 동안의 작업을 재실행
- start_date 부터 시작하지만 end_date 은 포함하지 않음
- 주로 작업이 실패했거나 실행되지 않은 경우에 유용하게 사용
- execution_date를 사용해서 Incremental update가 구현되어 있어야 의미가 있음, master성(full refresh는 멱등성이 보장된 방법이기에 필요가 없다.)
Backfill 기능이 필요한 경우
- 작업 실패: 이전에 실행된 DAG의 일부 작업이 실패하거나 일부 작업이 누락된 경우
- 새로운 태스크 추가: 기존 DAG에 새로 추가된 태스크가 이전 실행에 적용되지 않았을 때
- 특정 날짜에 대한 작업 실행: 특정 날짜에 대해 수동으로 작업을 재실행하고 싶을 때
Backfill 실행 방법
- Airflow에서 backfill을 실행하면, DAG의 특정 날짜 범위에 대해 태스크들을 다시 실행
- Airflow는 주어진 날짜 범위에 대한 모든 태스크 인스턴스를 확인하고, 그 기간 동안 실행되지 않은 태스크들을 실행


dag = DAG(
'backfill_example',
default_args=default_args,
description='A simple backfill example',
schedule_interval=timedelta(days=1), # 매일 실행
start_date=datetime(2025, 3, 1),
catchup=True, # 이 옵션이 True일 경우 backfill이 발생함
depends_on_past=True, # 이 옵션이 True일 경우, 이전의 DAG 실행이 실패 한 경우, 다음 실행이 되지 않음
)
명령어 실행 방법
airflow dags backfill <DAG_ID> -s <START_DATE> -e <END_DATE> [옵션들]
| 인수 | 설명 | 예시 |
| <DAG_ID> | 백필할 DAG의 ID. 이 인수는 필수 | example_dag |
| -s <START_DATE> | 백필 시작 날짜. 백필할 시작 날짜를 설정합니다. 날짜 형식은 YYYY-MM-DD. | -s 2023-01-01 |
| -e <END_DATE> | 백필 종료 날짜. 백필할 종료 날짜를 설정합니다. 날짜 형식은 YYYY-MM-DD | -e 2023-01-07 |
| -t <TASK_ID> | 특정 태스크만 백필할 경우 해당 태스크의 task_id를 지정합니다. 만약 이 인수를 사용하면 지정된 태스크만 백필 | -t task1 |
| --mark_success | 백필이 완료되면 해당 실행을 "성공"으로 표시합니다. 이 옵션을 사용하면 백필이 완료된 후 실행된 DAG 인스턴스는 success 상태로 설정 | --mark_success |
| --rerun_failed_tasks | 실패한 태스크만 다시 실행합니다. 기본적으로는 백필이 시작되면 모든 태스크가 실행되지만, 이 옵션을 사용하면 실패한 태스크만 실행 | --rerun_failed_tasks |
| --exclude_queued | 대기 중인 태스크를 제외하고 백필합니다. 이 옵션을 사용하면 대기 중인(queued) 상태의 태스크는 백필에서 제외 | --exclude_queued |
| --dry_run | 실제로 실행하지 않고 시뮬레이션만 합니다. 백필이 어떤 태스크를 실행할지 확인하고 싶은 경우에 사용 | --dry_run |
| --no_confirm | 사용자 확인을 생략하고 즉시 실행합니다. 이 옵션을 사용하면 백필 시작 전에 사용자 확인을 생략하고 바로 시작 | --no_confirm |
| --verbose | 백필의 상세 로그를 표시합니다. 실행 과정에서 발생하는 로그를 더 자세히 보고 싶을 때 사용 | --verbose |
| --pool <POOL> | 지정된 풀에서 백필을 실행합니다. Airflow는 태스크 실행을 풀에 따라 제한할 수 있습니다. 풀 이름을 지정하여 백필을 실행할 수 있음 | --pool example_pool |
| --wait_for_downstream | 하위 태스크가 완료될 때까지 기다립니다. 이 옵션을 사용하면, 상위 태스크가 실행된 후 하위 태스크가 완료될 때까지 기다렸다가 실행 | --wait_for_downstream |
trigger_rule vs depends_on_past
둘 다 의존성과 관련이 있다고 볼 수 있지만,
trigger_rule은 전 후 task에 대한 의존성을 즉, task끼리 실패 성공 여부에 따라 다음 task를 실행 할 지 판단하는 파라미터라면,
depends_on_past는 dag전체에 대한 실패 성공 여부에 따라 다음 dag를 실행 할 지 판단하는 파라미터이다.
주의할 점
backfill이란 것은 결국 설정한 start_date 값으로 부터 하나씩 차례로 dag를 실행시키는 것이기 때문에 결국 변수는 start_date(execution_date)이라는 것이 핵심
즉, DAG내 실행시키는 작업이 python, SQL인 경우에 변수가 start_date를 사용하지 않는 경우라면, 동일한 코드를 과거시점에서부터 현재까지 여러번 실행시키게 될 뿐, 의도한 과거의 데이터부터 현재까지의 데이터가 채워지지 않게 될 것입니다.
Catchup
Apache Airflow에서 catchup 옵션은 DAG(Directed Acyclic Graph)가 실행되지 않는 기간 동안 발생한 task instance를 backfill 또는 누락되거나 건너뛴 task instance를 catchup 할지 여부를 결정합니다.

catchup = False
- 현재 날짜 이후부터 task를 스케쥴링
catchup = True
- scheduler가 마지막 data interval 이후로 실행되지 않거나 지워진 모든 interval를 탐색하여 재실행합니다.
trouble shooting

제가 backfill 외에 catchup을 사용하려고 했던 dag는 위와 같이 catchup tasks가 scheduled tasks 사이에 존재하는 경우였습니다.
- 예상한 시나리오
- (missed or skiped) tasks를 탐색하여 catchup 해줄 것을 기대.
- 실제
- 마지막으로 실행된 data interval이 catchup할 task보다 뒤에 존재하므로 current_date 이후 task에 대해서만 스케쥴링 함.
- 해결
- (missed or skiped) task 중, 최초 data interval에 대해 manual하게 실행하여 마지막 data interval을 원하는 위치로 조정.
best practices
catchup 옵션을 true로 설정하기 전에 주의해야하는 포인트 몇가지를 간략하게 정리합니다.
1.start_date와 schedule_interval을 올바르게 설정했는지 확인해야 합니다.
- date range가 크거나 data interval이 작은 경우에는 catchup 프로세스에 시간이 오래 걸리고 Airflow 시스템 속도가 느려질 수 있습니다.
2.Task dependencies를 확인해야 합니다.
- 다른 task에 dependancy가 있는지 airflow는 체크해주지 않으므로, 현재의 dag와 dependancy가 있는 task를 확인해야 합니다.
3. idempotent(멱등성)을 보장하는 task인지 확인해야 합니다.
- 언제 실행하더라도 같은 결과를 가져오는 task가 아니라면, catchup tasks에서 side effects를 발생할 수 있습니다.
마무리
지금까지 Airflow의 역사 중 가장 큰 변화라고 할 수 있는 AIP-39 Richer scheduler_interval에서 제안된 schedule_interval의 변신과 execution_date의 종말, 그리고 새로 도입된 Timetable에 대해서 살펴봤습니다. 기존에 혼재되어 있었던 개념인 작업의 실행 시점과 처리해야 하는 데이터의 범위가 명확하게 분리되었고, 파이썬 코드를 이용해서 DAG의 스케줄 시점을 입맛에 맞게 자유롭게 설정할 수 있게 되었습니다.
더 자세한 내용은 Airflow 위키에 있는 AIP-39 Richer scheduler_interval 문서를 참고해 주세요.
참고
- Airflow 위키의 공식 Proposal 문서 AIP-39 Richer scheduler_interval 에 제안 배경과 해결방법 등이 자세히 서술되어 있어요.
- Astronmer에서 발행한 Airflow 2.2: All You Need to Know 문서를 참고해 보시는 것도 좋습니다.
- https://www.bucketplace.com/post/2021-04-13-%EB%B2%84%ED%82%B7%ED%94%8C%EB%A0%88%EC%9D%B4%EC%8A%A4-airflow-%EB%8F%84%EC%9E%85%EA%B8%B0/
버킷플레이스 Airflow 도입기 - 오늘의집 블로그
탁월한 데이터플랫폼을 위한 Airflow 도입기
www.bucketplace.com

'🔥 Data Engineer > Airflow' 카테고리의 다른 글
| [Airflow] SLA(Service Layer Agreement) 서비스 수준 계약 정리 (0) | 2026.04.19 |
|---|---|
| [Airflow] - Airflow Sensor의 soft_fail (0) | 2026.04.18 |
| [Airflow] - Retry(exponential backoff) (0) | 2026.04.18 |
| [Airflow] - Sensor의 Reschedule & poke & Deferrable Operator (0) | 2026.04.17 |
| [Airflow] - DAG CI/CD (0) | 2026.04.15 |