파이썬의 import 시스템은 코드를 실행하기 전에 sys.modules에 부분적으로 초기화된 모듈을 즉시 캐싱하여 순환 의존성을 해결합니다. 이 메커니즘은 모듈 A가 B를 가져오고 B가 동시에 A를 가져올 때 무한 재귀를 방지하지만, 속성이 접근할 수 없는 창을 만들기도 합니다.
근본적인 문제는 파이썬의 실행 모델에서 발생하는데, 이는 import 시에 모듈 네임스페이스를 순차적으로 채우기 때문입니다. 두 개의 모듈을 고려해보면 module_a.py에는 import module_b가 포함되어 있고 그 뒤에 def func(): pass가 있으며, module_b.py는 module_a.func()를 호출하려고 시도합니다. 이 경우 속성 검색이 실패하는데, 그 이유는 module_a가 sys.modules에 존재하지만 func가 아직 바인딩되지 않았기 때문입니다.
# module_a.py import module_b # 여기서 실행이 일시 중지되며, A는 비어 있는 상태로 캐시됩니다. def important_function(): return "critical data" # module_b.py import module_a # AttributeError 발생: module 'module_a'에 중요한 속성이 없습니다. result = module_a.important_function()
이 문제를 해결하기 위해서는 순환을 제거하거나 지연 평가 패턴을 사용하여 구조를 변경해야 합니다. 개발자는 import 문을 함수 정의 내부로 이동시키거나 importlib를 사용하여 동적으로 import 하거나, 두 당사자가 모두 가져오는 제3의 모듈로 공유 의존성을 리팩토링할 수 있습니다.
우리의 FastAPI 마이크로서비스는 database.py (연결 풀 포함)와 models.py (SQLAlchemy ORM 클래스 정의) 간의 순환 import로 고통받았습니다. 데이터베이스 모듈은 초기 스키마 설정을 실행하기 위해 모델을 가져왔고, 모델은 테이블 생성을 위해 데이터베이스에서 엔진을 가져오는 방식으로 서로 의존하고 있었으며, 이로 인해 애플리케이션 시작 시 ImportError가 발생하여 배포를 방해했습니다.
우리는 세 가지 해결책을 평가했습니다. import 문을 create_tables() 함수 내부로 이동시키는 것이 즉각적인 오류를 해결했지만, 실행 시간 동안 import 논리를 다시 실행해야 하므로 성능 오버헤드가 발생하고 의존성을 숨기므로 코드 가독성이 줄어들었습니다. 추상 기본 클래스를 포함하는 interfaces.py 모듈을 만드는 것이 의존성 전환을 통해 순환을 끊었지만, 이는 상당한 리팩토링이 필요했으며 작은 서비스의 간접성 복잡성을 증가시켰습니다. 파이선의 typing.Protocol을 사용한 의존성 주입 컨테이너 구현은 두 모듈이 로드된 후 데이터베이스 엔진을 등록하게 하여 실제 연결 설정을 애플리케이션 시작 시점까지 연기할 수 있게 해주었습니다.
우리는 성능을 희생하지 않으면서 깔끔한 아키텍처 원칙을 유지하기 때문에 의존성 주입 접근 방식을 선택했습니다. 이 솔루션은 모든 모듈이 초기화된 후 데이터베이스 세션을 라우트 핸들러에 주입하기 위해 FastAPI의 Depends() 메커니즘을 사용하여 순환 의존성을 제거하는 동시에 테스트 가능성을 개선했습니다. 이는 시작 실패를 100% 줄이고 통합 테스트 설정 시간을 60% 단축했습니다.
왜 if __name__ == "__main__"이 모듈 수준에서 순환 import 오류를 방지하지 못하는가?
이 방어 구문은 메인 스크립트 컨텍스트 내에서 코드 실행만을 제어하며, import 메커니즘 자체는 제어하지 않습니다. 파이썬이 import module를 만나면, __name__ 검사가 있더라도 전체 모듈 파일을 즉시 로드하고 완료까지 실행합니다. 순환 import 오류는 이 로딩 단계에서 발생하며, 특정적으로 인터프리터가 부분적으로 구성된 네임스페이스에서 기호를 해결하려고 시도할 때 발생합니다. 이는 방어 구문이 실행되거나 실패를 완화할 기회를 가지지 못함을 의미합니다.
from module import name이 순환 의존성을 해결할 때 import module과 어떻게 다른가?
from 문은 모듈 객체가 sys.modules에서 검색된 후에 즉시 속성 조회를 수행하지만, 모듈이 실행을 끝내기 전에 발생할 수 있습니다. import module을 사용할 경우, 인터프리터는 모듈 객체 자체에 대한 참조를 반환하므로 순환 import 체인이 완료될 때까지 속성 접근을 연기할 수 있습니다. 이 구별은 import module 이후에 module.name에 접근하는 것이 성공하고, from module import name이 실패하는 이유를 설명합니다. 점 표기법은 접근 시점에 네임스페이스를 다시 평가하므로 초기 import 중 이름을 바인딩하지 않습니다.
Python 3.3+에서 네임스페이스 패키지 및 그에 따른 순환 import 해결에 대한 변화는 무엇인가?
PEP 420은 __init__.py 파일이 없는 암묵적 네임스페이스 패키지를 소개하여, import 시 모듈 객체를 구성하는 방식에 변화를 주었습니다. 전통적인 패키지는 초기화 경계를 제공하기 위해 즉시 __init__.py 코드를 실행하는 반면, 네임스페이스 패키지는 경로 항목마다 다른 로딩 시퀀스를 트리거할 수 있습니다. 후보자들은 네임스페이스 패키지를 포함하는 순환 import가 동일한 import 문에도 불구하고 서로 다른 모듈 인스턴스를 받을 수 있다는 점을 자주 간과합니다.