Modules Management — PYTHONPATH, 패키징, 이름 충돌 함정
Apache Airflow Official Documentation (in-repo snapshot) — Apache Software Foundation administration-and-deployment/modules_management.rst (전체)
이 모듈을 다 읽으면
- Airflow가 dags/config/plugins 세 폴더를 sys.path에 추가하는 방식의 차이(등록만 vs 실제 임포트)를 설명할 수 있다
- plugins 폴더의 .py 파일이 왜, 어떻게 표준 라이브러리 모듈과 충돌할 수 있는지 구체적으로 설명할 수 있다
- .airflowignore 파일의 역할과 필요한 위치를 설명할 수 있다
- 커스텀 코드를 공유하는 세 가지 방법(자동 PYTHONPATH 폴더, 수동 PYTHONPATH 추가, 패키지 설치)의 장단점을 비교할 수 있다
Airflow는 dags·config·plugins 세 폴더를 자동으로 sys.path에 추가해 커스텀 코드를 임포트할 수 있게 해주는데, 이 중 plugins 폴더만은 단순히 경로에 추가되는 것을 넘어 그 안의 모든 .py 파일이 파일 이름 그대로 최상위 모듈로 실제 임포트되어 표준 라이브러리·서드파티 모듈과 충돌할 위험이 있다. 이 모듈은 이 세 폴더의 차이, 이름 충돌을 피하는 관례, 그리고 상대 임포트를 피해야 하는 이유를 다룬다.
PYTHONPATH와 세 가지 코드 공유 방법
Airflow는 Dag와 Airflow 설정 안에서 사용자 자신의 파이썬 모듈을 쓸 수 있게 해준다. 커스텀 코드를 쓰는 방법은 세 가지다: Airflow가 자동으로 ``PYTHONPATH``에 추가하는 폴더 중 하나에 모듈을 두거나, 코드를 보관하는 별도 폴더를 ``PYTHONPATH``에 직접 추가하거나, 코드를 파이썬 패키지로 만들어 Airflow와 함께 설치하는 것이다.
파이썬이 모듈을 로드하려고 시도하는 디렉터리 목록은 ``sys.path`` 변수가 갖고 있다. ``sys.path``는 프로그램 시작 시 초기화되며, 최우선 순위는 현재 디렉터리(``path[0]``, 인터랙티브 셸이면 빈 문자열)이고, 두 번째 순위는 ``PYTHONPATH``(지정된 경우), 그다음이 ``site`` 모듈이 관리하는 설치 종속 기본 경로들이다. ``sys.path``는 세션 도중에도 ``sys.path.append(...)`` 같은 방식으로 수정할 수 있으며, Airflow는 PYTHONPATH에 디렉터리를 추가하는 기능에서 이 특성을 활용한다. ``sys.path``에는 ``site-packages`` 디렉터리도 있는데, 여기에는 pip나 conda로 설치한 외부 패키지들이 들어 있다.
핵심 포인트
- sys.path 탐색 우선순위는 현재 디렉터리 -> PYTHONPATH -> site 모듈이 관리하는 설치 종속 기본 경로 순이다
- sys.path.append(...)로 런타임에도 경로를 추가할 수 있으며, Airflow의 자동 PYTHONPATH 추가 기능이 이 방식을 활용한다
Airflow가 자동 추가하는 세 폴더와 plugins의 특수한 위험
Airflow는 런타임에 세 디렉터리를 ``sys.path``에 추가한다: ``dags`` 폴더(``[core] dags_folder``로 설정), ``config`` 폴더(``AIRFLOW_HOME`` 기준 기본값 ``{AIRFLOW_HOME}/config``), ``plugins`` 폴더(``[core] plugins_folder``로 설정).
여기서 반드시 알아야 할 비대칭이 있다. ``dags``와 ``config``는 ``sys.path``에 '추가'만 될 뿐이지만, ``plugins`` 폴더의 ``.py`` 파일들은 시작 시점에 실제로 임포트(actively imported)되고, 서브디렉터리 구조와 무관하게 그냥 파일명(bare filename)만으로 최상위 모듈로 등록된다. 이는 ``AirflowPlugin`` 서브클래스를 정의하는 파일뿐 아니라 plugins 트리 안의 모든 ``.py`` 파일에 적용된다. 예를 들어 ``plugins/my_company/utils/logging.py``라는 파일은 ``logging``이라는 모듈로 등록되어, 표준 라이브러리의 ``logging`` 모듈을 전역적으로 가려버리고 Airflow 자체가 시작하지 못하게 만든다. 서드파티 패키지 이름과의 충돌은 더 알아채기 어려운 실패로 이어진다.
또한 ``dags`` 폴더는 Webserver와 공유해서는 안 된다 — Airflow 1.10과 달리 Airflow 2/3는 Webserver가 dags 폴더에 접근할 것이라 가정하지 않으며, Dag를 작성하는 사람이 Webserver가 실행할 코드를 쓸 수 있게 되는 것은 보안 위험이기도 하다. Webserver와 코드를 공유해야 한다면 ``config``나 ``plugins`` 폴더, 또는 설치된 Airflow 패키지를 통해 공유하는 것이 권장된다 — 이 폴더들은 보통 Dag를 작성하는 데이터 과학자들과는 다른 사용자(관리자/DevOps)가 관리·접근하므로, 설치 구성의 일부로서 더 안전하다고 간주된다.
핵심 포인트
- dags·config는 sys.path에 경로만 추가되지만, plugins의 .py 파일은 서브디렉터리 깊이와 무관하게 파일명 그대로 실제 임포트되어 전역 모듈로 등록된다
- plugins/my_company/utils/logging.py는 logging이라는 이름으로 등록되어 표준 라이브러리 logging 모듈을 가리고 Airflow 시작 자체를 막을 수 있다
- dags 폴더는 보안상 Webserver와 공유하지 말고, 공유가 필요한 코드는 config·plugins·설치된 패키지를 통해야 한다
이름 충돌 방지 관례와 .airflowignore
가장 중요한 원칙은 ``PYTHONPATH`` 최상위에 두는 이름을 일반적인(generic) 이름으로 짓지 않는 것이다. 예를 들어 ``DAGS_FOLDER``에 ``__init__.py``가 있는 ``airflow``라는 폴더를 추가하면 Airflow 패키지 자체와 충돌해 Airflow 패키지로부터 아무것도 임포트할 수 없게 된다. ``airflow.py`` 파일을 최상위에 두는 것도 마찬가지다. ``multiprocessing``이나 ``logging`` 같은 표준 라이브러리 이름도 최상위 패키지(``__init__.py``가 있는 폴더)나 모듈(``.py`` 파일)로 쓰면 안 된다. 이 원칙은 ``PYTHONPATH``에 있는 ``config``·``plugins`` 폴더와, 사용자가 직접 추가한 모든 경로에도 동일하게 적용된다.
권장되는 방식은 항상 Dag·공유 파일을 배포별로 고유한 서브패키지(예제의 ``my_company``) 아래에 두는 것이다. 최상위 폴더에 일반적인 이름을 쓰면 시스템에 이미 존재하는 다른 패키지와 충돌하기 너무 쉽다 — 예를 들어 ``airflow/operators`` 서브폴더를 만들면 Airflow가 이미 ``airflow.operators`` 패키지를 갖고 있어 ``from airflow.operators``로 임포트할 때 그쪽을 먼저 찾으므로 접근할 수 없게 된다.
plugins 폴더에 대해서는 이 조언이 최상위 이름을 넘어 임의 깊이까지 확장된다 — 서브디렉터리에 파일을 두는 것으로는 충돌을 막을 수 없다. plugins 트리 안의 모든 ``.py`` 파일명은 고유해야 하고, 표준 라이브러리나 설치된 서드파티 모듈 이름과 겹치면 안 된다. Airflow가 이미 실행 중일 때 충돌하는 파일이 추가되면, 다음 재시작 전까지는 충돌이 발생하지 않아 원인을 진단하기 어려워진다.
``.airflowignore`` 파일은 ``dags`` 폴더 루트에 두어, 스케줄러가 Dag를 찾을 때 어떤 파일을 제외할지 알려준다. 기본적으로 정규표현식을 쓰고, glob 표현식도 쓸 수 있다. 이 파일은 다른 ``PYTHONPATH`` 폴더에는 둘 필요가 없다(그 폴더들에는 실제 Dag가 아니라 공유 코드만 두어야 하므로). 예를 들어 Dag가 ``my_custom_dags`` 폴더에만 있고 ``common_package``는 스캔에서 제외하고 싶다면, ``.airflowignore``에 ``my_company/common_package/``와 ``my_company/my_custom_dags/base_dag.py``(다른 Dag들이 상속하는 베이스 Dag 파일)를 glob 문법으로 적어두면 된다.
마지막으로 상대 임포트(``.``로 시작)는 절대 쓰지 말아야 한다. 같은 Dag 파일이 스케줄러·워커·테스트 등 서로 다른 맥락에서 파싱될 수 있고, 그 맥락에 따라 상대 임포트가 다르게 동작할 수 있기 때문이다. 항상 ``PYTHONPATH``에 추가된 디렉터리를 기준으로 한 전체 패키지 경로(예: ``from my_company.my_custom_dags.base_dag import BaseDag``)를 써야 한다. 또한 파이썬 3의 암묵적 네임스페이스 패키지 개념이 있음에도, Airflow는 모든 패키지 폴더에 (빈 파일이라도) ``__init__.py``가 있기를 기대하므로 반드시 추가해야 한다.
핵심 포인트
- airflow·logging·multiprocessing 같은 일반적/표준 라이브러리 이름을 PYTHONPATH 최상위 패키지·모듈 이름으로 쓰면 원본 패키지 임포트 자체가 막힌다
- plugins 폴더는 이름 충돌 방지 원칙이 임의 깊이까지 확장되며, 실행 중 충돌 파일이 추가되면 다음 재시작 전까지 문제가 드러나지 않아 진단이 어렵다
- .airflowignore는 dags 폴더 루트에만 필요하며 regex(기본) 또는 glob으로 스케줄러가 무시할 파일/폴더를 지정한다
- 상대 임포트는 Dag가 스케줄러·워커·테스트 등 서로 다른 맥락에서 파싱될 때 다르게 동작할 수 있어 금지되며, 항상 전체 패키지 경로를 써야 한다
진단 방법과 패키지로 배포하기
``airflow info`` 명령으로 현재 ``sys.path``에 해당하는 ``Python PATH``를 포함해 OS·도구·경로·설정·프로바이더 정보를 한 번에 확인할 수 있다. ``PYTHONPATH`` 환경 변수로 추가 디렉터리를 지정할 수도 있다 — 예를 들어 ``PYTHONPATH=/home/arch/projects/airflow_operators python``처럼 실행하면 그 디렉터리가 ``sys.path``에 추가되고, 같은 방식으로 ``PYTHONPATH=... airflow info`` 처럼 Airflow 명령에도 적용할 수 있다.
가장 체계적으로 코드를 공유하는 방법은 파이썬 패키지로 만드는 것이다. 이 방식은 버저닝 전략을 통제하고, 어떤 버전의 공유 코드가 설치되어 있는지 관리하며, 통제된 방식으로 모든 인스턴스·컨테이너에 배포할 수 있다는 장점이 있다 — 별도 팀이 이 공유 코드를 관리하는 규모라면 특히 적합하지만, 소규모 배포에서도 파이썬에 익숙하다면 이 방식으로 배포할 수 있다. 대략적인 절차는: PEP 621 호환 빌드 도구(setuptools, poetry, hatch, flit 등) 선택 -> 패키지 디렉터리와 ``__init__.py`` 생성 -> ``pyproject.toml``에 빌드 도구 설정 작성 -> 빌드(예: ``hatch build -t wheel``로 ``dist`` 폴더에 ``.whl`` 생성) -> ``pip install dist/....whl``로 설치. 제거는 ``pip uninstall <패키지명>``으로 한다. Airflow의 plugins나 프로바이더 패키지들도 이런 방식으로 설치되므로, 패키지를 만드는 법을 익혀두면 유용하다.
핵심 포인트
- airflow info 명령은 현재 유효한 sys.path(Python PATH)를 포함한 진단 정보를 한 번에 보여준다
- PYTHONPATH 환경변수는 파이썬 인터프리터뿐 아니라 airflow CLI 명령 실행 시에도 그대로 적용된다
- 공유 코드를 파이썬 패키지로 배포하면 버전 통제와 일관된 배포가 가능해지며, PEP 621 호환 빌드 도구로 wheel을 만들어 pip install/uninstall로 관리한다