순환 의존성을 깨는 방법을 알아두라

매일 공부(ML)·2022년 10월 21일
0

파이썬 코딩의 기술

목록 보기
25/27

순환 의존성을 깨는 방법을 알아두라

다른 사람들과 협업하다 보면 불가피하게 모듈들이 상호 의존하는 경우가 생기고 혼자 작업을 할 때도 발생할 수 있다

#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 모듈이 프로그램 시작 시 대화창을 표시하고자 앞에서 정의한 dailog 클래스를 임포트한다는 점이다.
"""


#app.py

import dialog

class Prefs:
	...
    def get(self, name):
    	...

prefs = Prefs()
dialog.show()

#순환 의존 관계가 생겨서 app모듈을 메인 프로그램에서 임포트 시도

import app

"""
$ python3 main.py
Traceback(most recent call last):
	File ".../main.py", line 17, in <module>
    	import app
    File ".../app.py", line 17, in <module>
    	import dialog
    File ".../dialog.py", line 23, in <module>
    	save_dialog = Dialog(app.prefs.get('save_dir'))
AttributeError: partially initialized module 'app' has no attribute 'prefs' (most likely due to a circular import)
"""

파이썬 임포트 기능의 작동 원리

모듈이 임포트될 때 깊이 우선순위

  1. sys.path에서 모듈 위치를 검색한다.
  2. 모듈의 코드를 로딩하고 컴파일되는지 확인한다.
  3. 임포트할 모듈에 상응하는 빈 모듈 객체를 만든다.
  4. 모듈을 sys.modules에 넣는다.
  5. 모듈 객체에 있는 코드를 실행해서 모듈의 내용을 정의한다.

순환의 문제는 코드 리팩터링을 하면 해결이 되지만 그것이 쉽지 않다.


코드리팩토링은 어려워!! 다른 방법

첫 번째 접근: 임포트 순서를 바꾸는 것이다.

#app 모듈의 다른 내용이 모두 실행된 다음,맨 뒤에서 dialog 모듈을 임포트하면 AttributeError가 사라진다.

#app.py

class Prefs:
	...
prefs = Prefs()

import dialog # 위치 바뀜
dialog.show()

"""
이런 코드가 제대로 작동하는 이유는 dialog 모듈이 나중에 로딩될 때 dialog안에사 재귀적으로 임포트한 app에 app.prefs가 이미 정의돼 있기 때문이다.

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

파일의 뒷부분에 임포트를 넣으면 깨지기 쉽고, 코드 순서를 약간만 바꿔도 전체 모듈이 망가질 수 있다.

순환 임포트 문제를 해결하기 위해선 임포트 순서를 변경하는 것은 권하지 않는다.
"""

두 번째 접근: 임포트, 설정, 실행

임포트 시점에 부작용을 최소화한 모듈을 사용하는 것이고, 모듈이 함수, 클래스, 상수만 정의하게 하고, 임포트 시점에 실제로 함수를 전혀 실행하지 않게 만든다.

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

configure의 목적

  • 다른 모든 모듈을 임포트한 다음에 configure를 실행하여 configure가 실행되는 시점에는 항상 모든 애트리뷰트가 정의돼 있다.
#configure가 호출될 때만 prefs 객체에 접근하도록 dialog 모듈을 재정의한다.

#dialog.py

import app

class Dialog:
	...
 
save_dialog = Dialog()

def show():
	...

def configure():
	save_dialog.save_dir = app.prefs.get('save_dir')
    
#app 모듈도 임포트 시 동작을 수행하지 않게 다시 정의한다.

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_dialog.save_dir = app.prefs.get('save_dir')
    ...
    
#app.py
import dialog

class Prefs:
	...
    
prefs = Prefs()
dialog.show()

임포트,설정, 실행 단계를 사용하는 방식과 비슷한 효과를 나타낸다. 차이가 있다면, 동적 임포트 방식에서는 모듈을 정의하고 임포트하는 방식을 구조적으로 바꾸지 않아도 된다.


Summary

  • 두 모듈이 임포트 시점에 서로를 호출하면 순환 의존 관계가 생긴다.

  • 순환 의존 관계가 있으면 프로그램이 시작되다가 오류가 발생하면서 중단될 수 있다.

  • 순환 의존 관계를 깨는 가장 좋은 방법은 상호 의존 관계 트리의 맨 아래에 위치한 별도의 모듈로 리팩터링하는 것이다.

  • 동적 임포트는 리팩터링과 복잡도 증가를 최소화하면서 모듈 간의 순환 의존 관계를 깨는 가장 단순한 해법이다.

profile
성장을 도울 아카이빙 블로그

0개의 댓글