솔직히 말하면 Airflow 날짜는 몇 번을 써도 헷갈린다.
이번에 제대로 당했다. 2월 12일 01:10에 DAG이 실행됐다.
그래서 당연히 2월 11일 데이터를 넣는 줄 알았다.
그런데 DB에 들어간 날짜는 20260210.
하루가 밀려 있었다. 쿼리를 의심했다. 파티션을 의심했다. Spark를 의심했다.
결론은 전부 아니었다. 로그를 다시 봤다.
execution_date : 2026-02-10 16:10:00+00:00
logical_date : 2026-02-10 16:10:00+00:00
ds_nodash : 20260210
data_interval_end : 2026-02-11 16:10:00+00:00
그 순간 깨달았다. 아, 이거 UTC구나.
나는 이렇게 생각했다.
“12일 새벽 1시 10분에 돌았으니까
11일 데이터를 처리하겠지.”
이 생각 자체가 잘못이었다.
Airflow는 실행 시각 기준으로 실행되지 않는다.
Airflow가 보는 것은 단 하나다.
이 DAG Run이 책임지는 시간 구간은 무엇인가?
Airflow는 모든 timestamp를 내부적으로 UTC로 저장한다.
하지만 데이터 interval은 DAG에 설정한 timezone 기준으로 계산된다.
찾아보니 이런 방식이였다
| 항목 | 의미 | 기준 |
|---|---|---|
| execution_date | DAG 실행 타임스탬프 | alias, UTC |
| logical_date | interval 시작 시점 | UTC |
| data_interval_start | 처리 interval 시작 | UTC |
| data_interval_end | 처리 interval 끝 | UTC |
| ds_nodash | logical_date 문자열 | UTC |
나는 이렇게 설정해두었다.
AIRFLOW__CORE__DEFAULT_TIMEZONE=Asia/Seoul
그래서 당연히 KST 기준으로 스케줄이 계산된다고 생각했다.
아니였다. 정확히 말하면
logical_date는 timezone-aware 객체이며, 내부적으로 UTC로 저장된다.DEFAULT_TIMEZONE은 UI 표시 및 naive datetime 해석에만 영향core.default_timezone은 DAG에 timezone을 명시하지 않았을 때 적용되는 기본 timezone이다.
UI 표시만 바꾸는 옵션은 아니며, DAG에 timezone을 지정하지 않으면 이 값을 기준으로 스케줄이 계산된다.
내가 이렇게 썼다.
op_kwargs={"target_date":"{{ ds_nodash }}" }
여기서 사고가 터졌다.
ds_nodash는 무엇인가?
→ logical_date 기반 문자열
→ 그리고 logical_date는 UTC
12일 01:10 KST 실행
= 11일 16:10 UTC
그리고 중요한 사실 하나:
logical_date는 “이 DAG Run이 책임지는 구간의 시작 시점”이다.
그래서 결과는 이렇게 된다.
ds_nodash =20260210
하루가 밀린 게 아니다. 내가 Airflow의 시간 모델을 잘못 이해한 거였다.
DAG:
schedule ="10 1 * * *"
timezone = Asia/Seoul
12일 01:10 KST 실행된 DAG의 내부 값:
logical_date = 2026-02-1016:10:00+00:00
data_interval_start = 2026-02-1016:10:00+00:00
data_interval_end = 2026-02-1116:10:00+00:00
이를 KST로 변환하면:
data_interval_start = 2026-02-1101:10
KSTdata_interval_end = 2026-02-1201:10 KST
즉 이 DAG Run은
11일 01:10 ~ 12일 01:10 구간을 책임진다.
여기서 핵심:
Airflow는
“지금 실행한다”가 아니라
“이 구간을 마감한다”라는 개념이다.
이게 진짜 함정이다. DAG 실행 시간은 데이터 처리 날짜가 아니다.
Airflow의 사고방식은 항상 이 구조다.
[data_interval_start ~ data_interval_end)
구간 중심 사고다. 시간 중심 사고로 접근하면 계속 하루 밀린다.
이제 ds_nodash를 거의 쓰지 않는다.
interval 기준으로 명시적으로 계산한다.
target_date = (
context["data_interval_end"]
.in_timezone("Asia/Seoul")
.strftime("%Y%m%d")
)
전날 데이터가 필요하면:
target_date = (
context["data_interval_end"]
.in_timezone("Asia/Seoul")
.subtract(days=1)
.strftime("%Y%m%d")
)
이제 날짜 계산을 코드로 명시한다.
암묵적 추론을 하지 않는다.
이 이슈 이후 바꾼 것:
ds_nodash 사용 최소화execution_date, logical_date, data_interval_end를 항상 로그에 출력이제 날짜가 밀리는 일은 없다.
추가로 Airflow 2.x에서는
execution_date는logical_date의 alias이며,실제 처리 구간은
data_interval_start ~ data_interval_end로 정의된다.
Airflow는 친절하지 않다.. 특히 날짜 관련해서는
“네가 정확히 이해하고 써”라는 시스템같다.
DEFAULT_TIMEZONE만 믿고 있으면 언젠가 반드시 한 번은 틀린다.
그래서 이번 기회를 통해 알아간다!
logical_date는 실행 시각이 아니라 책임 구간의 시작이다.ds_nodash는 단순 문자열 매크로일 뿐이다.Airflow 날짜는
“지금 몇 시에 돌았는가”가 아니라
“어떤 구간을 마감하는가”를 이해해야 풀린다.