'ImportError: cannot import name ... (most likely due to a circular import)' 에러 Troubleshooting

김동욱·2024년 7월 18일
0

Troubleshooting

목록 보기
12/14
post-thumbnail

상황

고객 예약 생성과 같이 복잡한 로직을 구현 중에 템플릿 메소드 패턴과 전략 패턴 등의 디자인 패턴을 적용하고 있었다. 전략 패턴을 위해 구현한 추상 클래스에서 예약 관련 비즈니스 로직을 구현한 클래스와 서로 import를 하도록 구현하여 발생한 문제였다. 로직이 길고 복잡하다보니 이러한 부분을 생각하지 못했다. 아래 코드를 보면 보이다시피 서로 다른 모듈이 서로를 import하고 있다.

[abstracts.py]
from mung_manager.reservations.services.reservations import ReservationService
...


class AbstractReservationService(ABC):

    ...

    @abstractmethod
    def register_reservation(self, customer: Customer, pet_kindergarden: PetKindergarden, reservation_data: dict) -> None:
        raise NotImplementedException()


class AbstractReservationStrategy(ABC):

    def __init__(
            self,
            customer_pet_selector: CustomerPetSelector,
            reservation_service: ReservationService,
    ):
        self._customer_pet_selector = customer_pet_selector
        self._reservation_service = reservation_service

    def validate(self, customer: Customer, pet_kindergarden: PetKindergarden, reservation_data: dict) -> None:
        # 공통 검증 로직
        self.common_validation(customer, pet_kindergarden, reservation_data)
        # 개별 검증 로직
        self.specific_validation(customer, pet_kindergarden, reservation_data)

    @abstractmethod
    def specific_validation(self, customer: Customer, pet_kindergarden: PetKindergarden, reservation_data: dict) -> None:
        raise NotImplementedException()

    @abstractmethod
    def reserve(self, reservation_data: dict) -> None:
        raise NotImplementedException()

    def common_validation(self, customer: Customer, pet_kindergarden: PetKindergarden, reservation_data: dict) -> None:
        # 해당 반려동물이 해당 고객에게 속해있는지 검증
        check_object_or_not_found(
            self._customer_pet_selector.exists_by_customer_and_pet_id(
                customer=customer,
                pet_id=reservation_data["pet_id"]
            ),
            msg=SYSTEM_CODE.message("NOT_FOUND_PET"),
            code=SYSTEM_CODE.code("NOT_FOUND_PET"),
        )

        # 등원 날짜가 예약할 수 있는 날짜인지 검증
        if reservation_data["reserved_date"].strftime('%Y-%m-%d') not in self._reservation_service.get_available_reservation_dates(
            pet_kindergarden_id=pet_kindergarden.id,
            customer=customer,
            ticket_type=reservation_data["ticket_type"],
            ticket_id=reservation_data["ticket_id"],
        ):
            raise NotFoundException(
                detail=SYSTEM_CODE.message("INVALID_RESERVED_AT"),
                code=SYSTEM_CODE.code("INVALID_RESERVED_AT"),
            )


[reservations.py]
from mung_manager.reservations.services.abstracts import AbstractReservationService, AbstractReservationStrategy
...


class ReservationService(AbstractReservationService):
    """
    이 클래스는 예약과 관련된 비즈니스 로직을 담당합니다.
    """

    def __init__(
            self,
            reservation_selector: ReservationSelector,
            customer_ticket_usage_log_selector: CustomerTicketUsageLogSelector,
            daily_reservation_selector: DailyReservationSelector,
            customer_ticket_selector: CustomerTicketSelector,
            day_off_selector: DayOffSelector,
            pet_kindergarden_selector: PetKindergardenSelector,

    ):
        self._reservation_selector = reservation_selector
        self._daily_reservation_selector = daily_reservation_selector
        self._customer_ticket_usage_log_selector = customer_ticket_usage_log_selector
        self._customer_ticket_selector = customer_ticket_selector
        self._day_off_selector = day_off_selector
        self._pet_kindergarden_selector = pet_kindergarden_selector
        self._strategy = None

    ...

    @transaction.atomic
    def register_reservation(self, customer: Customer, pet_kindergarden: PetKindergarden, reservation_data: dict) -> None:  # 반환값 뭐로 할지 고민하자
        """
        이 함수는 티켓의 유형에 맞게 값을 검증 후 예약을 생성합니다.

        Args:
            customer (Customer): 고객 객체
            pet_kindergarden (PetKindergarden): 유치원 객체
            reservation_data (dict): 예약 관련 데이터로, 필드에는 다음의 값들이 포함됩니다:
                pet_id (int): 반려동물 아이디
                ticket_type (str): 티켓 타입 (예: "4시간", "종일", "호텔")
                ticket_id (int, optional): 티켓 아이디로, 호텔권일 경우 불필요
                reserved_date (datetime): 등원 날짜
                end_date (datetime, optional): 하원 날짜, 호텔권일 경우만 필요
                attendance_time (datetime, optional): 등원 시간으로, 시간권일 경우만 필요

        Returns:
            None  # 바뀔지도...
        """
        ticket_type = reservation_data['ticket_type']
        strategy = self.get_strategy(ticket_type)
        strategy.validate(customer, pet_kindergarden, reservation_data)
        strategy.reserve(reservation_data)  # 로그 생성, 데일리 예약 증가, 티켓 회수 등


class TimeReservationStrategy(AbstractReservationStrategy):

    ...


class AllDayReservationStrategy(AbstractReservationStrategy):

    ...


class HotelReservationStrategy(AbstractReservationStrategy):

    ...

해결 방법

순환 참조와 관련하여 해결할 수 있는 방법 중 고려한 해결 방법은 아래의 두 가지이다.

  • 클래스의 위치를 재구성하는 방법
  • 함수 내부에서 import를 수행하는 방법

이 방법 중 클래스 위치를 재구성 하기로 했다. 이유는 코드의 모듈 간의 의존성을 줄여 유지보수성을 높이고, 단일 책임 원칙을 준수하여 각 모듈이 하나의 책임에 집중할 수 있게 하기 위함이다. 따라서 순환 참조 문제를 전략 패턴을 위해 구현한 클래스를 외부로 분리하여 import하는 방식으로 해결했다.

profile
안녕하세요! 질문과 피드백은 언제든지 환영입니다:)

0개의 댓글