LLM WikiAccess-protected knowledge portal
← 스터디 홈
1편 · 약 22분

오케스트레이션 설계 원칙: DAG 경계, idempotency, dependency contract

오케스트레이션은 "실행 순서를 지정하는 것"이 아니다

Airflow나 Dagster 같은 오케스트레이터를 처음 접하면 "어떤 task를 어떤 순서로 실행할지 정의하는 도구"라는 인상을 받기 쉽다. 하지만 순서 지정은 오케스트레이션의 최소 기능이다. 실제로 오케스트레이터가 해결해야 하는 문제는 다른 곳에 있다.

  • 재실행 안전성: task가 실패해서 재시도할 때, 혹은 backfill할 때 이전 실행과 동일한 결과를 보장할 수 있는가?
  • 경계의 명확성: 이 DAG가 책임지는 데이터 범위와 다음 DAG가 기대하는 데이터 범위가 일치하는가?
  • 장애 격리: 한 task의 실패가 전혀 관련 없는 다른 task까지 멈추게 하는가?

이 세 가지 질문에 제대로 답하지 못한 파이프라인은 처음에는 잘 동작하다가 backfill, 스키마 변경, 팀원 추가, 데이터 볼륨 증가 중 어느 한 순간에 예측 불가능하게 깨진다. 오케스트레이션 설계 원칙은 그 깨짐을 미리 막는 구조적 결정이다.

DAG 경계
하나의 DAG가 책임지는
데이터·시간 범위를 명시
경계가 없으면 downstream이
기대하는 범위를 알 수 없다
Idempotency
같은 입력으로 N번 실행해도
결과가 한 번과 동일
재시도·backfill에서 중복이나
누락이 없어야 한다
Dependency contract
upstream이 제공하는 것과
downstream이 기대하는 것을 명시
암묵적 의존성이 운영 사고의
주요 원인이 된다
세 축이 함께 갖춰져야 backfill, 장애 복구, 팀 간 협업이 예측 가능하게 동작한다
오케스트레이션 설계의 세 축

1. DAG 경계: 하나의 DAG가 책임져야 하는 범위

DAG 경계는 두 가지 차원으로 생각한다. 시간 범위데이터 범위다.

1.1 시간 범위: logical date와 data interval

Airflow에서 각 DAG run은 logical_date(이전 이름: execution_date)를 가진다. 이 값은 DAG run이 처리해야 하는 시간 구간의 시작점이다. data_interval_startdata_interval_end는 그 DAG run이 다루는 데이터의 정확한 시간 구간을 나타낸다.

Airflow 문서는 이 구조를 통해 각 DAG run이 "어느 데이터 구간을 처리했는가"를 명확하게 정의할 수 있다고 설명한다. 예를 들어 schedule="@hourly"인 DAG에서 logical_date = 2026-07-02 10:00:00 UTC이면, 그 DAG run의 data_interval10:00:00 ~ 11:00:00이다.

이 구조가 중요한 이유는 backfill이다. 과거 구간을 재처리할 때 각 DAG run은 자신의 data_interval만 처리하면 된다. 경계가 명확하면 backfill을 병렬로 실행할 수 있고, 재처리 범위를 정확하게 제어할 수 있다.

반대로 경계가 모호한 DAG는 어떤 모습인가? task 코드 안에 datetime.now()today()를 직접 쓰거나, 가장 최근 데이터를 SELECT해서 처리하는 경우다. 이런 task는 언제 실행하느냐에 따라 결과가 달라지고, backfill 시 어느 구간을 처리했는지 알 수 없다.

나쁜 패턴: task 안에서 현재 시각을 사용
  SELECT * FROM events WHERE created_at >= NOW() - INTERVAL 1 HOUR

좋은 패턴: data_interval을 task에 명시적으로 전달
  SELECT * FROM events
  WHERE created_at >= '{{ data_interval_start }}'
    AND created_at <  '{{ data_interval_end }}'

1.2 데이터 범위: DAG 하나가 다루는 데이터 셋

DAG 경계의 두 번째 차원은 데이터 영역이다. "이 DAG가 어떤 테이블·파티션·토픽의 데이터를 처리하는가"를 명확히 해야 한다.

일반적인 설계 원칙은 하나의 DAG가 하나의 도메인 경계 또는 물리적 데이터 단위를 다루게 하는 것이다. 예를 들어 주문 이벤트를 정제해서 orders_daily 파티션을 만드는 DAG와, orders_daily를 집계해서 revenue_report를 만드는 DAG를 분리한다. 두 일을 하나의 DAG에 묶으면 한쪽이 실패해도 반쪽만 실행할 수 없고, 독립적인 retry도 어렵다.

Astronomer 문서는 이를 "모놀리식 DAG 대신 작고 집중된 DAG를 만들라"는 원칙으로 표현한다. 작은 DAG는 테스트하기 쉽고, 실패 범위가 좁으며, 다른 팀이 이해하기 쉽다.


2. Task 원자성: 하나의 task는 하나의 상태를 가진다

DAG가 적절한 크기로 쪼개졌다면, 그 안의 task 역시 원자적이어야 한다. task 원자성이란 task가 "실행 전" 또는 "완전히 실행됨" 두 상태만을 가지는 것이다. 중간 상태, 즉 "절반만 실행됨"은 허용되지 않는다.

원자성이 깨지는 대표적인 패턴은 다음과 같다.

패턴문제
하나의 task가 여러 테이블을 write테이블 A write 후 B write 실패 시 A만 변경된 불완전 상태
하나의 task가 조회·변환·적재를 모두 수행적재 실패 시 재시도가 변환을 다시 수행해 중복 가능
파일 부분 기록 후 명시적 commit 없이 종료재실행 시 부분 기록된 파일과 새 기록이 충돌

Astronomer 문서는 "ETL에서 Extract, Transform, Load를 각각 독립된 task로 나누어야 각 단계를 독립적으로 재실행할 수 있다"고 설명한다. 이 원칙은 Airflow뿐 아니라 모든 오케스트레이터에서 동일하게 적용된다.

task 원자성의 실질적 의미는 재시도 안전성이다. task가 실패해서 Airflow가 자동으로 재시도할 때, 이전 실행의 부분 결과가 남아 있어도 최종 결과가 올바르게 나와야 한다. 이것이 다음 절의 idempotency와 연결된다.


3. Idempotency: 재실행해도 결과가 같아야 한다

idempotency는 "같은 입력으로 몇 번 실행해도 최종 결과가 한 번 실행한 것과 동일하다"는 성질이다. Airflow 문서는 idempotent DAG를 설계하면 장애 복구 시간이 줄어들고 데이터 손실 가능성이 낮아진다고 설명한다.

3.1 Idempotency를 깨는 패턴

idempotency를 무의식적으로 깨는 패턴이 몇 가지 있다.

INSERT without deduplication. task가 테이블에 INSERT를 수행할 때 중복 방지 로직이 없으면, 재시도 시 동일한 row가 두 번 삽입된다. 해결책은 INSERT INTO ... ON CONFLICT DO NOTHING (PostgreSQL) 또는 MERGE/UPSERT를 사용하거나, 먼저 해당 파티션 데이터를 DELETE한 뒤 INSERT하는 것이다.

누적 집계. task가 기존 값에 값을 더하는 방식으로 집계하면, 재실행 시 이전 실행의 합산이 다시 더해진다. 해결책은 매 실행마다 집계를 처음부터 다시 계산하도록 한다.

외부 API 호출. 외부 시스템에 write하는 task(Slack 알림, 이메일 발송, 결제 요청)는 멱등성을 보장하기 어렵다. 이런 task는 별도 DAG로 분리하거나, 외부 시스템이 idempotency key를 지원하면 이를 사용한다.

시간 종속 로직. datetime.now()로 타임스탬프를 생성하면 재실행 시 다른 값이 들어간다. 대신 {{ data_interval_end }}처럼 오케스트레이터가 보장하는 값을 사용한다.

3.2 파티션 덮어쓰기 패턴

분석 데이터 파이프라인에서 가장 많이 쓰는 idempotent write 패턴은 파티션 덮어쓰기다. 날짜 파티션 테이블에 데이터를 적재할 때, 먼저 그 날짜 파티션 전체를 삭제하고 새로 INSERT한다. 이 방법은 재실행해도 항상 해당 날짜의 데이터가 정확히 한 번만 존재하게 보장한다.

-- 파티션 덮어쓰기 패턴 (BigQuery 스타일)
INSERT INTO orders_daily
PARTITION (dt = '{{ ds }}')
SELECT ...
FROM raw_orders
WHERE DATE(created_at) = '{{ ds }}'
-- BigQuery: write_disposition=WRITE_TRUNCATE + partition filter

object storage 기반 데이터 레이크에서는 "해당 날짜 prefix에 파일을 쓰기 전에 prefix 전체를 삭제한 뒤 새로 쓴다"는 패턴이 이에 해당한다.


4. Dependency contract: 암묵적 의존성이 장애를 만든다

DAG 간 의존성에는 두 종류가 있다. 명시적 의존성암묵적 의존성이다.

명시적 의존성은 Airflow의 ExternalTaskSensor, Dagster의 asset dependency, 또는 팀 간 합의된 SLA처럼 문서화되어 있고 시스템이 인식하는 의존성이다. 암묵적 의존성은 DAG A가 생성하는 테이블을 DAG B가 아무런 선언 없이 읽는 경우처럼, 코드만 보면 알 수 있지만 오케스트레이터가 인식하지 못하는 의존성이다.

암묵적 의존성이 위험한 이유는 단순하다. DAG A가 지연되거나 실패해도 DAG B는 이를 모르고 실행된다. 결과적으로 DAG B는 오래된 데이터나 불완전한 데이터를 가져다 집계하고, 그 집계 결과가 downstream 대시보드에 틀린 숫자로 표시된다. 이 오류는 발견되기까지 몇 시간에서 며칠이 걸릴 수 있다.

4.1 Dependency contract의 구성 요소

dependency contract는 upstream DAG가 downstream에게 제공하는 보장을 명시한 합의다. 최소한 다음 네 가지를 포함해야 한다.

항목설명예시
데이터 스키마downstream이 기대하는 테이블·파일 구조orders_daily(dt, order_id, amount, status)
완료 시점 SLAupstream이 완료를 보장하는 최신 시각"KST 07:00 기준 전일 데이터 완료"
완료 신호 방식downstream이 완료를 어떻게 감지하는가Airflow ExternalTaskSensor, _SUCCESS 파일, 메타데이터 테이블
Breaking change 정책스키마나 SLA가 바뀔 때 사전 통지 기간"7일 전 downstream 팀에 통지"

4.2 완료 신호 구현 패턴

downstream이 upstream의 완료를 어떻게 감지하는지는 구체적인 구현이 필요하다. 흔히 쓰는 세 가지 패턴이 있다.

ExternalTaskSensor (Airflow): 다른 DAG의 특정 task가 성공 상태일 때까지 기다린다. DAG 간 명시적 연결이 가능하지만, 두 DAG의 logical_date가 일치해야 하므로 스케줄이 다른 경우 처리가 복잡하다.

_SUCCESS 파일: object storage에 데이터 파일을 쓴 뒤 _SUCCESS 빈 파일을 마지막에 생성한다. downstream은 이 파일 존재 여부를 확인해 upstream 완료를 감지한다. 단순하고 어디서나 쓸 수 있지만, 실패 후 부분 기록이 남을 수 있어 멱등성 설계와 함께 써야 한다.

메타데이터 테이블: 파이프라인 완료 정보를 별도 테이블에 기록한다. pipeline_run_log(dag_id, run_date, status, completed_at) 형태로 관리하면 downstream sensor가 이 테이블을 쿼리해 의존성을 확인할 수 있다.

Upstream DAG
데이터 생성·파티션 적재
완료 신호 emit
(_SUCCESS · 메타데이터 · ExternalTaskSensor)
contract:
스키마·SLA·신호방식
Downstream DAG
완료 신호 확인 (Sensor)
데이터 읽기·집계·서빙
암묵적 의존성: downstream이 완료 신호 없이 직접 테이블을 읽음 → upstream 지연 시 틀린 데이터 집계 위험
DAG 간 dependency contract 구조

4.3 Breaking change 관리

dependency contract가 바뀔 때 발생하는 사고를 "계약 파기"라고 부를 수 있다. 흔한 계약 파기 시나리오는 다음과 같다.

  • upstream 팀이 컬럼 이름을 바꾸거나 삭제했는데 downstream 팀이 모름.
  • upstream SLA가 07:00에서 09:00으로 밀렸는데 downstream 대시보드 SLA는 08:00임.
  • upstream의 파티션 단위가 일별에서 시간별로 바뀌었는데 downstream sensor가 여전히 일별로 확인함.

이를 방지하려면 계약 변경 시 downstream 팀에 사전 통지하고, 가능하면 schema registry나 data catalog에 변경을 기록한다. 통지 없이 변경이 발생하는 경우를 막으려면 CI/CD에서 downstream에 영향을 주는 스키마 변경을 자동으로 감지하는 검사를 추가하는 것이 최선이다.


5. 의존성 방향: 불필요한 직렬화를 피한다

의존성을 정의할 때 흔히 저지르는 실수는 과도한 직렬화다. 실제로는 독립적으로 실행할 수 있는 task를 순서대로 연결해 두면, 앞 task가 실패할 때 전혀 관련 없는 뒤 task도 멈춘다.

나쁜 예: A → B → C → D (모두 직렬)
  A가 실패하면 B, C, D 모두 실행 불가

좋은 예: A → C, B → C, C → D (A와 B는 병렬)
  A와 B가 독립적이면 별도로 실행하고
  C는 A, B 둘 다 완료 후 실행

Airflow에서 task_a >> task_c 대신 [task_a, task_b] >> task_c를 쓰면 task_a와 task_b가 병렬로 실행된다. 이를 통해 전체 DAG run 시간이 줄고, 한 task의 실패가 다른 독립 task를 막지 않는다.

의존성 방향을 결정할 때 물어볼 질문은 하나다. "task B를 실행하려면 task A의 결과가 반드시 필요한가?" 그렇지 않다면 직렬 의존성을 제거하는 것이 낫다.


마무리: 설계 원칙은 복잡성 관리 도구다

오케스트레이션 설계 원칙이 필요한 이유는 파이프라인이 복잡해지기 때문이다. DAG 10개로 시작한 플랫폼이 300개가 되면, 누가 언제 어떤 DAG를 바꿔도 전체 시스템이 예측 가능하게 동작해야 한다. 그 예측 가능성을 보장하는 것이 DAG 경계, idempotency, dependency contract다.

경험상 이 원칙들이 지켜지지 않는 이유는 대부분 "처음부터 이런 문제를 겪을 거라고 몰랐기 때문"이다. 다음 편부터 다룰 Airflow 운영 심화 내용도 결국 이 세 원칙이 실제 코드와 운영에서 어떻게 나타나는지를 구체화한 것이다.

References

  • Apache Airflow Docs, "DAG Writing Best Practices": https://airflow.apache.org/docs/apache-airflow/stable/best-practices.html
  • Astronomer Documentation, "DAG Writing Best Practices": https://www.astronomer.io/docs/learn/dag-best-practices
  • Astronomer Blog, "10 Airflow Best Practices": https://www.astronomer.io/blog/10-airflow-best-practices/
  • Apache Airflow Docs, "Timetables": https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/timetable.html
  • Orchestra, "Airflow Concepts: Best Practices for Airflow DAGs": https://www.getorchestra.io/guides/airflow-concepts-best-practices-for-airflow-dags
  • Medium (Satyam Sahu), "5 DAG Design Principles That Make Your Airflow Pipelines Readable, Maintainable, and Production-Ready": https://medium.com/towards-data-engineering/5-dag-design-principles-that-make-your-airflow-pipelines-readable-maintainable-and-production-ready-05e2d8473886
  • Mage AI Blog, "Data Pipeline Orchestration: The Ultimate Guide": https://www.mage.ai/blog/data-pipeline-orchestration-the-ultimate-guide-for-data-engineers