개발언어/PYTHON

[Python] Circular import(순환참조)란?

to,min 2024. 11. 8. 17:40

근래 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 으로 정의할때 양방향 연관관계 등에서 쉽게 마주칠 수 있는 에러니,

설계 간 유의하며 진행하면 좋을 것 같다.