Back to catalog

application-expert

Spring Application Layer 전문가. UseCase, Command/Query 분리, Transaction 경계, Facade 패턴을 준수하는 Application 서비스를 생성합니다. @Transactional 내 외부 API 호출 금지, Spring Proxy 제약사항을 보장합니다.

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: "application-expert"
description: "Spring Application Layer 전문가. UseCase, Command/Query 분리, Transaction 경계, Facade 패턴을 준수하는 Application 서비스를 생성합니다. @Transactional 내 외부 API 호출 금지, Spring Proxy 제약사항을 보장합니다."
---

# Spring Application Layer Expert

Application Layer 전문가 Skill입니다. UseCase(포트), 비즈니스 흐름 조율, Transaction 관리를 담당합니다.

## 전문 분야

1. **UseCase 설계**: Port/In 인터페이스, Command/Query 분리, 메서드 네이밍 규칙
2. **Transaction 경계**: `@Transactional` 올바른 사용, 외부 API 호출 분리
3. **Facade 패턴**: 복잡한 UseCase 조합
4. **Manager 패턴**: 복잡한 비즈니스 흐름 조율 (Facade보다 상위 개념)
5. **Assembler 패턴**: DTO ↔ Domain 변환

## 사용 시점

- UseCase(Port/In) 생성 또는 수정
- Command/Query DTO 설계
- Transaction 경계 관리
- 복잡한 비즈니스 흐름 구현

## 핵심 규칙

### 1. UseCase 설계 (Port/In)

**위치**: `docs/coding_convention/03-application-layer/usecase-design/`

**메서드 네이밍 규칙**:
- **Command 메서드**: `execute{Aggregate}{Action}()` 패턴 (예: `executeOrderCreation()`)
- **Query 메서드**: `query{Aggregate}By{Condition}()` 패턴 (예: `queryOrderById()`)
- **상세 가이드**: [UseCase 메서드 네이밍 컨벤션](../../docs/coding_convention/03-application-layer/usecase-design/04_usecase-method-naming.md)

**Port/In 인터페이스**:
```java
package com.ryuqq.application.order.port.in;

public interface CreateOrderUseCase {
    // ✅ Command 메서드: execute + Order + Creation
    OrderResult executeOrderCreation(CreateOrderCommand command);
}

public interface GetOrderUseCase {
    // ✅ Query 메서드: query + Order + ById
    OrderDetailResponse queryOrderById(GetOrderQuery query);
}
```

**UseCase 구현체**:
```java
package com.ryuqq.application.order.service;

@Service
@Transactional(readOnly = true)
public class CreateOrderService implements CreateOrderUseCase {

    // Port/Out 의존성
    private final OrderRepository orderRepository;
    private final CustomerRepository customerRepository;

    // Assembler
    private final OrderAssembler orderAssembler;

    // External Service (Transaction 밖에서 호출)
    private final PaymentClient paymentClient;

    // ✅ Plain Java Constructor (Lombok 금지)
    public CreateOrderService(
            OrderRepository orderRepository,
            CustomerRepository customerRepository,
            OrderAssembler orderAssembler,
            PaymentClient paymentClient) {
        this.orderRepository = orderRepository;
        this.customerRepository = customerRepository;
        this.orderAssembler = orderAssembler;
        this.paymentClient = paymentClient;
    }

    @Override
    @Transactional  // Write 작업
    public OrderResult executeOrderCreation(CreateOrderCommand command) {
        // 1. Domain 조회
        Customer customer = customerRepository.findById(command.customerId())
            .orElseThrow(() -> new CustomerNotFoundException(command.customerId()));

        // 2. Domain 생성
        Order order = orderAssembler.toDomain(command);

        // 3. 비즈니스 로직 (Domain 메서드 호출)
        order.validateForPlacement(customer);

        // 4. 저장
        Order savedOrder = orderRepository.save(order);

        // ⚠️ 주의: 외부 API 호출은 Transaction 밖에서!
        // ❌ paymentClient.processPayment(savedOrder);  // 여기서 호출 금지!

        // 5. 결과 반환
        return orderAssembler.toResult(savedOrder);
    }
}
```

**규칙**:
- ✅ Port/In 인터페이스 정의
- ✅ `@Service` + `@Transactional(readOnly = true)` 기본
- ✅ Write 작업에는 `@Transactional` 오버라이드
- ✅ Port/Out(Repository) 의존성 주입
- ✅ Assembler로 DTO ↔ Domain 변환
- ❌ **Transaction 내 외부 API 호출 절대 금지**

### 2. Transaction 경계 (Zero-Tolerance)

**위치**: `docs/coding_convention/03-application-layer/transaction-management/`

**올바른 Transaction 분리**:
```java
@Service
public class PlaceOrderFacade {

    private final PlaceOrderService placeOrderService;
    private final PaymentClient paymentClient;
    private final NotificationService notificationService;

    // ✅ Plain Java Constructor (Lombok 금지)
    public PlaceOrderFacade(
            PlaceOrderService placeOrderService,
            PaymentClient paymentClient,
            NotificationService notificationService) {
        this.placeOrderService = placeOrderService;
        this.paymentClient = paymentClient;
        this.notificationService = notificationService;
    }

    public OrderResult placeOrder(PlaceOrderCommand command) {
        // 1. Transaction 내: Domain 작업
        OrderResult result = placeOrderService.execute(command);

        // 2. Transaction 밖: 외부 API 호출
        try {
            paymentClient.processPayment(result.orderId());
        } catch (PaymentException ex) {
            // 결제 실패 시 보상 Transaction
            placeOrderService.cancelOrder(result.orderId());
            throw ex;
        }

        // 3. Transaction 밖: 알림 발송
        notificationService.sendOrderConfirmation(result);

        return result;
    }
}

@Service
@Transactional(readOnly = true)
class PlaceOrderService implements PlaceOrderUseCase {

    private final OrderRepository orderRepository;
    private final OrderAssembler orderAssembler;

    // ✅ Plain Java Constructor (Lombok 금지)
    public PlaceOrderService(
            OrderRepository orderRepository,
            OrderAssembler orderAssembler) {
        this.orderRepository = orderRepository;
        this.orderAssembler = orderAssembler;
    }

    @Override
    @Transactional  // ✅ Transaction은 짧게 유지
    public OrderResult execute(PlaceOrderCommand command) {
        // Domain 작업만 수행
        Order order = orderAssembler.toDomain(command);
        Order savedOrder = orderRepository.save(order);
        return orderAssembler.toResult(savedOrder);
    }

    @Transactional
    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
        order.cancel();
        orderRepository.save(order);
    }
}
```

**규칙**:
- ✅ Transaction은 짧게 유지 (Domain 작업만)
- ✅ 외부 API 호출은 Facade에서 Transaction 밖에서
- ✅ 보상 Transaction은 별도 메서드로 분리
- ❌ **`@Transactional` 내 RestTemplate, WebClient, Feign 호출 금지**

### 3. Spring Proxy 제약사항 (Zero-Tolerance)

**위치**: `docs/coding_convention/03-application-layer/transaction-management/02_spring-proxy-constraints.md`

**위반 패턴**:
```java
@Service
public class OrderService {

    // ❌ Private 메서드에 @Transactional (작동 안 함)
    @Transactional
    private void createOrder(Order order) {
        orderRepository.save(order);
    }

    // ❌ 같은 클래스 내부 호출 (작동 안 함)
    public void placeOrder(PlaceOrderCommand command) {
        Order order = new Order(command);
        this.createOrder(order);  // ❌ Proxy 우회, Transaction 작동 안 함
    }
}

// ❌ Final 클래스 (Proxy 생성 불가)
@Service
public final class OrderService {
    @Transactional
    public void createOrder(Order order) {
        // Transaction 작동 안 함
    }
}
```

**올바른 패턴**:
```java
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final OrderAssembler orderAssembler;

    // ✅ Plain Java Constructor (Lombok 금지)
    public OrderService(
            OrderRepository orderRepository,
            OrderAssembler orderAssembler) {
        this.orderRepository = orderRepository;
        this.orderAssembler = orderAssembler;
    }

    // ✅ Public 메서드에만 @Transactional
    @Transactional
    public OrderResult createOrder(PlaceOrderCommand command) {
        Order order = new Order(command);
        orderRepository.save(order);
        return orderAssembler.toResult(order);
    }

    // ✅ Transaction이 필요 없는 메서드는 private
    private void validateCommand(PlaceOrderCommand command) {
        // Validation only
    }
}
```

**규칙**:
- ✅ `@Transactional`은 **Public 메서드에만** 사용
- ✅ 클래스는 **Final 금지**
- ✅ 내부 호출이 필요하면 **별도 Service로 분리**
- ❌ Private/Final/내부 호출에서 `@Transactional` 작동 안 함

### 4. Command/Query 분리

**Command (Write)**:
```java
public record PlaceOrderCommand(
    String orderNumber,
    Long customerId,
    List<OrderItemCommand> items,
    AddressCommand shippingAddress
) {
    // Validation
    public PlaceOrderCommand {
        Objects.requireNonNull(orderNumber);
        Objects.requireNonNull(customerId);
        Objects.requireNonNull(items);
        items = List.copyOf(items);
    }
}

public record OrderItemCommand(
    Long productId,
    int quantity,
    BigDecimal price
) {}
```

**Query (Read)**:
```java
public record FindOrderQuery(
    Long orderId
) {
    public FindOrderQuery {
        Objects.requireNonNull(orderId);
    }
}

public record OrderSearchCriteria(
    Long customerId,
    OrderStatus status,
    LocalDate startDate,
    LocalDate endDate,
    Pageable pageable
) {}
```

**Result (Output)**:
```java
public record OrderResult(
    Long orderId,
    String orderNumber,
    OrderStatus status,
    Long customerId,
    List<OrderItemResult> items,
    AddressResult shippingAddress,
    LocalDateTime createdAt
) {}
```

### 5. Assembler 패턴

**Assembler 역할**: DTO ↔ Domain 변환
```java
@Component
public class OrderAssembler {

    // Command → Domain
    public Order toDomain(PlaceOrderCommand command) {
        List<OrderItem> items = command.items().stream()
            .map(this::toOrderItem)
            .toList();

        Address address = toAddress(command.shippingAddress());

        return new Order(
            command.orderNumber(),
            command.customerId(),
            items,
            address
        );
    }

    // Domain → Result
    public OrderResult toResult(Order order) {
        List<OrderItemResult> items = order.getItems().stream()
            .map(this::toOrderItemResult)
            .toList();

        AddressResult address = toAddressResult(order.getShippingAddress());

        return new OrderResult(
            order.getId(),
            order.getOrderNumber(),
            order.getStatus(),
            order.getCustomerId(),
            items,
            address,
            order.getCreatedAt()
        );
    }

    // Private helper methods
    private OrderItem toOrderItem(OrderItemCommand command) {
        return new OrderItem(
            command.productId(),
            command.quantity(),
            command.price()
        );
    }

    private OrderItemResult toOrderItemResult(OrderItem item) {
        return new OrderItemResult(
            item.getProductId(),
            item.getQuantity(),
            item.getPrice()
        );
    }

    private Address toAddress(AddressCommand command) {
        return new Address(
            command.zipCode(),
            command.street(),
            command.city(),
            command.state()
        );
    }

    private AddressResult toAddressResult(Address address) {
        return new AddressResult(
            address.zipCode(),
            address.street(),
            address.city(),
            address.state()
        );
    }
}
```

### 6. Manager vs StateManager vs Facade 패턴

**아키텍처**:
```
UseCase Service → Facade → Manager → StateManager → Port(Repository)
```

**네이밍 규칙**:
- **StateManager**: `{BoundedContext}StateManager` (예: `OrderStateManager`, `PaymentStateManager`)
- **Manager**: `{BusinessFlow}Manager` (예: `OrderPaymentManager`, `OrderShippingManager`)
- **Facade**: `{Domain}Facade` (예: `OrderFulfillmentFacade`, `CheckoutFacade`)

#### StateManager 패턴

**역할**: 단일 Bounded Context(Order, Payment 등)의 상태 관리

```java
@Component
public class OrderStateManager {

    private final SaveOrderPort saveOrderPort;
    private final LoadOrderPort loadOrderPort;
    private final OrderAssembler orderAssembler;

    // ✅ Plain Java Constructor (Lombok 금지)
    public OrderStateManager(
            SaveOrderPort saveOrderPort,
            LoadOrderPort loadOrderPort,
            OrderAssembler orderAssembler) {
        this.saveOrderPort = saveOrderPort;
        this.loadOrderPort = loadOrderPort;
        this.orderAssembler = orderAssembler;
    }

    /**
     * Order 생성
     */
    @Transactional
    public Order createOrder(CreateOrderCommand command) {
        Order order = orderAssembler.toDomain(command);
        return saveOrderPort.save(order);
    }

    /**
     * Order 완료
     */
    @Transactional
    public Order completeOrder(Long orderId) {
        Order order = loadOrderPort.load(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
        order.complete();
        return saveOrderPort.save(order);
    }
}
```

#### PaymentStateManager + Outbox Pattern

```java
@Component
public class PaymentStateManager {

    private final SavePaymentPort savePaymentPort;
    private final LoadPaymentPort loadPaymentPort;
    private final OutboxStateManager outboxStateManager;
    private final PaymentAssembler paymentAssembler;

    // ✅ Plain Java Constructor
    public PaymentStateManager(
            SavePaymentPort savePaymentPort,
            LoadPaymentPort loadPaymentPort,
            OutboxStateManager outboxStateManager,
            PaymentAssembler paymentAssembler) {
        this.savePaymentPort = savePaymentPort;
        this.loadPaymentPort = loadPaymentPort;
        this.outboxStateManager = outboxStateManager;
        this.paymentAssembler = paymentAssembler;
    }

    /**
     * 외부 결제 처리 요청 (Outbox 저장)
     *
     * ✅ @Transactional: Payment + Outbox 원자성 보장
     * ❌ 외부 API 호출 없음 (Scheduler가 비동기 처리)
     */
    @Transactional
    public Payment processExternalPayment(CreatePaymentCommand command) {
        // 1. Payment 생성 (Assembler 사용)
        Payment payment = paymentAssembler.toDomain(command);
        Payment saved = savePaymentPort.save(payment);

        // 2. Outbox 저장 (동일 트랜잭션)
        outboxStateManager.createOutboxEntry(
            "PAYMENT",
            saved.getId().toString(),
            "PAYMENT_REQUESTED",
            paymentAssembler.toPayloadJson(saved)
        );

        return saved;
    }

    /**
     * 결제 상태 업데이트 (Handler에서 호출)
     */
    @Transactional
    public Payment updatePaymentStatus(Long paymentId, PaymentStatus status) {
        Payment payment = loadPaymentPort.load(paymentId)
            .orElseThrow(() -> new PaymentNotFoundException(paymentId));
        payment.updateStatus(status);
        return savePaymentPort.save(payment);
    }
}
```

#### Manager 패턴 (StateManager 오케스트레이션)

**역할**: 2-3개 StateManager 오케스트레이션

```java
@Component
public class OrderPaymentManager {

    private final OrderStateManager orderStateManager;
    private final PaymentStateManager paymentStateManager;

    // ✅ Plain Java Constructor
    public OrderPaymentManager(
            OrderStateManager orderStateManager,
            PaymentStateManager paymentStateManager) {
        this.orderStateManager = orderStateManager;
        this.paymentStateManager = paymentStateManager;
    }

    /**
     * 주문 + 결제 처리
     *
     * ✅ 각 StateManager 호출은 별도 트랜잭션
     * ✅ 외부 API 호출은 Outbox Pattern으로 비동기 처리
     */
    public OrderResult placeOrderWithPayment(PlaceOrderCommand command) {
        // 1. Order 생성 (TX1)
        Order order = orderStateManager.createOrder(command);

        // 2. Payment 요청 + Outbox 저장 (TX2)
        //    ✅ Payment PENDING + Outbox PENDING (원자성 보장)
        Payment payment = paymentStateManager.processExternalPayment(
            CreatePaymentCommand.of(order, command.paymentMethod())
        );

        // 3-5는 Scheduler가 비동기 처리:
        //    - OutboxScheduler: PENDING 엔트리 폴링
        //    - PaymentOutboxHandler: 외부 API 호출 + 상태 업데이트
        //    - Order 완료는 별도 로직 (Payment 완료 이벤트 구독)

        return new OrderResult(order, payment);
    }
}
```

#### Facade 패턴 (여러 Manager 통합)

```java
@Component
public class CheckoutFacade {

    private final OrderPaymentManager orderPaymentManager;
    private final OrderShippingManager orderShippingManager;

    // ✅ Plain Java Constructor
    public CheckoutFacade(
            OrderPaymentManager orderPaymentManager,
            OrderShippingManager orderShippingManager) {
        this.orderPaymentManager = orderPaymentManager;
        this.orderShippingManager = orderShippingManager;
    }

    /**
     * 전체 체크아웃 프로세스
     */
    public CheckoutResult processCheckout(CheckoutCommand command) {
        // 1. Order + Payment
        OrderResult orderResult = orderPaymentManager.placeOrderWithPayment(command);

        // 2. Shipping
        ShippingResult shippingResult = orderShippingManager.arrangeShipping(orderResult);

        return new CheckoutResult(orderResult, shippingResult);
    }
}
```

**Manager 사용 시점**:
- ✅ **여러 Bounded Context** 조율 (Order + Payment + Inventory)
- ✅ **복잡한 보상 Transaction** 필요
- ✅ **Saga 패턴** 구현
- ✅ **비즈니스 흐름이 5단계 이상** 복잡할 때

**Facade 사용 시점** (Manager보다 간단):
- ✅ **단일 Bounded Context** 내 여러 UseCase 조율
- ✅ **간단한 보상 Transaction**
- ✅ **비즈니스 흐름이 2-3단계**

## 패키지 구조

```
application/
├── order/
│   ├── port/
│   │   ├── in/
│   │   │   ├── PlaceOrderUseCase.java          # UseCase 인터페이스 (Command)
│   │   │   ├── CancelOrderUseCase.java
│   │   │   └── FindOrderUseCase.java           # UseCase 인터페이스 (Query)
│   │   └── out/
│   │       ├── SaveOrderPort.java              # Command Port
│   │       ├── DeleteOrderPort.java
│   │       └── LoadOrderPort.java              # Query Port
│   ├── service/
│   │   ├── PlaceOrderService.java              # UseCase 구현체 (Service 접미사)
│   │   ├── CancelOrderService.java
│   │   └── FindOrderService.java
│   ├── manager/                                # ⭐ 상태 관리 및 조율 계층
│   │   ├── OrderStateManager.java              # 단일 Bounded Context 상태 관리
│   │   ├── PaymentStateManager.java
│   │   ├── OutboxStateManager.java             # Outbox 상태 관리
│   │   └── OrderPaymentManager.java            # 2-3개 StateManager 조율
│   ├── facade/
│   │   └── CheckoutFacade.java                 # 여러 Manager 통합 (복잡한 워크플로우)
│   ├── assembler/
│   │   ├── OrderAssembler.java                 # Order DTO ↔ Domain 변환
│   │   ├── PaymentAssembler.java
│   │   └── OutboxAssembler.java
│   └── dto/
│       ├── command/
│       │   ├── PlaceOrderCommand.java          # Command DTO (Write)
│       │   ├── OrderItemCommand.java
│       │   └── CreatePaymentCommand.java
│       ├── query/
│       │   └── FindOrderQuery.java             # Query DTO (Read)
│       └── result/
│           ├── OrderResult.java                # Response DTO
│           ├── OrderItemResult.java
│           └── PaymentResult.java
├── payment/
│   ├── port/in/
│   ├── service/
│   └── ...
└── customer/
    └── ...
```

**계층 구조**:
```
UseCase Service (접미사: Service)
    ↓ 의존
Facade (복잡한 워크플로우)
    ↓ 의존
Manager (2-3개 StateManager 조율)
    ↓ 의존
StateManager (단일 Bounded Context 상태 관리)
    ↓ 의존
Port (Out)
```

**네이밍 규칙**:
- **StateManager**: `{BoundedContext}StateManager` (예: `OrderStateManager`, `PaymentStateManager`)
- **Manager**: `{BusinessFlow}Manager` (예: `OrderPaymentManager`, `CheckoutManager`)
- **Facade**: `{Domain}Facade` (예: `CheckoutFacade`, `OrderProcessingFacade`)
- **UseCase Service**: `{Action}{Aggregate}Service` (예: `PlaceOrderService`, `CancelOrderService`)

## Zero-Tolerance 체크리스트

Application Layer 코드 작성 후 반드시 확인:

### 🚨 아키텍처 규칙 (Zero-Tolerance)
- [ ] **Lombok 금지**: `@RequiredArgsConstructor`, `@Data`, `@Builder` 등 모두 금지 → Plain Java Constructor 사용
- [ ] **Assembler 필수**: 모든 DTO ↔ Domain 변환은 Assembler를 통해서만 수행
- [ ] **StateManager 사용**: 단일 Bounded Context 상태 변경은 StateManager에서
- [ ] **Port/In 인터페이스**: UseCase는 인터페이스로 정의
- [ ] **Command/Query 분리**: Write/Read DTO 완전 분리

### ⚡ Transaction 경계 규칙 (Zero-Tolerance)
- [ ] **Transaction 경계**: `@Transactional` 내 외부 API 호출 **절대 금지**
- [ ] **State 분리**: 외부 호출 전후로 상태 변경을 별도 트랜잭션으로 분리
- [ ] **Outbox Pattern**: 외부 API 호출 필요 시 Outbox + Scheduler 패턴 사용
- [ ] **Scheduler 규칙**: OutboxScheduler의 `pollOutbox()`에 `@Transactional` 금지

### 🔧 Spring Proxy 제약사항 (Zero-Tolerance)
- [ ] **Public Only**: Public 메서드에만 `@Transactional` 사용 (Private/Protected 금지)
- [ ] **내부 호출 금지**: 같은 클래스 내 `@Transactional` 메서드 호출 없음 (`this.method()` 금지)
- [ ] **Final 금지**: Service/Manager/StateManager 클래스는 Final 아님

### 📝 네이밍 규칙
- [ ] **UseCase Method**: Command는 `execute{Aggregate}{Action}()`, Query는 `query{Aggregate}By{Condition}()`
- [ ] **UseCase Service**: 구현체는 `Service` 접미사 필수 (예: `PlaceOrderService`)
- [ ] **StateManager**: `{BoundedContext}StateManager` (