본문 바로가기

파이썬 코딩의 기술

파이썬코딩의기술 - BW88 : 순환 의존성을 깨는 방법을 알아두라

(문제제기)

다른 사람들과 협업하다보면 불가피하게 모듈들이 상호 의존하는 경우가 생깁니다.

심지어 혼자 한 프로그램의 여러 부분을 홀로 작업할 때도 발생할 수 있습니다.

 

    - 다음 코드는 전역 선호도 설정에서 디폴트 문서 저장위치를 가져오는 대화창을 정의함

# dialog.py
import app

class Dialog:
	def __init__(self, save_dir):
    	self.save_dir = save_dir
        ...
        save_dialog = Dialog(app.prefs.get('save_dir'))
        
    def show():
    	...

문제는 prefs 객체가 들어있는 app 모듈이 프로그램 시작 시 대화창을 표시하고자 앞에서 정의한

dialog 클래스를 임포트한다는 점 입니다.

 

# app.py
import dialog

class Prefs:
	...
    def get(self, name):
    	...
        
prefs = Prefs()
dialog.show()

--> 이로 인해 순환 의존 관계가 생깁니다.

--> app 모듈을 메인 프로그램에서 임포트하려고 시도하면

# main.py

import app

예외가 발생합니다 ....

 

예외가 발생하는 이유를 알기 위해 파이썬의 임포트 기능이 일반적으로 어떻게 작동하는지 알아야 합니다.

    - 모듈이 임포트 되면 파이썬이 실제로 어떤 일을 하는지를 깊이 우선순위(depth first order)로 나타냄

    - 1. sys.path에서 모듈 위치를 검색함

    - 2. 모듈의 코드를 로딩하고 컴파일 되는지 확인함

    - 3. 임포트할 모듈에 상응하는 빈 모듈 객체를 만듬

    - 4. 모듈을 sys.modules에 넣음

    - 5. 모듈 객체에 있는 코드를 실행해서 모듈의 내용을 정의함

 

(문제) 위 예제에서 app 모듈은 다른 모든 내용을 정의하기 전에 dialog 모듈을 임포트합니다.

    - 그 후 dialog 모듈은 app을 임포트합니다.

    - app이 아직 실행되지 않았기 때문에 app 모듈은 비어있습니다.

    - 따라서, prefs 애트리뷰트를 정의하는 코드가 아직 실행되지 못했기 때문에 AttributeError가 발생합니다.

 

(해결방법) 코드를 리팩터링해서 prefs 데이터 구조를 의존 관계 트리의 맨 밑바닥으로 보내는 것 입니다.

    --> 이렇게 변경하고 나면, app과 dialog가 모두 같은 유틸리티 모듈을 임포트하고 순환 임포트를 피할 수 있음

 

(문제제기) 그러면, 리팩토링하기 어려운 경우는 어떻게 할까?

(해결방법) 순환 임포트를 깨는 다른 방법 3가지가 있습니다.

 

첫번째. 임포트 순서 바꾸기

첫번째 방법은 임포트 순서를 바꾸는 것 입니다.

    - (예) app 모듈의 다른 내용이 모두 실행된 다음, 맨 뒤에서 dialog 모듈을 임포트하면 AttributeError가 사라집니다.

 

# app.py
class Prefs:
    ...
    
prefs = Prefs()
import dialog # 위치 바꿈
dialog.show()

- (제대로 작동하는 이유)  dialog 모듈이 나중에 로딩될 때 dialog 안에서 재귀적으로 임포트한 app에 app.prefs가 이미 정의돼 있기 때문입니다.

    - 그러나, AttributeError를 없애주기는 하지만, PEP 8 스타일 가이드에 위배됨

    - 스타일 가이드는 항상 파이썬 파일의 맨 위에 임포트를 넣으라고 제안함

    - 임포트가 맨 앞에 있어야 여러분이 의존하는 모듈이 여러분 모듈 코드의 모든 영역에서 항상 사용 가능할 것이라 확신할 수 있음

    - (문제가 될 수 있는 사항) 코드 순서를 약간만 바꿔도 전체 모듈이 망가질 수 있음 

 

결론1 : 순환 임포트 문제를 해결하기 위해 임포트 순서를 변경하는 것을 권하지 않습니다.

 

두번째. 임포트, 설정, 실행

두번째 방법은 임포트 시점에 부작용을 최소화한 모듈을 사용하는 것입니다.

 

모듈이 함수, 클래스, 상수만 정의하게 하고, 임포트 시점에 실제로 함수를 전혀 실행하지 않게 만듭니다.

    - 그 후 다른 모듈이 모두 임포트를 끝낸 후, 호출할 수 있는 configure 함수를 제공함

    - configure의 목적은 다른 모듈들의 애트리뷰트에 접근해 모듈 상태를 준비하는 것.

    - 다른 모든 모듈을 임포트한 다음에 configure를 실행하므로 configure가 실행되는 시점에는 항상 모든 애트리뷰트가 정의돼 있음

 

    - (예) 다음 코드는 configure가 호출될 때만 prefs 객체에 접근하도록 dialog 모듈을 재정의합니다.

# dialog.py
import app

class Dialog:
	...
    
save_dialog = Dialog()

def configure():
	save_dialog.save_dir = app.prefs.get('save_dir')

또한, app 모듈도 임포트 시 동작을 수행하지 않게 다시 정의합니다.

 

# app.py
import dialog

class Prefs:
	...
    
prefs = Prefs()

def configure():
	...

마지막으로 main 모듈은 모든 것을 임포트하고, 모든 것을 configure하고, 프로그램의 첫 동작을 실행하는 세 가지 단계를 거칩니다.

# main.py
import app
import dialog
app.configure()
dialog.configure()

dialog.show()

--> 이런 구조는 잘 작동하고 의존관계주입 같은 다른 패턴 적용 가능

- (문제제기) 코드 구조를 변경해서 명시적인 configure 단계를 분리할 수 없을 때도 있음

- (단점) 모듈 안에 서로 단계가 둘 이상 있으면, 객체를 정의하는 부분과 객체를 설정하는 부분이 분리되기 때문에 코드를 읽기가 더 어려워짐 

 

세번째. 동적 임포트

세번째 방법은 import문을 함수나 메서드 안에서 사용하는 것 입니다.

    - 프로그램이 처음 시작하거나 모듈을 초기화하는 시점이 아니라 프로그램이 실행되는 동안 모듈 임포트가 일어나기 때문에 이를 동적 임포트라고 부릅니다.

    - 다음 코드는 동적 임포트를 사용해 dialog 모듈을 재정의함

    - dialog 모듈이 초기화될 때 app을 임포트하는 대신, dialog.show 함수가 실행 시점에 app 모듈을 임포트 합니다.

 

# dialog.py
class Dialog:
    ...
    
    
save_dialog = Dialog()

def show():
    import app # 동적 임포트
    save_dir.save_dir = app.prefs.get('save_dir')
app.prefs.get('save_dir')
	...

이제 app 모듈은 맨 처음 예제 코드와 똑같습니다.

    - app 모듈은 dialog를 맨 위에서 임포트하고 맨 아래에서 dialog.show를 호출함

 

이런 접근 방법은 앞에서 본 임포트, 설정, 실행 단계를 사용하는 방식과 비슷한 효과를 나타냄

 

- (차이점) 동적 임포트 방식에서는 모듈을 정의하고 임포트하는 방 식을 구조적으로 바꾸지 않아도 됩니다.

    - 순환적인 임포트를 실제로 다른 모듈에 접근해야만 하는 시점으로 지연시켰을 뿐.. 다른 모듈이 이미 초기화됨

  

일반적으로 이런 동적인 임포트는 피하는 게 좋습니다.

    - import 문의 비용이 무시못할 만큼 큼

    - 자주 빠르게 반복되는 로프 안에서 임포트를 사용하면 악영향이 커짐

    - 임포트 실행을 미루기 때문에 실행시점에 예기치 못한 오류로 인해 놀랄 수 있음

        - (예) 프로그램이 시작되고 실행된 다음에 한참 있다가 SyntaxError가 발생하는 등의 일이 벌어질 수 있음

 

결론 : 하지만 이런 단점을 감수하는 것이 프로그램 전체 구조를 바꾸는 것보다 더 나은 경우가 많습니다.

 

 

 

### 기억해야할 내용 ###

  • 두 모듈이 임포트 시점에 서로를 호출하면 순환 의존 관계가 생긴다. 순환 의존 관계가 있으면 프로그램이 시작되다가 오류가 발생하면서 중단될 수 있다.
  • 순환 의존 관계를 깨는 가장 좋은 방법상호 의존 관계를 의존 관계 트리의 맨 아래에 위치한 별도의 모듈로 리팩터링하는 것이다.
  • 동적 임포트리팩터링과 복잡도 증가를 최소화하면서 모듈 간의 순환 의존 관계를 깨는 가장 단순한 해법이다.