현재는 도메인 중심으로 repository를 설계하고 있음.
class PlaceRepository(ABC):
@abstractmethod
def find_for_gallery_display(self, place_id: int) -> Optional[Place]:
pass
@abstractmethod
def find_for_detail_page(self, place_id: int) -> Optional[Place]:
pass사실은 도메인 중심도 아니고 service로 repository의 최적화 로직이 유출되고 있는 상황이지만…(load 할거 선택) 도메인 방향으로 고친다고 가정하더라도 도메인이 많아질수록 조합이 폭발하듯 많아지는 문제가 발생함.
어떻게 해결할까?
- Query Object 패턴을 사용해서 Repository에 쿼리 자체를 전달한다
# 쿼리 스펙을 객체로 표현
@dataclass
class PlaceQuery:
place_id: int
include_images: bool = False
include_reviews: bool = False
include_tags: bool = False
@classmethod
def for_gallery(cls, place_id: int) -> 'PlaceQuery':
return cls(place_id=place_id, include_images=True)
@classmethod
def for_detail_page(cls, place_id: int) -> 'PlaceQuery':
return cls(
place_id=place_id,
include_images=True,
include_reviews=True,
include_tags=True
)
@classmethod
def for_basic(cls, place_id: int) -> 'PlaceQuery':
return cls(place_id=place_id)
# Repository는 하나의 메서드만
class PlaceRepository(ABC):
@abstractmethod
def find(self, query: PlaceQuery) -> Optional[Place]:
pass
# SQLAlchemy 구현체
class SQLAlchemyPlaceRepository(PlaceRepository):
def find(self, query: PlaceQuery) -> Optional[Place]:
stmt = select(Place).where(Place.id == query.place_id)
# 쿼리 객체에 따라 동적으로 로딩 옵션 적용
options = []
if query.include_images:
options.append(selectinload(Place.images))
if query.include_reviews:
options.append(selectinload(Place.reviews))
if query.include_tags:
options.append(selectinload(Place.tags))
if options:
stmt = stmt.options(*options)
return self.session.execute(stmt).scalar_one_or_none()
# Service에서 사용
class PlaceService:
def get_place_for_gallery(self, place_id: int):
query = PlaceQuery.for_gallery(place_id)
return self.place_repository.find(query)
def get_place_for_detail(self, place_id: int):
query = PlaceQuery.for_detail_page(place_id)
return self.place_repository.find(query)이런식으로 2. CQRS 패턴을 사용한다
command용과 query용을 분리해서 사용하는건데… 아직까지 우리 서비스에 이정도까지 하는건 오버킬 같다
