본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 대규모 코드베이스에서 Import 순환 참조 해결 전략 5가지와 구조적 차이점

by Papa Martino V 2026. 4. 3.
728x90

순환 참조(Circular Import)
순환 참조(Circular Import)

 

파이썬 프로젝트의 규모가 커지고 모듈 간의 관계가 복잡해지면 반드시 마주하게 되는 고질적인 문제가 있습니다. 바로 순환 참조(Circular Import)입니다. 두 개 이상의 모듈이 서로를 참조하면서 인터프리터가 모듈의 초기화 순서를 결정하지 못해 발생하는 ImportError 또는 AttributeError는 개발자의 생산성을 크게 떨어뜨리는 주범입니다. 오늘 이 글에서는 대규모 코드베이스에서 발생하는 순환 참조를 근본적으로 차단하는 5가지 전문적인 해결 전략과 아키텍처 관점에서의 구조적 차이를 심도 있게 다룹니다.


1. 순환 참조의 본질과 파이썬의 모듈 로딩 메커니즘

파이썬은 모듈을 처음 불러올 때 sys.modules라는 캐시에 등록합니다. 만약 모듈 A를 로드하는 도중에 모듈 B를 불러오고, 다시 모듈 B가 아직 초기화가 끝나지 않은 모듈 A를 참조하려고 하면 문제가 발생합니다. 대규모 프로젝트에서는 수백 개의 파일이 얽혀 있어 눈에 보이지 않는 순환 고리가 형성되곤 합니다. 이는 단순한 코딩 실수가 아니라 객체 지향 설계의 결합도(Coupling) 문제로 접근해야 합니다.


2. 순환 참조 해결을 위한 주요 전략 비교

각 해결 방법은 코드의 가독성, 성능, 유지보수 측면에서 뚜렷한 차이를 보입니다. 상황에 맞는 적절한 해결책을 선택하는 것이 중요합니다.

해결 전략 핵심 개념 장점 구조적 차이 및 영향
로컬 임포트 (Local Import) 함수나 메서드 내부에서 import 수행 가장 빠르고 간단한 해결책 런타임 오버헤드 발생 가능, 정적 분석 어려움
아키텍처 재설계 공통 의존성을 제3의 모듈로 분리 근본적인 결합도 해결 프로젝트 구조 변경 필요, 장기적 안정성 향상
TYPE_CHECKING 활용 타입 힌트용 import만 별도 분리 런타임 순환 참조 방지 타입 체크 모드와 실행 모드의 로직 격리
추상화 인터페이스 사용 구체 클래스 대신 인터페이스 의존 의존성 역전 원칙(DIP) 준수 객체 간의 직접 참조 제거, 결합도 최소화

3. 실전 전략: TYPE_CHECKING과 지연 로딩의 조화

현대적인 파이썬 개발(3.10+)에서 가장 권장되는 해결 방법은 typing.TYPE_CHECKING 상수를 사용하는 것입니다. 이는 정적 타입 분석 도구(Mypy 등)가 실행될 때는 True이지만, 실제 파이썬 인터프리터가 실행될 때는 False로 평가됩니다.

핵심 해결 프로세스

  • 런타임에 필요하지 않은 타입 힌트용 모듈은 if TYPE_CHECKING: 블록 안에 배치합니다.
  • 실행 시점에 필요한 참조는 함수 내부에서 로컬 임포트하여 지연 로딩(Lazy Loading)을 유도합니다.
  • 필요하다면 문자열 기반의 타입 힌트(Forward Reference)를 사용하여 참조 에러를 회피합니다.

4. Sample Example: 순환 참조 에러와 해결 코드

다음은 전형적인 순환 참조 발생 케이스와 이를 전문적으로 해결한 코드의 차이입니다.

[Case] 순환 참조 발생 (에러 유발)


# user.py
from order import Order  # Error!

class User:
    def add_order(self, order: Order):
        pass

# order.py
from user import User  # Error!

class Order:
    def set_owner(self, owner: User):
        pass

[Solution] 전문가의 해결 전략 (TYPE_CHECKING 활용)


# user.py
from typing import TYPE_CHECKING, List

if TYPE_CHECKING:
    # 런타임에는 무시되고 타입 체크시에만 동작
    from order import Order

class User:
    def __init__(self, name: str):
        self.name = name
        self.orders: List["Order"] = [] # 문자열 포워드 참조

    def add_order(self, order_id: int):
        # 실제 객체가 필요한 시점에 로컬 임포트
        from order import Order
        new_order = Order(order_id, self)
        self.orders.append(new_order)

5. 아키텍처 관점에서의 해결: 의존성 주입(DI)

순환 참조가 빈번하다면 모듈이 서로의 내부 구현을 너무 깊게 알고 있다는 신호입니다. 이를 해결하기 위해 의존성 주입(Dependency Injection) 패턴을 도입하십시오. 모듈 A가 모듈 B를 직접 import하는 대신, 실행 시점에 외부에서 모듈 B의 인스턴스를 주입받도록 설계하면 import 레벨에서의 결합도를 완벽히 제거할 수 있습니다.

6. 결론: 지속 가능한 코드베이스 관리

순환 참조 해결의 핵심은 단순히 에러를 없애는 것이 아니라 모듈 간의 단방향 의존성 흐름을 만드는 것입니다. 로컬 임포트는 임시방편일 뿐이며, 대규모 프로젝트일수록 TYPE_CHECKING을 통한 타입 분리와 인터페이스 기반의 재설계를 최우선으로 고려해야 합니다. 이러한 노력이 모여 견고하고 확장 가능한 파이썬 애플리케이션이 완성됩니다.


내용 출처 및 참고 문헌

  • Python Documentation: "Import System and sys.modules" (2026)
  • Guido van Rossum: "The History of Python Import Systems"
  • Real Python: "Circular Imports in Python: Common Pitfalls and Solutions"
728x90