이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 운영 가능한 Airflow DAG 예시는 어떤 schedule, catchup, task boundary, validation을 포함해야 하는가?
  • DAG 코드에서 업무 로직과 orchestration 책임을 어떻게 분리해야 하는가?
  • Java/Spring Batch를 호출하는 DAG는 어떤 parameter와 retry 정책을 가져야 하는가?
  • DAG 예시를 리뷰할 때 어떤 위험 신호를 봐야 하는가?

개요

Airflow DAG 예시는 Python 문법보다 운영 의미가 중요하다.

좋은 DAG는 target date, task boundary, retry, validation, owner, alert, catchup 의도를 드러낸다.

업무 계산 로직은 DAG 안에 길게 쓰지 않는다.

백엔드 batch나 SQL job을 호출하고 그 결과를 검증하는 형태가 운영하기 쉽다.

예시 DAG

from datetime import datetime, timedelta
from airflow.decorators import dag, task
from airflow.exceptions import AirflowFailException
 
default_args = {
    "owner": "data-platform",
    "retries": 2,
    "retry_delay": timedelta(minutes=5),
}
 
@dag(
    dag_id="daily_order_mart",
    schedule="0 3 * * *",
    start_date=datetime(2026, 1, 1),
    catchup=False,
    default_args=default_args,
    max_active_runs=1,
    tags=["orders", "mart"],
)
def daily_order_mart():
    @task
    def extract(ds=None) -> str:
        return run_batch("extract-orders", target_date=ds)
 
    @task
    def build_mart(raw_path: str, ds=None) -> str:
        return run_batch("build-order-mart", target_date=ds, raw_path=raw_path)
 
    @task
    def validate(mart_partition: str) -> None:
        result = run_quality_rule("order-mart-daily", mart_partition)
        if not result.passed:
            raise AirflowFailException(result.summary)
 
    validate(build_mart(extract()))
 
daily_order_mart()

이 예시는 DAG가 batch를 직접 구현하지 않고 호출만 한다.

ds를 넘겨 logical date 기준으로 실행한다.

validation task를 마지막에 둔다.

max_active_runs=1로 같은 DAG의 동시 실행을 제한한다.

Schedule

schedule은 business freshness 요구와 연결된다.

03:00 실행이면 source 데이터가 03:00 전에 도착해야 한다.

source가 03:30에 늦게 도착한다면 DAG는 매일 실패하거나 빈 데이터를 만들 수 있다.

schedule은 source availability와 report SLA 사이에서 정한다.

Catchup

catchup=False는 과거 미실행 구간을 자동으로 모두 실행하지 않겠다는 뜻이다.

과거 데이터를 채워야 한다면 별도 backfill 절차로 실행한다.

무심코 catchup=True를 켜면 과거 수백 개 DAG run이 한꺼번에 target을 건드릴 수 있다.

catchup 설정은 반드시 의도를 적는다.

Task Boundary

extract, build, validate를 분리한 이유는 실패 복구 범위를 나누기 위해서다.

extract 실패와 validation 실패는 원인과 대응이 다르다.

report 발송이나 downstream publish가 있다면 validation 이후 task로 둔다.

모든 것을 한 task에 넣으면 DAG 상태가 원인을 설명하지 못한다.

Batch 호출

백엔드 batch 호출은 deterministic해야 한다.

java -jar batch.jar build-order-mart --target-date 2026-06-30

now() 기준으로 처리하면 retry와 backfill에서 결과가 달라진다.

target date, input path, run id를 명시적으로 넘긴다.

batch job은 같은 parameter로 두 번 실행해도 target 결과가 같아야 한다.

Validation Task

validation은 선택 사항이 아니다.

SELECT count(*) > 0
FROM mart_order_daily
WHERE order_date = :target_date;

간단한 rule이라도 DAG 안에 넣어야 DAG success와 데이터 success를 가깝게 만들 수 있다.

count, freshness, amount sum 같은 rule을 점진적으로 늘린다.

운영 지표

  • DAG run duration
  • task duration
  • retry count
  • validation failure count
  • max active run blocked count
  • backfill run count
  • freshness lag after DAG success

DAG 예시는 코드와 함께 어떤 지표를 볼지도 포함해야 한다.

개인 프로젝트 기준

개인 프로젝트에서는 실제 Airflow가 부담스럽다면 DAG 코드를 문서와 함께 남겨도 된다.

가능하면 Docker Compose로 Airflow를 띄우고 daily order mart DAG 하나를 실행한다.

task 하나를 실패시킨 뒤 retry와 downstream skip을 확인한다.

validation 실패가 DAG failure로 표시되는지 확인한다.

위험 신호!

  • DAG 안에 복잡한 정산 로직이 길게 들어간다.
  • datetime.now()로 target date를 정한다.
  • catchup 설정 의도가 없다.
  • validation task 없이 publish task가 있다.
  • 같은 DAG run이 동시에 target table을 쓴다.

확인 질문

확인 질문

  • DAG의 logical date가 batch parameter로 전달되는가?
    • ds나 data interval을 사용해야 한다.
  • DAG success가 데이터 품질 success와 어떻게 연결되는가?
    • validation task가 있어야 한다.
  • backfill을 실행하면 regular run과 충돌하지 않는가?
    • max active runs, target partition, idempotent write를 확인해야 한다.

참고 문서