Skip to the content.

Service Guide — UseCase 구현체 (CQRS)

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

CommandQuery를 분리하여 각각의 흐름을 따릅니다.


1) 핵심 원칙


2) 패키지 구조

application/{bc}/
├─ service/
│  ├─ command/                       ← Command UseCase 구현
│  │  └─ PlaceOrderService.java
│  └─ query/                         ← Query UseCase 구현
│     └─ GetOrderService.java
│
├─ factory/
│  ├─ command/
│  │  └─ OrderCommandFactory.java    ← Command → Domain, PersistBundle
│  └─ query/
│     └─ OrderQueryFactory.java      ← Query → Criteria
│
├─ facade/
│  ├─ command/
│  │  └─ OrderFacade.java            ← 복잡한 Command (Manager 2개+)
│  └─ query/
│     └─ OrderQueryFacade.java       ← 복잡한 Query (ReadManager 2개+)
│
├─ manager/
│  ├─ command/
│  │  └─ OrderTransactionManager.java ← Command용 (영속화)
│  └─ query/
│     └─ OrderReadManager.java        ← Query용 (조회)
│
├─ dto/
│  ├─ command/
│  │  └─ PlaceOrderCommand.java
│  ├─ query/
│  │  └─ OrderSearchQuery.java
│  ├─ bundle/
│  │  ├─ OrderPersistBundle.java     ← Command용 Bundle
│  │  └─ OrderQueryBundle.java       ← Query용 Bundle
│  └─ response/
│     └─ OrderResponse.java
│
├─ assembler/
│  └─ OrderAssembler.java            ← Domain/Bundle → Response
│
└─ config/
   └─ TransactionEventRegistry.java  ← 커밋 후 Event 발행

3) Command vs Query 흐름

Command 흐름

┌─────────────────────────────────────────────────────────────┐
│ [복잡한 Command] - Manager 2개 이상                          │
│                                                             │
│ Controller                                                  │
│     ↓                                                       │
│ UseCase (PlaceOrderService)                                 │
│     ↓                                                       │
│ CommandFactory.createBundle(Command) → PersistBundle        │
│     ↓                                                       │
│ Facade.persistXxx(Bundle)                                   │
│     ├─ Manager1.persist(Order)                              │
│     ├─ Manager2.persist(History)                            │
│     └─ Manager3.persist(Outbox)                             │
│     ↓                                                       │
│ EventRegistry.registerForPublish(Event)                     │
│     ↓                                                       │
│ Assembler.toResponse(Order) → Response                      │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ [단순 Command] - Manager 1개                                 │
│                                                             │
│ Controller                                                  │
│     ↓                                                       │
│ UseCase                                                     │
│     ↓                                                       │
│ CommandFactory.create(Command) → Domain                     │
│     ↓                                                       │
│ Manager.persist(Domain)                                     │
│     ↓                                                       │
│ Assembler.toResponse(Domain) → Response                     │
└─────────────────────────────────────────────────────────────┘

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) 필수 규칙 (Zero-Tolerance)

규칙 설명
@Service 어노테이션 UseCase 구현체는 @Service
Port-In 구현 UseCase 인터페이스 implements
command/query 분리 패키지로 명확히 구분
@Transactional 금지 Manager/Facade에 위임
비즈니스 로직 금지 Domain Layer 책임
객체 생성 금지 Factory 책임
직접 Port 호출 금지 Manager/Facade 통해 접근
Lombok 금지 생성자 직접 작성

5) 구현 예시

Command Service (복잡한 경우)

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

import com.ryuqq.application.order.assembler.OrderAssembler;
import com.ryuqq.application.order.dto.bundle.OrderPersistBundle;
import com.ryuqq.application.order.dto.command.PlaceOrderCommand;
import com.ryuqq.application.order.dto.response.OrderResponse;
import com.ryuqq.application.order.facade.command.OrderFacade;
import com.ryuqq.application.order.factory.command.OrderCommandFactory;
import com.ryuqq.application.common.config.TransactionEventRegistry;
import com.ryuqq.application.port.in.command.PlaceOrderUseCase;
import com.ryuqq.domain.order.aggregate.Order;
import org.springframework.stereotype.Service;

/**
 * 주문 생성 UseCase 구현체
 * - 복잡한 Command: Facade 사용 (Manager 3개 조합)
 */
@Service
public class PlaceOrderService implements PlaceOrderUseCase {

    private final OrderCommandFactory commandFactory;
    private final OrderFacade orderFacade;
    private final TransactionEventRegistry eventRegistry;
    private final OrderAssembler assembler;

    public PlaceOrderService(
        OrderCommandFactory commandFactory,
        OrderFacade orderFacade,
        TransactionEventRegistry eventRegistry,
        OrderAssembler assembler
    ) {
        this.commandFactory = commandFactory;
        this.orderFacade = orderFacade;
        this.eventRegistry = eventRegistry;
        this.assembler = assembler;
    }

    @Override
    public OrderResponse execute(PlaceOrderCommand command) {
        // 1. Command → Bundle (Factory)
        OrderPersistBundle bundle = commandFactory.createBundle(command);

        // 2. 영속화 (Facade - 여러 Manager 조합)
        Order savedOrder = orderFacade.persistOrderBundle(bundle);

        // 3. Event 등록 (커밋 후 발행)
        eventRegistry.registerForPublish(savedOrder.pullDomainEvents());

        // 4. Response 변환 (Assembler)
        return assembler.toResponse(savedOrder);
    }
}

Command Service (단순한 경우)

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

import com.ryuqq.application.order.assembler.OrderAssembler;
import com.ryuqq.application.order.dto.command.UpdateOrderStatusCommand;
import com.ryuqq.application.order.dto.response.OrderResponse;
import com.ryuqq.application.order.factory.command.OrderCommandFactory;
import com.ryuqq.application.order.manager.command.OrderTransactionManager;
import com.ryuqq.application.port.in.command.UpdateOrderStatusUseCase;
import com.ryuqq.domain.order.aggregate.Order;
import org.springframework.stereotype.Service;

/**
 * 주문 상태 변경 UseCase 구현체
 * - 단순 Command: Manager 직접 호출 (1개)
 */
@Service
public class UpdateOrderStatusService implements UpdateOrderStatusUseCase {

    private final OrderCommandFactory commandFactory;
    private final OrderTransactionManager orderManager;
    private final OrderAssembler assembler;

    public UpdateOrderStatusService(
        OrderCommandFactory commandFactory,
        OrderTransactionManager orderManager,
        OrderAssembler assembler
    ) {
        this.commandFactory = commandFactory;
        this.orderManager = orderManager;
        this.assembler = assembler;
    }

    @Override
    public OrderResponse execute(UpdateOrderStatusCommand command) {
        // 1. Command → Domain (Factory)
        Order order = commandFactory.createForStatusUpdate(command);

        // 2. 영속화 (Manager 직접 - 단일)
        Order savedOrder = orderManager.persist(order);

        // 3. Response 변환 (Assembler)
        return assembler.toResponse(savedOrder);
    }
}

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);
    }
}

6) 컴포넌트 역할 요약

컴포넌트 Command Query
Service PlaceOrderService GetOrderService
Factory CommandFactory QueryFactory
Facade Facade (persist*) QueryFacade (fetch*)
Manager TransactionManager ReadManager
Bundle PersistBundle QueryBundle
Assembler Domain → Response Domain/Bundle → Response

7) 사용 기준

Facade 사용 기준

조건 Command Query
Manager 2개 이상 Facade 사용 QueryFacade 사용
Manager 1개 Manager 직접 호출 ReadManager 직접 호출

Factory 사용 기준

조건 Command Query
DTO → Domain/Criteria 변환 필요 CommandFactory 사용 QueryFactory 사용
단순 ID 조회 Factory 불필요 Factory 불필요

8) Do / Don’t

✅ Good

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

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

// ✅ Good: Factory, Facade/Manager, Assembler 조합
Order order = commandFactory.create(command);
Order saved = orderManager.persist(order);
return assembler.toResponse(saved);

// ✅ Good: 복잡한 경우 Facade 사용
OrderPersistBundle bundle = commandFactory.createBundle(command);
Order saved = orderFacade.persistOrderBundle(bundle);

❌ Bad

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

// ❌ Bad: @Transactional 직접 사용
@Service
public class PlaceOrderService {
    @Transactional  // ❌ Manager/Facade에 위임
    public OrderResponse execute(...) { ... }
}

// ❌ Bad: Port 직접 호출
@Service
public class PlaceOrderService {
    private final OrderCommandPort orderPort;  // ❌ Manager 사용

    public OrderResponse execute(...) {
        orderPort.save(order);  // ❌
    }
}

// ❌ Bad: 객체 직접 생성
public OrderResponse execute(PlaceOrderCommand command) {
    Order order = Order.forNew(...);  // ❌ Factory 사용
}

// ❌ Bad: 비즈니스 로직 포함
public OrderResponse execute(PlaceOrderCommand command) {
    if (command.totalAmount() > MAX) {  // ❌ Domain 책임
        throw new BusinessException("Too expensive");
    }
}

9) 체크리스트

Command Service

Query Service


10) 관련 문서

Command 관련

Query 관련

공통


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