Skip to the content.

Query Service Guide — UseCase 구현체

QueryService는 Port-In(UseCase) 인터페이스의 구현체입니다.

Query → Criteria → 조회 → Response 흐름을 조율합니다.


1) 핵심 원칙

원칙 설명
Port-In 구현 UseCase 인터페이스를 구현
조율만 수행 비즈니스 로직은 Domain, 변환은 Factory/Assembler
읽기 전용 상태 변경 없음, 조회만 수행
Lombok 금지 생성자 직접 작성 (Plain Java)

2) 패키지 구조

application/{bc}/
├─ service/
│  └─ query/                          ← Query UseCase 구현
│     ├─ GetOrderDetailService.java   ← 복잡한 Query (QueryFacade 사용)
│     └─ SearchOrdersService.java     ← 단순 Query (ReadManager 직접)
│
├─ factory/query/
│  └─ OrderQueryFactory.java          ← Query → Criteria
│
├─ facade/query/
│  └─ OrderQueryFacade.java           ← 복잡한 Query (ReadManager 2개+)
│
├─ manager/query/
│  └─ OrderReadManager.java           ← 단일 조회 담당
│
├─ assembler/
│  └─ OrderAssembler.java             ← Domain/Bundle → Response
│
└─ port/in/query/
   └─ GetOrderDetailUseCase.java      ← Port-In 인터페이스

3) Query 흐름

복잡한 Query (ReadManager 2개 이상)

Controller
    ↓
UseCase (GetOrderDetailService)
    ↓
QueryFactory.createCriteria(Query) → Criteria
    ↓
QueryFacade.fetchXxx(Criteria)
    ├─ ReadManager1.findBy(Criteria) → Order
    ├─ ReadManager2.findBy(OrderId) → List<Item>
    └─ ReadManager3.findBy(CustomerId) → Customer
    ↓
QueryBundle 반환
    ↓
Assembler.toResponse(QueryBundle) → Response

단순 Query (ReadManager 1개)

Controller
    ↓
UseCase
    ↓
QueryFactory.createCriteria(Query) → Criteria (필요시)
    ↓
ReadManager.findBy(Criteria) → Domain
    ↓
Assembler.toResponse(Domain) → Response

4) 사용 기준

QueryFacade vs ReadManager 직접 호출

조건 사용
ReadManager 2개 이상 조합 QueryFacade 사용
ReadManager 1개 ReadManager 직접 호출

Factory 사용 기준

조건 사용
Query → Criteria 변환 필요 QueryFactory 사용
단순 ID 조회 Factory 불필요

5) 구현 예시

복잡한 Query Service

package com.ryuqq.application.order.service.query;

import com.ryuqq.application.order.assembler.OrderAssembler;
import com.ryuqq.application.order.dto.bundle.OrderDetailQueryBundle;
import com.ryuqq.application.order.dto.query.OrderDetailQuery;
import com.ryuqq.application.order.dto.response.OrderDetailResponse;
import com.ryuqq.application.order.facade.query.OrderQueryFacade;
import com.ryuqq.application.order.factory.query.OrderQueryFactory;
import com.ryuqq.application.port.in.query.GetOrderDetailUseCase;
import com.ryuqq.domain.order.criteria.OrderDetailCriteria;
import org.springframework.stereotype.Service;

/**
 * 주문 상세 조회 UseCase 구현체
 * - 복잡한 Query: QueryFacade 사용 (ReadManager 3개 조합)
 */
@Service
public class GetOrderDetailService implements GetOrderDetailUseCase {

    private final OrderQueryFactory queryFactory;
    private final OrderQueryFacade queryFacade;
    private final OrderAssembler assembler;

    public GetOrderDetailService(
        OrderQueryFactory queryFactory,
        OrderQueryFacade queryFacade,
        OrderAssembler assembler
    ) {
        this.queryFactory = queryFactory;
        this.queryFacade = queryFacade;
        this.assembler = assembler;
    }

    @Override
    public OrderDetailResponse execute(OrderDetailQuery query) {
        // 1. Query → Criteria (Factory)
        OrderDetailCriteria criteria = queryFactory.createDetailCriteria(query);

        // 2. 조회 (QueryFacade - 여러 ReadManager 조합)
        OrderDetailQueryBundle bundle = queryFacade.fetchOrderDetail(criteria);

        // 3. Response 변환 (Assembler)
        return assembler.toDetailResponse(bundle);
    }
}

단순 Query Service

package com.ryuqq.application.order.service.query;

import com.ryuqq.application.order.assembler.OrderAssembler;
import com.ryuqq.application.order.dto.query.OrderSearchQuery;
import com.ryuqq.application.order.dto.response.OrderListResponse;
import com.ryuqq.application.order.factory.query.OrderQueryFactory;
import com.ryuqq.application.order.manager.query.OrderReadManager;
import com.ryuqq.application.port.in.query.SearchOrdersUseCase;
import com.ryuqq.domain.order.aggregate.Order;
import com.ryuqq.domain.order.criteria.OrderSearchCriteria;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 주문 목록 조회 UseCase 구현체
 * - 단순 Query: ReadManager 직접 호출 (1개)
 */
@Service
public class SearchOrdersService implements SearchOrdersUseCase {

    private final OrderQueryFactory queryFactory;
    private final OrderReadManager orderReadManager;
    private final OrderAssembler assembler;

    public SearchOrdersService(
        OrderQueryFactory queryFactory,
        OrderReadManager orderReadManager,
        OrderAssembler assembler
    ) {
        this.queryFactory = queryFactory;
        this.orderReadManager = orderReadManager;
        this.assembler = assembler;
    }

    @Override
    public OrderListResponse execute(OrderSearchQuery query) {
        // 1. Query → Criteria (Factory)
        OrderSearchCriteria criteria = queryFactory.createSearchCriteria(query);

        // 2. 조회 (ReadManager 직접 - 단일)
        List<Order> orders = orderReadManager.findBy(criteria);

        // 3. Response 변환 (Assembler)
        return assembler.toListResponse(orders);
    }
}

ID로 단순 조회 Service

package com.ryuqq.application.order.service.query;

import com.ryuqq.application.order.assembler.OrderAssembler;
import com.ryuqq.application.order.dto.response.OrderResponse;
import com.ryuqq.application.order.manager.query.OrderReadManager;
import com.ryuqq.application.port.in.query.GetOrderByIdUseCase;
import com.ryuqq.domain.order.aggregate.Order;
import com.ryuqq.domain.order.vo.OrderId;
import org.springframework.stereotype.Service;

/**
 * 주문 단건 조회 UseCase 구현체
 * - 단순 ID 조회: Factory 불필요
 */
@Service
public class GetOrderByIdService implements GetOrderByIdUseCase {

    private final OrderReadManager orderReadManager;
    private final OrderAssembler assembler;

    public GetOrderByIdService(
        OrderReadManager orderReadManager,
        OrderAssembler assembler
    ) {
        this.orderReadManager = orderReadManager;
        this.assembler = assembler;
    }

    @Override
    public OrderResponse execute(Long orderId) {
        // 1. 조회 (ReadManager - ID 직접 사용)
        Order order = orderReadManager.getById(new OrderId(orderId));

        // 2. Response 변환 (Assembler)
        return assembler.toResponse(order);
    }
}

페이지네이션 Query Service

package com.ryuqq.application.order.service.query;

import com.ryuqq.application.order.assembler.OrderAssembler;
import com.ryuqq.application.order.dto.query.OrderPageQuery;
import com.ryuqq.application.order.dto.response.OrderPageResponse;
import com.ryuqq.application.order.factory.query.OrderQueryFactory;
import com.ryuqq.application.order.manager.query.OrderReadManager;
import com.ryuqq.application.port.in.query.GetOrderPageUseCase;
import com.ryuqq.domain.order.aggregate.Order;
import com.ryuqq.domain.order.criteria.OrderPageCriteria;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 주문 페이지 조회 UseCase 구현체
 */
@Service
public class GetOrderPageService implements GetOrderPageUseCase {

    private final OrderQueryFactory queryFactory;
    private final OrderReadManager orderReadManager;
    private final OrderAssembler assembler;

    public GetOrderPageService(
        OrderQueryFactory queryFactory,
        OrderReadManager orderReadManager,
        OrderAssembler assembler
    ) {
        this.queryFactory = queryFactory;
        this.orderReadManager = orderReadManager;
        this.assembler = assembler;
    }

    @Override
    public OrderPageResponse execute(OrderPageQuery query) {
        // 1. Query → Criteria (Factory)
        OrderPageCriteria criteria = queryFactory.createPageCriteria(query);

        // 2. 조회 (ReadManager)
        List<Order> orders = orderReadManager.findBy(criteria);
        long totalCount = orderReadManager.countBy(criteria);

        // 3. Response 변환 (Assembler)
        return assembler.toPageResponse(orders, totalCount, criteria.page(), criteria.size());
    }
}

6) 필수 규칙 (Zero-Tolerance)

규칙 설명 위반 시
@Service 어노테이션 UseCase 구현체 표시 빌드 실패
Port-In 구현 UseCase 인터페이스 implements 빌드 실패
service/query/ 패키지 올바른 위치 빌드 실패
@Transactional 금지 읽기 전용, 필요 없음 빌드 실패
Port 직접 호출 금지 ReadManager/QueryFacade 통해 접근 빌드 실패
상태 변경 금지 조회만 수행 코드 리뷰
Lombok 금지 Plain Java 빌드 실패

7) Do / Don’t

✅ Good

// ✅ Good: @Service 어노테이션
@Service
public class GetOrderDetailService implements GetOrderDetailUseCase { ... }

// ✅ Good: Port-In 인터페이스 구현
public class GetOrderDetailService implements GetOrderDetailUseCase { ... }

// ✅ Good: Factory, QueryFacade/ReadManager, Assembler 조합
OrderDetailCriteria criteria = queryFactory.createDetailCriteria(query);
OrderDetailQueryBundle bundle = queryFacade.fetchOrderDetail(criteria);
return assembler.toDetailResponse(bundle);

// ✅ Good: 단순한 경우 ReadManager 직접 호출
OrderSearchCriteria criteria = queryFactory.createSearchCriteria(query);
List<Order> orders = orderReadManager.findBy(criteria);
return assembler.toListResponse(orders);

// ✅ Good: 명시적 생성자 (Lombok 금지)
public GetOrderDetailService(
    OrderQueryFactory queryFactory,
    OrderQueryFacade queryFacade,
    OrderAssembler assembler
) {
    this.queryFactory = queryFactory;
    this.queryFacade = queryFacade;
    this.assembler = assembler;
}

❌ Bad

// ❌ Bad: @Component 어노테이션
@Component  // ❌ @Service 사용해야 함
public class GetOrderDetailService { ... }

// ❌ Bad: @Transactional 사용 (읽기 전용, 불필요)
@Service
public class GetOrderDetailService {
    @Transactional(readOnly = true)  // ❌ 불필요
    public OrderDetailResponse execute(...) { ... }
}

// ❌ Bad: Port 직접 호출
@Service
public class GetOrderDetailService {
    private final OrderQueryPort orderPort;  // ❌ ReadManager 사용

    public OrderDetailResponse execute(...) {
        orderPort.findById(id);  // ❌
    }
}

// ❌ Bad: 상태 변경 (Query에서 금지)
public OrderDetailResponse execute(OrderDetailQuery query) {
    Order order = orderReadManager.getById(orderId);
    order.updateStatus(...);  // ❌ Command 책임
}

// ❌ Bad: Lombok 사용
@Service
@RequiredArgsConstructor  // ❌ Lombok 금지
public class GetOrderDetailService { ... }

8) 체크리스트


9) 관련 문서


작성자: Development Team 최종 수정일: 2025-12-04 버전: 1.0.0