Airflow 3 업그레이드 (3): 파괴적 변경 목록과 logical_date/data_interval 의미 변화
Apache Airflow Official Documentation (in-repo snapshot) — Apache Software Foundation installation/upgrading_to_airflow3.rst, 'Breaking Changes' ~ 끝 (L348-455)
이 모듈을 다 읽으면
- Airflow 3에서 제거된 SubDAGs, Sequential Executor, SLA 등 주요 기능이 무엇으로 대체되었는지 말할 수 있다
- xcom_pull()의 기본 동작이 어떻게 바뀌었는지, 왜 이것이 조용한 버그를 유발할 수 있는지 설명할 수 있다
- 수동 트리거된 DagRun에서 logical_date와 data_interval의 관계가 더 이상 자동 보장되지 않는 이유를 설명할 수 있다
- create_cron_data_intervals 기본값 변경이 실제로 어떤 Dag에 영향을 주는지 판단할 수 있다
Airflow 3에서 제거되거나 크게 동작이 바뀐 항목들을 정리한다. 기능 제거 목록(SubDAGs, Sequential Executor 등)과 함께, 특히 조용히 버그를 유발할 수 있는 두 가지 미묘한 동작 변화 — xcom_pull()의 기본 검색 범위 축소, 그리고 수동 트리거 DagRun에서 logical_date와 data_interval의 관계 — 를 자세히 다룬다.
제거된 기능들과 대체재
Airflow 2.x에서 deprecated 되었던 기능 중 다음은 Airflow 3에서 완전히 사용할 수 없다.
SubDAGs는 TaskGroups, Assets, Data Aware Scheduling으로 대체되었다. Sequential Executor는 SQLite와 함께 로컬 개발 용도로 쓸 수 있는 LocalExecutor로 대체되었다. CeleryKubernetesExecutor와 LocalKubernetesExecutor는 여러 Executor를 동시에 구성하는 Multiple Executor Configuration으로 대체되었다. SLA는 완전히 제거되었고 Deadline Alerts로 대체된다. 많은 CLI 명령의 `--subdir`/`-S` 인자는 Dag Bundles 개념으로 대체되었다. REST API `/api/v1`은 새로운 FastAPI 기반 안정 `/api/v2`로 완전히 대체되었다.
또한 태스크 인스턴스 컨텍스트에서 `tomorrow_ds`, `tomorrow_ds_nodash`, `yesterday_ds`, `yesterday_ds_nodash`, `prev_ds`, `prev_ds_nodash`, `prev_execution_date`, `prev_execution_date_success`, `next_execution_date`, `next_ds_nodash`, `next_ds`, `execution_date` 같은 컨텍스트 변수들이 더 이상 제공되지 않는다. 대체하지 않고 그대로 Dag에서 참조하면 Dag 오류를 유발한다.
핵심 포인트
- SubDAGs→TaskGroups/Assets/Data Aware Scheduling, Sequential Executor→LocalExecutor, CeleryKubernetesExecutor/LocalKubernetesExecutor→Multiple Executor Configuration, SLA→Deadline Alerts로 각각 대체되었다
- --subdir/-S는 Dag Bundles로, REST API /api/v1은 FastAPI 기반 /api/v2로 완전히 대체되었다
- execution_date, prev_ds, yesterday_ds 등 다수의 템플릿 컨텍스트 변수가 제거되어, 그대로 남아있으면 Dag 오류가 난다
기본값이 바뀐 설정들
`catchup_by_default` Dag 파라미터의 기본값이 이제 `False`다. `create_cron_data_intervals` 설정의 기본값도 `False`로 바뀌어, 기본적으로 `CronDataIntervalTimetable` 대신 `CronTriggerTimetable`이 사용된다.
이 변화는 `schedule=`에 순수 cron 문자열(예: `"0 0 * * *"`)을 넘기는 Dag에만 영향을 준다 — 명시적 timetable 인스턴스를 넘기는 Dag은 영향을 받지 않는다. 태스크에서 `data_interval_start`/`data_interval_end`(그리고 이로부터 파생되는 `ds`, `ts` 같은 템플릿 값)에 의존하고 있다면, 두 timetable 사이에서 이 값들이 달라진다는 점을 염두에 두어야 한다. 기존 동작(`CronDataIntervalTimetable`)을 유지하고 싶다면 업그레이드 전에 명시적으로 `create_cron_data_intervals=True`를 설정해야 한다. 그렇지 않다면 새 `False` 기본값을 그대로 써도 무방하다.
이 플래그는 반드시 업그레이드 '전에' 설정해야 한다. 만약 Airflow 3에서 이미 일부 DagRun이 생성된 후에 플래그를 바꾼다면(`CronTriggerTimetable` → `CronDataIntervalTimetable` 방향), 이전 실행의 `logical_date`와 충돌하는 것을 피하기 위해 예정된 실행 하나가 건너뛰어진다.
핵심 포인트
- catchup_by_default 기본값이 False로 바뀌었다
- create_cron_data_intervals 기본값이 False가 되어, 순수 cron 문자열 schedule을 쓰는 Dag은 기본적으로 CronTriggerTimetable을 쓰게 된다(명시적 timetable을 넘긴 Dag은 영향 없음)
- 기존 CronDataIntervalTimetable 동작을 유지하려면 업그레이드 전에 create_cron_data_intervals=True를 명시해야 하며, 업그레이드 후 뒤늦게 바꾸면 실행 하나가 건너뛰어질 수 있다
인증 관련 변경: Simple Auth와 /auth 프리픽스
Simple Auth가 이제 기본 `auth_manager`가 되었다. 계속 FAB를 Auth Manager로 사용하려면 FAB provider를 설치하고 `auth_manager`를 아래처럼 명시적으로 설정해야 한다.
airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager
또한 Auth Manager가 정의하는 AUTH API 라우트들은 이제 `/auth` 경로 프리픽스가 붙는다. 애플리케이션 바깥에서 참조되는 URL(예: OAuth 리다이렉트 URL)도 이에 맞춰 갱신해야 한다. 예를 들어 Airflow 2.x에서 `https://<your-airflow-url.com>/oauth-authorized/google`이었던 URL은 Airflow 3.x에서 `https://<your-airflow-url.com>/auth/oauth-authorized/google`이 된다.
핵심 포인트
- Airflow 3의 기본 auth_manager는 Simple Auth이며, FAB를 계속 쓰려면 FAB provider 설치 후 auth_manager를 FabAuthManager로 명시해야 한다
- 인증 관련 API 라우트에 /auth 프리픽스가 붙으므로 OAuth 리다이렉트 URL 같은 외부 참조 URL도 함께 갱신해야 한다
xcom_pull()의 기본 검색 범위 축소
`task_ids` 인자 없이 `xcom_pull()`을 호출하는 동작이 바뀌었다. Airflow 2에서는 `task_ids`를 생략하면 해당 DagRun의 모든 태스크를 검색해 주어진 key에 대해 가장 최근에 push된 값을 반환했다. Airflow 3에서는 같은 호출이 오직 현재 태스크에서 push된 값만 검색한다.
# Airflow 2 - 어떤 태스크에서 온 것이든 가장 최근 값을 pull
value = ti.xcom_pull(key="shared_state")
# Airflow 3 - 동일한 호출이지만 현재 태스크만 검색
value = ti.xcom_pull(key="shared_state")
# Airflow 3 - 다른 태스크에서 pull하려면 task_ids를 명시해야 한다
value = ti.xcom_pull(task_ids="upstream_task", key="shared_state")
이 변경은 코드 형태가 그대로여도(같은 `xcom_pull(key=...)` 호출) 동작이 조용히 바뀌기 때문에 특히 주의가 필요하다. 업스트림 태스크가 쓴 XCom에 의존하던 Dag은 업그레이드 후 값을 못 찾고도 에러 없이 `None`을 반환할 수 있어, 반드시 `task_ids`를 명시하도록 코드를 점검해야 한다.
핵심 포인트
- Airflow 2의 xcom_pull(key=...)(task_ids 생략)은 DagRun 전체에서 최근 값을 찾았지만, Airflow 3에서는 현재 태스크로 범위가 축소된다
- 이 변경은 호출 코드가 동일해도 동작이 조용히 바뀌므로, 다른 태스크의 XCom을 읽던 코드는 반드시 task_ids를 명시하도록 고쳐야 한다
수동 트리거 DagRun과 logical_date / data_interval
스케줄된 실행에서는 `logical_date`와 `data_interval`이 둘 다 Dag의 timetable로부터 함께 파생된다. 하지만 Airflow 3에서 수동으로 트리거된 실행은 `data_interval_start`/`data_interval_end`가 사용자가 지정한 `logical_date`로부터 파생되거나 그와 같다고 가정해서는 안 된다. 결과로 나오는 `data_interval`은 timetable과 트리거 경로에 따라 달라지며, 일부 API는 data interval을 명시적으로 지정하는 것도 허용한다.
이 변화는 특히 다음 세 경우에 중요하다: 수동 실행 중 `data_interval_start`/`data_interval_end`를 사용하는 Dag, `TriggerDagRunOperator`로 하위 Dag을 트리거하는 Dag, 그리고 Airflow 2에서 마이그레이션하며 `data_interval_start`를 '요청된 수동 실행 날짜'로 취급해온 Dag.
Dag 로직이 사용자가 지정한 실행 날짜가 필요하다면 `logical_date`를 명시적으로 사용해야 한다.
from airflow.decorators import get_current_context, task
@task
def process_data():
context = get_current_context()
processing_date = context["logical_date"]
return f"Processing data for {processing_date}"
반대로 실행 구간(interval) 의미가 실제로 필요한 경우에는 계속 `data_interval_start`/`data_interval_end`를 사용하면 된다. Airflow 2에서 넘어올 때는 수동 트리거 워크플로가 `data_interval_start`/`data_interval_end`를 읽고 있다면, 실제로 필요한 것이 interval 의미였는지 아니면 사용자가 요청한 logical date였는지를 반드시 재검토해야 한다.
핵심 포인트
- Airflow 3의 수동 트리거 DagRun에서는 data_interval이 더 이상 logical_date로부터 자동으로 파생/동일하다고 가정할 수 없다
- TriggerDagRunOperator로 하위 Dag을 트리거하는 Dag, 수동 실행 중 data_interval을 읽는 Dag은 특히 영향을 받는다
- 사용자가 지정한 실행 날짜가 필요하면 logical_date를, 구간 의미가 필요하면 계속 data_interval_start/end를 명시적으로 사용해야 한다