문제 상황: 순환 종속성이 있는 코드
다음 코드는 두 Python 클래스 사이에 순환 종속성(circular dependency)이 존재하는 경우를 보여줍니다.
class P():
q = Q().do_something()
def do_something(self):
pass
class Q():
p = P().do_something()
def do_something(self):
pass
이 코드를 실행하면 NameError가 발생합니다. 그 이유는 무엇일까요?
왜 이런 문제가 발생할까?
함수와 달리 클래스는 정의되는 시점에 클래스 본문이 즉시 실행됩니다. 따라서 클래스 P를 정의하려면 Q의 메서드를 실제로 호출해야 하는데, 아직 클래스 Q가 정의되기 전이기 때문에 이 호출이 불가능합니다. 반대 방향(Q가 P를 참조)에서도 같은 문제가 발생합니다.
즉, 클래스 수준의 구문(q = Q.do_something())은 인스턴스를 생성할 때가 아니라 클래스를 정의하는 순간 실행되기 때문에, 서로를 참조하는 두 클래스는 어느 쪽을 먼저 정의해도 오류가 날 수밖에 없습니다.
해결 방법: 클래스 정의 후 속성 추가하기
이 문제를 우회하는 가장 간단한 방법은, 먼저 두 클래스의 뼈대만 정의한 뒤 나중에 속성을 추가하는 것입니다.
class P(object):
pass
class Q(object):
pass
P.q = Q.do_something()
Q.p = P.do_something()
두 클래스가 모두 정의된 상태에서 속성을 동적으로 할당하므로, 순환 참조 문제 없이 코드가 정상적으로 동작합니다.
이 방법의 한계
그러나 만약 각 do_something() 호출이 상대방의 호출이 이미 실행된 결과에 의존한다면, 위 방법만으로는 충분하지 않습니다. 이 경우에는 다음과 같은 추가적인 접근이 필요합니다.
- 지연 초기화(Lazy Initialization): 속성을 클래스 수준이 아니라 메서드나 생성자(
__init__) 안에서 필요한 시점에 할당합니다. - 참조 분리: 한쪽 클래스가 다른 쪽을 직접 참조하지 않도록 공통 로직을 제3의 모듈이나 클래스로 분리합니다.
마무리
두 클래스가 모두 정의된 이후에 클래스의 정의를 완성할 수 있다는 점은 Python이 동적 언어(dynamic language)라는 사실을 잘 보여주는 예입니다. 클래스 몸체가 정의 시점에 실행된다는 특성을 이해하고 있으면, 이런 순환 종속성 문제를 훨씬 쉽게 진단하고 해결할 수 있습니다.