근래 Circular import에 대한 질문을 받았다.
나름 답변을 했다고 생각했지만, 되짚어보니 Circular import 가 아니라 다중 상속 개념을 떠올리고 대답했다.......
이번 게시물에선, Circular import가 왜 발생하는지 짚어보고, 실제로 발생할 만한 케이스를 예시로 보며 익혀보려 한다.
# a.py
from b import bb
def aa():
bb()
print("a")
aa()
# b.py
from a import aa
def bb():
print("b")
python a.py
위 코드를 실행하면 아래와 같은 에러를 마주할 것이다.
ImportError: cannot import name 'bb' from partially initialized module 'b' (most likely due to a circular import)
고전적인 예시이다.
직관적으로 보면 a.py 에서 b를 import 하고 있고, b.py 에서 a를 import 하고 있다.
사실 척봐도 안될 것 같은 코드이다.
그래서 Circular import 가 뭔데?
모듈이나 객체가 생성될 때, 서로 참조하는 것들끼리 동시에 초기화하려 하니 충돌이 발생하는 것이다.
조금 더 보태자면, Python이 처음 실행되며 Import 하는 시점이 있고, 코드가 실행하는 runtime 시점이 있는데,
초기 Import 에서 충돌이 되는 것이다.
해결방식
내 생각엔 솔직히 저런 구조를 안 만드는 것이 1순위라고 본다.
하지만 여차저차 사정으로 저런 구조가 된다면, 여러 해결 방안이 있는데
위에서 실행 시점을 말했었는데, 단순히 시점을 바꾼다면 해결 가능하다.
# a.py
from b import bb
def aa():
bb()
print("a")
aa()
# b.py
def bb():
from a import aa # runtime 영역으로 옮김
print("b")
python a.py
위 코드와 같이 실행 시점을 옮기면 가능하다.
이처럼 실행 시점을 옮기는 방법은 여러개가 있는데, importlib 등 라이브러리를 사용할 수 있지만
내 생각엔 다시 한번 강조하지만, 저런 구조를 안만드는게 낫다고 본다.
마주칠 수 있는 Circular import 예시
아래 코드는 간단한 Flask Backend 서버 구조이다.
세 개 파일로 간단히 이뤄져 있는데도 문제가 발생하고, 유의깊게 보지 않으면 프로젝트 규모가 커질 수록 문제가 잘 보이지 않는다.
아래 코드는 각 모듈이 필요한 시점에 생성되지 않고, 동시에 초기화되어 Circular import 가 발생하는 코드 구조이다.
app에서 db 생성 및 router import -> router에서 model import -> model에서 db import 이렇게 순환되는 구조이다 .
# model.py
from app import db # app 모듈에서 db를 불러옴
from flask_sqlalchemy import SQLAlchemy
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
# router.py
from flask import Blueprint
from model import User # models.py에서 User를 불러옴
bp = Blueprint('main', __name__)
@bp.route('/')
def home():
user = User.query.first()
return f"Hello, {user.name}!"
# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from router import bp # routes를 불러옴
app = Flask(__name__)
db = SQLAlchemy(app)
app.register_blueprint(bp)
# 앱 실행 부분
if __name__ == '__main__':
app.run()
개선 로직
팩토리 패턴과, 역할 분리를 적용하여 예시 코드를 작성해 봤다. 물론 최선의 로직은 아니고, 접근하는 방식만 참고하면 될 것 같다.
app.py 에선 팩토리 패턴을 적용하여 app 인스턴스를 만들어 활용했고,
router 에선 바로 db에 접근하지 않고 service 레이어를 추가하여 접근하는 방식을 택했다.
#conn.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
# models.py
from flask_sqlalchemy import SQLAlchemy
from conn import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
# service.py
from model import User # models.py에서 User를 불러옴
def getUser():
user = User.query.first()
return user
# routes.py
from flask import Blueprint
from service import getUser
bp = Blueprint('main', __name__)
@bp.route('/')
def home():
user = getUser()
return f"Hello, {user.name}!"
# app.py
from flask import Flask
from router import bp
from conn import db
def create_app():
app = Flask(__name__)
app.register_blueprint(bp)
# db.init(app)
return app
app = create_app()
# 앱 실행 부분
if __name__ == '__main__':
app.run()
이 예시 외에, DB 모델을 ORM 으로 정의할때 양방향 연관관계 등에서 쉽게 마주칠 수 있는 에러니,
설계 간 유의하며 진행하면 좋을 것 같다.
'개발언어 > PYTHON' 카테고리의 다른 글
Jupyter 홈 디렉터리 변경 - 25.02 (0) | 2025.02.19 |
---|---|
[Python] 리스트의 원소 추가 방법 및 관련 문제 (0) | 2024.11.22 |
[Python] 팩토리 메서드 패턴이란? (0) | 2024.11.10 |
[Python] Garbage collector(가비지 컬렉터)란 ? (1) | 2024.11.09 |