
Table of Contents
(부제: 논리적 시간(Logical)과 물리적 시간(Physical) 분리하기)
개발자에게 “00시”는 하루의 끝이 아니다. 하루의 시작이자, 가장 예민한 시간이다.
500개가 넘는 배치가 동시에 “나부터 실행시켜줘!”라고 아우성치는 그 시간, 우리 데이터 팀의 슬랙은 항상 불타고 있었다.
1. The Dilemma: 정합성이냐, 효율성이냐 (죽느냐 사느냐)
데이터 엔지니어라면 누구나 겪는 가불기(딜레마)가 있다.
- 비즈니스 (Logical): “어제 매출 데이터는 오늘 00시 기준으로 마감되어야 하잖아요.” (Data Consistency)
- 인프라 (Physical): “500개를 동시에 돌린다고? DB 터트릴 일 있어?” (Resource Efficiency)
보통 여기서 우리는 타협한다.
“그래, ODS는 00시, DM은 01시, 리포트는 02시에 돌리자.”
하지만 우리는 이 타협을 거부했다. 대신 Airflow의 메커니즘을 극한으로 쥐어짜서 두 마리 토끼를 다 잡는 변태(?) 같은 아키텍처를 설계했다.
2. Anti-Pattern: 물리적 시간 분산의 함정 (시간차 공격)
가장 쉬운 해결책은 “시간 찢기“다. 하지만 이건 “기술 부채(Technical Debt)“를 일시불로 땡겨 쓰는 것과 같다.
왜냐고? 시간이 달라지는 순간 의존성 지옥(Dependency Hell)이 열리기 때문이다.
- Sensor 복잡도 떡상:
ExternalTaskSensor를 쓸 때마다 족보 정리를 해야 한다. - *”이 테이블은 1시간 전, 저 테이블은 30분 전…”* -> DAG 코드가 스파게티가 된다.
- SQL 쿼리의 오염: Jinja Template에서 시간을 비틀어 계산해야 한다.
{{ execution_date - macros.timedelta(hours=1) }}-> 쿼리 짤 때마다 날짜 계산기 두드려야 한다. (오타 나면 대참사)
우리는 이 꼴을 보기 싫어서 “스케줄링 시간은 통일하되, 실행 시점만 지연시키는 패턴“을 도입했다.
3. Architectural Decision: “게으른 실행(Lazy Execution)” 패턴
우리의 핵심 전략은 간단하다.
“논리적으로는 00시에 시작하되, 몸뚱아리(Worker)는 필요한 순간까지 움직이지 않는다.“
이를 위해 Deferable Operator와 Data Interval 개념을 섞어 찌개로 끓였다.
💡 잠깐! Data Interval이란?
Airflow 2.2부터 도입된 이 개념은 기존의 헷갈리던 execution_date를 대체하는 “데이터의 시간 범위“에 대한 표준이다.
- 과거:
execution_date가 실제 실행 시간인지, 데이터 기준 시간인지 혼란스러웠음. - 현재:
data_interval_start~data_interval_end로 명확하게 정의됨. - 예: 00시부터 01시까지의 데이터를 처리한다면?
start: 00:00,end: 01:00- 왜 중요한가? 이 시간 범위가 “불변의 기준점(Anchor)“이 되기 때문이다.
- Sensor가 데이터를 찾을 때도, SQL 쿼리가 날짜를 필터링할 때도 모두 이
data_interval을 기준으로 삼으면, 실제 DAG가 몇 시에 실행되든 상관없이 항상 “00시의 데이터“를 바라볼 수 있다.
Implementation Detail
모든 DAG의 schedule_interval은 00:00으로 고정한다. (이걸로 논리적 시간은 통일!)
대신, DAG의 가장 앞단에 TimeDeltaSensorAsync를 문지기로 세워둔다.
# 모든 DAG는 00시에 트리거되지만,
# 실제 Worker Slot을 점유하는 Heavy Task는 설정된 delta만큼 지난 후에 시작됨.
wait_task = TimeDeltaSensorAsync(
task_id="wait_until_target_time",
delta=timedelta(hours=2) # "02시까지 좀 쉬고 있을게요"
)
이 패턴의 강력함은 “대기하는 동안 Worker Slot을 먹지 않는다“는 점이다.
Airflow Triggerer가 가볍게 상태만 툭툭 체크하다가, 시간이 되면 그제야 Worker에 “야, 일해!” 하고 Task를 던져준다.
4. Key Benefits (이게 왜 개꿀인가?)
- Clean Dependency (의존성 관리의 단순화)
- 모든 DAG가 논리적으로 같은 시간(
00:00)을 공유한다.ExternalTaskSensor쓸 때delta계산? 그딴 거 필요 없다. 그냥 바라보면 된다.
- Clean Code (SQL/Template의 명확성)
- 쿼리 내에서
{{ data_interval_start }}를 그대로 쓰면 된다. - 억지로 시간을 비틀었다면 SQL 안에서
{{ data_interval_start - macros.timedelta(hours=1) }}같은 더러운 연산을 덕지덕지 발랐어야 했다.
- Resource Efficiency (리소스 효율성)
- 수백 개의 DAG가 동시에 뜨더라도, 실제 Worker CPU를 사용하는 시점은
00시 ~ 06시로 고르게 퍼진다. (Load Flattening) - 00시 정각의 CPU 피크가 사라지고, 아름다운 평지 그래프가 모니터에 그려진다. (마음의 평화는 덤이다.)
5. Conclusion
“편하자고 시간을 쪼개지 말자.“
당장의 리소스 병목을 피하기 위해 논리적 시간(Logical Time)을 포기하면, 나중에 더 큰 운영 비용을 치르게 된다. (미래의 내가 울고 있을 것이다.)
Airflow의 Deferable Operator를 활용하면, 정합성과 효율성이라는 두 마리 토끼를 우아하게 잡을 수 있다.
나중에 거대한 스파게티 괴물을 마주하고 싶지 않다면, 지금부터 ‘지속 가능한 오케스트레이션(Sustainable Orchestration)‘을 치열하게 고민해야 한다.
댓글 남기기