Skip to the content.

Command Service Guide — UseCase 구현체

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

Command → Domain → 영속화 → Response 흐름을 조율합니다.


1) 핵심 원칙

원칙 설명
Port-In 구현 UseCase 인터페이스를 구현
조율만 수행 비즈니스 로직은 Domain, 변환은 Factory/Assembler
트랜잭션 위임 직접 @Transactional 사용 금지, Manager/Facade에 위임
Lombok 금지 생성자 직접 작성 (Plain Java)

2) 패키지 구조

application/{bc}/
├─ service/
│  └─ command/                        ← Command UseCase 구현
│     ├─ PlaceOrderService.java       ← 복잡한 Command (Facade 사용)
│     └─ UpdateOrderStatusService.java ← 단순 Command (Manager 직접)
│
├─ factory/command/
│  └─ OrderCommandFactory.java        ← Command → Domain, PersistBundle
│
├─ facade/command/
│  └─ OrderFacade.java                ← 복잡한 Command (Manager 2개+)
│
├─ manager/command/
│  └─ OrderTransactionManager.java    ← 단일 영속화 담당
│
├─ assembler/
│  └─ OrderAssembler.java             ← Domain → Response
│
└─ port/in/command/
   └─ PlaceOrderUseCase.java          ← Port-In 인터페이스

3) 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

4) 사용 기준

Facade vs Manager 직접 호출

조건 사용
Manager 2개 이상 조합 Facade 사용
Manager 1개 Manager 직접 호출

Factory 사용 기준

조건 사용
Command → Domain 변환 필요 CommandFactory 사용
Command → PersistBundle 필요 CommandFactory.createBundle() 사용
단순 상태 변경 (ID만 필요) Factory 불필요

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

void 반환 Command Service

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

import com.ryuqq.application.order.dto.command.CancelOrderCommand;
import com.ryuqq.application.order.manager.command.OrderTransactionManager;
import com.ryuqq.application.port.in.command.CancelOrderUseCase;
import com.ryuqq.domain.order.aggregate.Order;
import com.ryuqq.domain.order.vo.OrderId;
import org.springframework.stereotype.Service;

/**
 * 주문 취소 UseCase 구현체
 * - void 반환: Response 불필요
 */
@Service
public class CancelOrderService implements CancelOrderUseCase {

    private final OrderTransactionManager orderManager;

    public CancelOrderService(OrderTransactionManager orderManager) {
        this.orderManager = orderManager;
    }

    @Override
    public void execute(CancelOrderCommand command) {
        // 1. 조회 (Manager)
        Order order = orderManager.getById(new OrderId(command.orderId()));

        // 2. 도메인 로직 실행 (Domain)
        order.cancel(command.reason());

        // 3. 영속화 (Manager)
        orderManager.persist(order);
    }
}

6) 필수 규칙 (Zero-Tolerance)

규칙 설명 위반 시
@Service 어노테이션 UseCase 구현체 표시 빌드 실패
Port-In 구현 UseCase 인터페이스 implements 빌드 실패
service/command/ 패키지 올바른 위치 빌드 실패
@Transactional 금지 Manager/Facade 책임 빌드 실패
Port 직접 호출 금지 Manager/Facade 통해 접근 빌드 실패
객체 직접 생성 금지 Factory 책임 빌드 실패
비즈니스 로직 금지 Domain 책임 코드 리뷰
Lombok 금지 Plain Java 빌드 실패

7) 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 조합
OrderPersistBundle bundle = commandFactory.createBundle(command);
Order saved = orderFacade.persistOrderBundle(bundle);
return assembler.toResponse(saved);

// ✅ Good: 단순한 경우 Manager 직접 호출
Order order = commandFactory.create(command);
Order saved = orderManager.persist(order);
return assembler.toResponse(saved);

// ✅ Good: 명시적 생성자 (Lombok 금지)
public PlaceOrderService(
    OrderCommandFactory commandFactory,
    OrderFacade orderFacade,
    OrderAssembler assembler
) {
    this.commandFactory = commandFactory;
    this.orderFacade = orderFacade;
    this.assembler = assembler;
}

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

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

8) 체크리스트


9) 관련 문서


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