Search

repository pattern은 어떤 값을 반환해야 하는가에 대한 지론

Repository pattern은 어떤 값을 반환해야 하는가에 대한 지론

위 글을 참고하여 정리했습니다.
파이썬을 사용 시, 레포지토리에서 DTO를 이용해서 반환하게 하는 패턴을 주로 사용하고 있습니다.
각각의 레포지토리가 그렇게 간단하지 않은 경우도 많고, 단일 엔티티를 조회하는 것이 아닌, 레포지토리 자체가 여러 엔티티를 조회해야하는 로직도 생길 수 있는데, 여러 엔티티를 다루는 DTO를 반환하게 합니다.
레포지토리에서 DTO를 반환하는 것이 항상 안티패턴인 것은 아니지만, 특정 상황에서는 안티패턴으로 간주될 수 있습니다. 여기서 중요한 점은 각 계층이 자신의 책임을 명확히 유지하는 것입니다.

요약

1.
이론적 관점:
레포지토리는 엔터티 타입별로 데이터 제공자 역할을 해야 합니다.
ARepository는 A 엔터티를, BRepository는 B 엔터티를 반환해야 합니다.
2.
현실적 관점:
여러 엔터티 타입을 하나의 요청에서 가져와야 할 때, 각 엔터티 타입별로 분리된 쿼리를 사용하는 것은 비효율적입니다.
SQL 데이터베이스는 조인을 통해 데이터를 효율적으로 가져올 수 있지만, 레포지토리 패턴은 이와 같은 효율적인 데이터 혼합 접근 방식을 구현하는데 한계가 있습니다.
이러한 경우, 레포지토리 사용이 오히려 성능 저하를 초래할 수 있습니다.
3.
해결책:
유닛 오브 워크(Unit of Work)를 사용하여 트랜잭션 안전성을 보장할 수 있습니다. 이는 모든 레포지토리가 동일한 컨텍스트를 사용하도록 합니다.
복잡한 데이터 쿼리를 작성할 위치를 결정하는 것은 어려운 문제입니다.
레포지토리는 단일 엔터티 타입에 대한 간단한 CRUD 기능에는 유용하지만, 복잡한 데이터 조회에는 문제를 초래할 수 있습니다.

여러 프로젝트에서 사용된 접근 방식

1.
주 엔터티 타입의 레포지토리에 복잡한 데이터 쿼리를 배치:
자동차와 소유자를 포함한 자동차 목록을 가져오는 쿼리는 CarRepository에 넣습니다.
소유한 자동차를 포함한 사람 목록을 가져오는 쿼리는 PersonRepository에 넣습니다.
장점: 기존 레포지토리 구조를 유지하면서 코드 위치를 명확하게 할 수 있습니다.
단점: 명확한 "주" 엔터티 타입이 없는 경우도 있습니다.
2.
복잡한 데이터 쿼리를 별도의 레포지토리에 배치:
단일 엔터티 타입에 초점을 맞춘 "기존" 레포지토리는 CRUD 작업에만 사용합니다.
A와 B 객체를 저장하는 메서드가 있다면 ABRepository를 만들고, 내부적으로 ARepository와 BRepository를 사용합니다. (이 경우 유닛 오브 워크가 매우 중요합니다.)
장점: CRUD 로직과 보고서 로직을 분리할 수 있습니다.
단점: 많은 조합(AB, AC, ABC, ACD 등)이 생길 수 있으며, 레포지토리 목록이 비대해질 수 있습니다.
해결책: 이러한 레포지토리를 기능(YearlyReportsRepository)별로 이름을 지정하고, 단순히 엔터티 타입의 목록(PersonCarRepository)으로 이름을 지정하지 않습니다.

레포지토리가 DTO를 반환하지 않는다면 어떤 모델을 반환해야 할까요?

1.
옵션 1을 선택한 경우:
자동차와 소유자를 포함한 자동차 목록을 가져오는 쿼리는 CarRepository에 배치하고, IEnumerable<Car>를 반환합니다.
소유한 자동차를 포함한 사람 목록을 가져오는 쿼리는 PersonRepository에 배치하고, IEnumerable<Person>를 반환합니다.
2.
옵션 2를 선택한 경우:
복잡한 데이터 보고서는 간단한 CRUD 작업과는 다른 것으로 간주됩니다. 이 경우, 커스텀 클래스를 반환하게 됩니다(CarOwnerResult).
단일 엔터티 타입에 바인딩된 단순한 레포지토리는 여전히 자신의 바인딩된 엔터티 타입만을 반환할 수 있습니다.

결론

레포지토리 패턴을 사용할 때 DTO를 반환하는 것이 안티패턴인지 여부는 사용자의 상황에 따라 다릅니다. 단일 엔터티 타입에 대한 간단한 CRUD 작업에는 레포지토리 패턴이 유용하지만, 복잡한 데이터 조회 작업에서는 다른 접근 방식을 고려하는 것이 좋습니다. 유닛 오브 워크 패턴(UOW)을 사용하여 트랜잭션 안전성을 보장하고, 복잡한 쿼리를 위한 별도의 레포지토리를 사용하는 것이 좋은 해결책이 될 수 있습니다.

예시 코드

Base = declarative_base() # 모델 정의 class Order(Base): __tablename__ = 'orders' id = Column(Integer, primary_key=True) customer_name = Column(String) total_amount = Column(Float) items = relationship("OrderItem", back_populates="order") class OrderItem(Base): __tablename__ = 'order_items' id = Column(Integer, primary_key=True) order_id = Column(Integer, ForeignKey('orders.id')) product_name = Column(String) quantity = Column(Integer) price = Column(Float) order = relationship("Order", back_populates="items") # DTO 정의 @dataclass class OrderItemDTO: product_name: str quantity: int price: float @dataclass class OrderDetailsDTO: order_id: int customer_name: str total_amount: float items: List[OrderItemDTO] # 레포지토리 class OrderRepository: def __init__(self, session): self.session = session def find_order_details_by_id(self, order_id: int) -> OrderDetailsDTO: order = self.session.query(Order).filter(Order.id == order_id).first() if not order: raise ValueError(f"Order not found with id: {order_id}") items = [OrderItemDTO(item.product_name, item.quantity, item.price) for item in order.items] return OrderDetailsDTO( order_id=order.id, customer_name=order.customer_name, total_amount=order.total_amount, items=items ) # 서비스 class OrderService: def __init__(self, order_repository: OrderRepository): self.order_repository = order_repository def get_order_details(self, order_id: int) -> OrderDetailsDTO: return self.order_repository.find_order_details_by_id(order_id)
Python
복사

전통적인 레포지토리 접근 방식 (옵션 1)

Base = declarative_base() # 모델 정의 class Car(Base): __tablename__ = 'cars' id = Column(Integer, primary_key=True) model = Column(String) owner_id = Column(Integer, ForeignKey('persons.id')) owner = relationship("Person", back_populates="cars") class Person(Base): __tablename__ = 'persons' id = Column(Integer, primary_key=True) name = Column(String) cars = relationship("Car", back_populates="owner") # 옵션 1: 전통적인 레포지토리 접근 방식 class CarRepository: def __init__(self, session): self.session = session def get_all_cars(self): return self.session.query(Car).all() class PersonRepository: def __init__(self, session): self.session = session def get_all_persons(self): return self.session.query(Person).all() # 옵션 2: 복잡한 데이터 쿼리를 위한 별도의 레포지토리 @dataclass class CarOwnerResult: car_model: str owner_name: str class CarOwnerRepository: def __init__(self, session): self.session = session def get_car_owners(self): results = self.session.query(Car.model, Person.name).\ join(Person, Car.owner_id == Person.id).all() return [CarOwnerResult(car_model=model, owner_name=name) for model, name in results] # 데이터베이스 설정 및 세션 생성 engine = create_engine('sqlite:///cars_and_owners.db') Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) session = Session() # 샘플 데이터 생성 def create_sample_data(): john = Person(name="John Doe") jane = Person(name="Jane Smith") car1 = Car(model="Toyota Corolla", owner=john) car2 = Car(model="Honda Civic", owner=jane) car3 = Car(model="Ford Mustang", owner=john) session.add_all([john, jane, car1, car2, car3]) session.commit() create_sample_data() # 옵션 1 사용 예시 car_repo = CarRepository(session) person_repo = PersonRepository(session) print("All Cars:") for car in car_repo.get_all_cars(): print(f"- {car.model}") print("\nAll Persons:") for person in person_repo.get_all_persons(): print(f"- {person.name}") # 옵션 2 사용 예시 car_owner_repo = CarOwnerRepository(session) print("\nCar Owners:") for result in car_owner_repo.get_car_owners(): print(f"- {result.car_model} owned by {result.owner_name}") session.close()
Python
복사
이와 같이, 복잡한 데이터 쿼리를 위한 별도의 레포지토리를 사용하는 접근 방식을 취할 수 있습니다.