Python 순환참조 (Circular Import) 원인과 해결 방법

렁호·2023년 3월 9일
2
post-thumbnail

0. 개요

파이썬 프로그램을 개발하다 보면 한 번쯤 순환 참조 상황을 맞닥뜨리게 된다.
발생하기 전엔 예측하기도 어려운 이 현상을 어떻게 예방/해결 하는짖 왜 일어나는지에 대해 알아보자.

1. 순환참조 발생 원인

순환참조는 서로가 서로를 참조하는 상황 안에서 특정 조건일 때 발생 할 수 있다.

순환 참조가 발생할만한 상황을 만들어 보고 원인을 분석해본다.

script_A.py

import script_B

def func_a():
	print(script_B.x)
print('script A code run')

script_B.py

import script_A
x = 10
def func_b():
    script_A.func_a()

func_b()

script_A 실행

script A code run
10
script A code run

script_B 실행

  File "/Users/yoonjungho/study/script_B.py", line 1, in <module>
    import script_A
  File "/Users/yoonjungho/study/script_A.py", line 1, in <module>
    import script_B
  File "/Users/yoonjungho/study/script_B.py", line 7, in <module>
    func_b()
  File "/Users/yoonjungho/study/script_B.py", line 5, in func_b
    script_A.func_a()
AttributeError: partially initialized module 'script_A' has no attribute 'func_a' (most likely due to a circular import)

위와 같은 결과가 나온 이유는 python이 모듈을 import 하는 과정을 알아야 이해가 쉽다.
복잡한 작업이 있겠지만 핵심적인 과정만으로 단순하고 나열하면 다음의 순서와 같다.
밑의 과정을 참고하여 위의 예시에서 어떻게 에러가 발생 했는지를 분석해보자.

  1. 모듈 탐색
  2. 탐색한 모듈의 코드 컴파일
  3. import 한 모듈의 기능을 할 빈 모듈 객체 생성
  4. import 한 모듈의 코드를 실행하며 모듈 객체 정의

위의 예시에서 script_B를 실행한다면 1단계로 모듈 탐색이 시작되고 4번 단계에 진입하며 script_A의 코드를 실행한다.
그리고 scrip_A는 import script_B 코드가 실행되며 모듈 객체를 만들기 위해 script_B 모듈의 코드를 실행하게 된다.
문제는 여기서 발생한다. script_B에서 script_A의 코드는 순환 참조 되어 한 줄도 실행되지 않았음으로 빈 모듈 객체(3단계 이후 4단계 이전 상태)로 있는데 모듈의 함수를 호출하니 AttributeError 에러가 발생하는 것이다.

2. 예방과 해결 방법

예방

사실 개요에서도 잠깐 언급했듯이 복잡하고 비대해진 서비스를 개발하면서 순환참조가 발생할 모든 상황을 예측하면서 개발하기는 쉽지 않으며 발생하고 나서야 인지하게 되는게 다반사이다.

그래도 모든 상황을 예측하고 미리 준비 할 수는 없더라도 발생 확률은 줄일 수가 있다. 감기에 안 걸릴 순 없지만 손을 잘 닦음으로서 확률을 줄일 수 있듯이 말이다.

우선 순환참조가 발생하는 원인의 원인이 무엇인가를 생각해봐야 한다.
위에 언급한 원인은 특정 상황에 발생할 수 있는 시스템적인 원인에 대한 분석이었고 그보다 앞서 왜 그런 상황 (서로가 서로를 참조하는 상황)이 생겼는지 분석하고 막는 것이 효율적인 방법일 것이다.

내가 생각하는 대부분의 원인은 모듈 계층 간 구분이 뚜렷하지 않거나 모듈의 기능별 조직화가 제대로 이루어지지 않아서라고 생각한다.

설계 단계에서 모듈의 상하위 계층에 대해서 확실히 설계를 하고, 서로 관련된 기능들을 모듈화 할 때 기능들 간의 관계에 집중하여 설계한다면 비교적 순환참조의 상황이 줄어들 것이다.

해결 방법

문제 발생 상황을 모두 차단하여 문제를 예방 한다면 베스트이지만 현실적으로 너무나도 어려운 일이기 때문에 간단하게 해결 할 수 있는 방법을 알아보자.

우서로가 서로를 참조하는 상황을 피할 수 있다면 제일 좋겠지만 그럴 수 없을 땐 import time 에 모듈을 import 하는 것이 아니라 runtime에 import 하도록 하면 해결된다.

script_A.py

def func_a():
	import script_B
	print(script_B.x)
print('script A code run')

위와 같이 runtime에 코드가 실행 되면서 하게 되면 문제가 간단하게 해결 된다.

추가로 django를 사용하면서 순환 참조가 발생하는 경우가 종종 있다.

첫번째로 서로를 외래키로 설정할 때이다.
이때 각 models.py 에서 서로의 모델을 참조하기 위해 import 하다보니 보통 문제가 발생하는데 이럴 땐 그냥 문자열로 'app.model' 의 형태로 인자를 넘기면 간단하게 해결 된다.

두번째로 모델과 모듈이 서로를 참조할 경우이다.
이럴 경우 django 내장 함수인 get_model 함수를 통해 해결 가능하다.
get_model 함수는 첫번째 인자로 app 이름을 받고 두번째 인자로 model class 이름을 받아 모델 클래스를 반환한다.

# 순환 참조 발생 !
Model.objects.get(id=1)

---
# 해결
from django.apps import apps

model = apps.get_model('app_name', 'Model')
Model.objects.get(id=1)
profile
식욕, 수면욕, 성욕 만땅

0개의 댓글