파이썬/Basic

순환 참조(Circular Import) 이해하기와 해결 방법

코샵 2024. 12. 6. 10:45
반응형

소개

Python 개발을 하다 보면 "most likely due to a circular import" 라는 에러를 자주 만나게 됩니다. 이는 모듈 간 순환 참조로 인해 발생하는 문제입니다. 이번 글에서는 순환 참조가 무엇인지, 어떤 문제를 일으키는지, 그리고 해결 방법에 대해 알아보겠습니다.

순환 참조란?

순환 참조는 두 개 이상의 모듈이 서로를 import할 때 발생합니다.

문제가 되는 코드 예시

# user.py
from post import Post

class User:
    def __init__(self, name):
        self.name = name
        self.posts = []

    def create_post(self, content):
        post = Post(content, self)
        self.posts.append(post)
        return post

# post.py
from user import User

class Post:
    def __init__(self, content, author: User):
        self.content = content
        self.author = author

순환 참조가 발생하는 일반적인 상황

1. 모델 간 상호 참조

# models/user.py
from models.team import Team

class User:
    def __init__(self):
        self.team: Team = None

# models/team.py
from models.user import User

class Team:
    def __init__(self):
        self.members: List[User] = []

2. 서비스 레이어 간 상호 의존

# services/user_service.py
from services.notification_service import NotificationService

class UserService:
    def __init__(self):
        self.notification_service = NotificationService()

# services/notification_service.py
from services.user_service import UserService

class NotificationService:
    def __init__(self):
        self.user_service = UserService()

해결 방법

1. 지연 임포트 (Lazy Import)

class User:
    def create_post(self, content):
        # 필요한 시점에 임포트
        from post import Post
        post = Post(content, self)
        self.posts.append(post)
        return post

2. 의존성 주입 (Dependency Injection)

# user.py
class User:
    def __init__(self, name):
        self.name = name
        self.posts = []

    def create_post(self, post_class, content):
        post = post_class(content, self)
        self.posts.append(post)
        return post

# post.py
class Post:
    def __init__(self, content, author):
        self.content = content
        self.author = author

# usage
from user import User
from post import Post

user = User("John")
post = user.create_post(Post, "Hello World")

3. 중간 레이어 도입

# models.py (중간 레이어)
class UserModel:
    name: str
    posts: list

class PostModel:
    content: str
    author_id: int

# user.py
from models import PostModel

class User:
    def create_post(self, content):
        return PostModel(content=content, author_id=self.id)

# post.py
from models import UserModel

class Post:
    def get_author(self):
        return UserModel.get(self.author_id)

4. Type Hints에서 문자열 사용

from typing import List, TYPE_CHECKING

if TYPE_CHECKING:
    from post import Post
    from user import User

class User:
    posts: List['Post']  # 문자열로 타입 힌트 제공

class Post:
    author: 'User'  # 문자열로 타입 힌트 제공

5. 공통 인터페이스 사용

# interfaces.py
from abc import ABC, abstractmethod

class UserInterface(ABC):
    @abstractmethod
    def create_post(self, content): pass

class PostInterface(ABC):
    @abstractmethod
    def get_author(self): pass

# user.py
from interfaces import UserInterface, PostInterface

class User(UserInterface):
    def create_post(self, content):
        # 구현

# post.py
from interfaces import PostInterface

class Post(PostInterface):
    def get_author(self):
        # 구현

각 해결 방법의 장단점

해결 방법 장점 단점
지연 임포트 - 구현이 간단함
- 기존 코드 수정이 최소화됨
- 성능 오버헤드 발생 가능
- 코드 가독성 저하
의존성 주입 - 느슨한 결합
- 테스트 용이성
- 추가 코드 작성 필요
- 복잡도 증가
중간 레이어 - 명확한 구조
- 모듈간 의존성 감소
- 추가 코드 필요
- 오버엔지니어링 가능성
문자열 타입 힌트 - 타입 힌트 유지
- 최소한의 코드 변경
- IDE 지원 제한적
- 런타임 타입 체크 불가
공통 인터페이스 - 명확한 계약
- 유지보수성 향상
- 많은 보일러플레이트
- 복잡도 증가

권장하는 접근 방법

  1. 먼저 아키텍처 설계 단계에서 순환 참조를 피하도록 구조 검토
  2. 작은 규모의 순환 참조는 지연 임포트로 해결
  3. 큰 규모의 애플리케이션에서는 의존성 주입 또는 중간 레이어 도입 고려
  4. 타입 힌트가 중요한 경우 문자열 타입 힌트 사용
  5. 확장성이 중요한 경우 인터페이스 기반 설계 고려

마치며

순환 참조는 피할 수 있다면 피하는 것이 좋지만, 때로는 불가피한 경우가 있습니다. 이런 경우 프로젝트의 규모와 요구사항에 맞는 적절한 해결 방법을 선택하는 것이 중요합니다. 특히 큰 규모의 프로젝트에서는 순환 참조 문제를 해결하기 위해 아키텍처 수준의 결정이 필요할 수 있습니다.