Dag 선언과 태스크 의존성
Apache Airflow Official Documentation (in-repo snapshot) — Apache Software Foundation core-concepts/dags.rst - Declaring a Dag, Task Dependencies, Loading Dags, Dag Assignment, Default Arguments, The Dag decorator
이 모듈을 다 읽으면
- Dag를 선언하는 세 가지 방식(컨텍스트 매니저, 생성자, @dag 데코레이터)의 차이를 설명할 수 있다
- >>/<<, set_upstream/set_downstream, cross_downstream, chain의 용도 차이를 구분해 설명할 수 있다
- Airflow가 Python 파일에서 어떤 객체를 Dag로 인식하는지, 그리고 dag_discovery_safe_mode와 .airflowignore가 하는 역할을 설명할 수 있다
Dag는 태스크들 사이의 의존성과 실행 순서, 재시도·타임아웃 같은 실행 방식을 정의하는 객체로, 태스크 내부에서 실제로 무슨 일이 일어나는지에는 관여하지 않는다. 이 모듈은 Dag를 선언하는 세 가지 문법, 태스크 간 의존성을 표현하는 연산자와 헬퍼 함수, Airflow가 Python 파일에서 Dag를 로드하는 규칙, 그리고 default_args와 @dag 데코레이터를 다룬다.
Dag를 선언하는 세 가지 방법
Dag는 세 가지 방식으로 선언할 수 있다. 첫째는 ``with DAG(...)`` 컨텍스트 매니저를 쓰는 방식으로, 블록 안에 만들어지는 모든 것이 암묵적으로 그 Dag에 추가된다. 둘째는 표준 생성자를 써서 ``DAG(...)`` 객체를 만들고, 이후 각 오퍼레이터에 ``dag=`` 인자로 넘겨주는 방식이다. 셋째는 ``@dag`` 데코레이터로 함수를 Dag 생성 함수로 바꾸는 방식이다. Dag는 태스크(주로 Operator, Sensor, TaskFlow ``@task``) 없이는 의미가 없다.
핵심 포인트
- Dag 선언 방식은 with DAG(...) 컨텍스트 매니저, DAG() 생성자 + dag= 인자, @dag 데코레이터 세 가지다
- Dag 자체는 태스크를 실행하는 순서·재시도·타임아웃 등 '어떻게'만 다루고, 태스크 내부에서 '무엇을' 하는지는 관여하지 않는다
태스크 의존성 선언 방법
개별 태스크 간 의존성을 선언하는 대표적인 두 방법은 ``>>``/``<<`` 비트시프트 연산자와, 더 명시적인 ``set_upstream``/``set_downstream`` 메서드다. 이 둘은 동일한 일을 하지만, 일반적으로 가독성이 좋은 비트시프트 연산자가 권장된다.
더 복잡한 의존성을 위한 지름길도 있다. 태스크 목록을 다른 태스크 목록에 의존시키고 싶다면(각 원소가 서로 교차로 의존) ``>>``나 ``<<``만으로는 표현할 수 없으므로 ``cross_downstream``을 쓴다. 반대로 의존성을 사슬처럼 잇고 싶다면 ``chain``을 쓸 수 있는데, ``chain(op1, op2, op3, op4)``는 ``op1 >> op2 >> op3 >> op4``와 같다. ``chain``은 같은 크기의 리스트들 사이에서 페어와이즈(pairwise) 의존성을 만드는 데도 쓸 수 있다 — 이는 모든 원소가 서로 교차로 의존하는 ``cross_downstream``의 교차 의존성과는 다른 개념으로, ``chain(op1, [op2, op3], [op4, op5], op6)``는 ``op1 >> op2 >> op4 >> op6``와 ``op1 >> op3 >> op5 >> op6`` 두 개의 별도 경로를 만든다.
핵심 포인트
- >> / << 연산자와 set_upstream / set_downstream 메서드는 동일한 의존성 선언 결과를 만들며, 비트시프트 연산자가 권장된다
- cross_downstream은 태스크 목록 간 교차 의존성을, chain은 순차 의존성 또는 같은 크기 리스트 간 페어와이즈 의존성을 만든다
Dag 로딩 규칙과 dag_discovery_safe_mode
Airflow는 Dag 번들 안의 Python 소스 파일에서 Dag를 로드한다. 파일을 실행한 뒤 그 안에서 Dag 객체를 찾아 로드하는데, 이때 오직 파일의 최상위 레벨(``globals()``)에 있는 Dag 인스턴스만 인식한다. 예를 들어 함수 내부에서 지역 변수로 생성된 Dag는 그 생성자가 호출되더라도 로드되지 않는다. 하나의 Python 파일에 여러 Dag를 정의하거나, 반대로 하나의 복잡한 Dag를 여러 파일에 걸쳐 import로 나누어 정의하는 것도 가능하다.
Dag를 검색할 때 Airflow는 기본적으로 파일 내용에 대소문자 구분 없이 ``airflow``와 ``dag`` 문자열이 모두 포함된 Python 파일만 최적화 차원에서 고려한다. 이 동작은 ``[core] dag_discovery_safe_mode`` 설정 플래그로 끌 수 있으며, Dag가 래퍼나 추상화를 통해 정의되어 소스에 저 문자열들이 나타나지 않는 경우에 이 설정을 꺼야 한다. 이 플래그는 Dag 파일 처리기가 읽으므로, 이 컴포넌트(그리고 ``airflow dags reserialize``를 실행하는 모든 곳)에 설정하고 재시작해야 반영된다 — 그렇지 않으면 수동 reserialize 후 Dag가 잠깐 나타났다가 다음 처리기 스캔에서 다시 사라질 수 있다.
``.airflowignore`` 파일을 Dag 번들이나 그 하위 폴더에 두어 로더가 무시할 파일 패턴을 지정할 수도 있고, ``might_contain_dag_callable`` 설정으로 파일 파싱 필요 여부를 판단하는 콜러블을 직접 구현해 기본 휴리스틱을 대체할 수도 있다.
핵심 포인트
- Airflow는 Python 파일의 최상위 레벨(globals())에 있는 Dag 인스턴스만 로드하며, 함수 안에서 생성된 Dag는 로드되지 않는다
- 기본적으로 airflow와 dag 문자열을 모두 포함하는 파일만 탐색 대상이 되며, dag_discovery_safe_mode=False로 이 최적화를 끌 수 있다
- dag_discovery_safe_mode는 Dag 파일 처리기가 읽는 설정이므로 그 컴포넌트에 설정하고 재시작해야 반영된다
Dag 할당, Default Arguments, @dag 데코레이터
모든 Operator/Task는 실행되려면 반드시 어떤 Dag에 할당되어야 한다. Airflow는 명시적으로 전달하지 않아도 Dag를 계산해내는 여러 방법을 갖고 있다 — ``with DAG`` 블록 안에서 선언하거나, ``@dag`` 데코레이터 안에서 선언하거나, Dag가 있는 다른 Operator의 업스트림/다운스트림에 두는 경우다. 그렇지 않다면 각 Operator에 ``dag=``를 직접 전달해야 한다.
많은 Operator가 동일한 기본 인자(예: ``retries``)를 필요로 할 때마다 매번 개별적으로 지정하는 대신, Dag를 생성할 때 ``default_args``를 전달하면 그 Dag에 묶인 모든 Operator에 자동으로 적용된다.
Airflow 2.0부터는 컨텍스트 매니저나 ``Dag()`` 생성자 외에도 ``@dag``로 함수를 데코레이트해 Dag 생성 함수로 만들 수 있다. 이 방식은 함수의 파라미터를 그대로 Dag의 Params로 만들어주어, Dag를 트리거할 때 그 파라미터를 설정할 수 있게 해준다. 다만 ``@dag``로 함수를 정의하는 것만으로는 부족하고, 반드시 파일 안에서 그 함수를 최소 한 번 호출해 최상위 객체에 할당해야 Airflow가 인식한다(앞서 다룬 최상위 로딩 규칙과 동일한 이유).
핵심 포인트
- Operator는 with DAG 블록, @dag 데코레이터, 업스트림/다운스트림 관계, 또는 dag= 명시적 전달 중 하나로 Dag에 할당되어야 한다
- default_args를 Dag 생성 시 전달하면 그 Dag에 묶인 모든 Operator에 자동 적용된다
- @dag 데코레이터로 만든 Dag 생성 함수도 반드시 파일 최상위에서 호출·할당해야 Airflow가 인식한다