Skip to the content.

Command DTO Guide — 상태 변경 요청 DTO

Command DTO는 HTTP 요청을 받아 상태 변경 작업(POST, PUT, PATCH, DELETE)을 수행하는 DTO입니다.

API Layer → Application Layer 변환 전용, Domain 변환 금지.


1) 핵심 원칙

금지사항


2) 네이밍 규칙

기본 원칙

접미사: *ApiRequest 접두사: 유비쿼터스 언어로 표현된 액션 동사 또는 비즈니스 행위

네이밍 가이드

HTTP 메서드 액션 동사 예시 구체 예시
POST Create, Register, Place, Enroll CreateOrderApiRequest, PlaceOrderApiRequest
PUT/PATCH Update, Modify, Change, Adjust UpdateOrderStatusApiRequest, ChangeShippingAddressApiRequest
POST/PATCH Cancel, Confirm, Approve, Reject, Complete CancelOrderApiRequest, ApproveOrderApiRequest

핵심:


3) 기본 패턴

단순 Command (Flat Structure)

/**
 * Order 상태 변경 요청
 *
 * @param orderId 주문 ID
 * @param status 변경할 상태
 * @author ryu-qqq
 * @since 2025-11-13
 */
public record UpdateOrderStatusApiRequest(
    @NotNull(message = "주문 ID는 필수입니다")
    Long orderId,

    @NotBlank(message = "상태는 필수입니다")
    @Pattern(regexp = "PLACED|CONFIRMED|SHIPPED|DELIVERED|CANCELLED", message = "유효하지 않은 상태입니다")
    String status
) {
    // ✅ Compact Constructor (추가 검증)
    public UpdateOrderStatusApiRequest {
        if (orderId != null && orderId <= 0) {
            throw new IllegalArgumentException("유효하지 않은 주문 ID: " + orderId);
        }
    }
}

복잡한 Command (Nested Record)

/**
 * Order 생성 요청
 *
 * @param customerId 고객 ID
 * @param items 주문 항목 목록
 * @param shippingAddress 배송 주소
 * @author ryu-qqq
 * @since 2025-11-13
 */
public record CreateOrderApiRequest(
    @NotNull(message = "고객 ID는 필수입니다")
    Long customerId,

    @NotEmpty(message = "주문 항목은 필수입니다")
    @Valid
    List<OrderItemRequest> items,

    @NotNull(message = "배송 주소는 필수입니다")
    @Valid
    AddressRequest shippingAddress
) {
    // ✅ Compact Constructor
    public CreateOrderApiRequest {
        if (customerId != null && customerId <= 0) {
            throw new IllegalArgumentException("유효하지 않은 고객 ID: " + customerId);
        }
        // 불변 리스트로 방어적 복사
        items = items == null ? List.of() : List.copyOf(items);
    }

    /**
     * Order 항목 요청
     */
    public record OrderItemRequest(
        @NotNull(message = "상품 ID는 필수입니다")
        Long productId,

        @NotNull(message = "수량은 필수입니다")
        @Min(value = 1, message = "수량은 1 이상이어야 합니다")
        Integer quantity,

        @NotNull(message = "가격은 필수입니다")
        @Min(value = 0, message = "가격은 0 이상이어야 합니다")
        Long price
    ) {}

    /**
     * 배송 주소 요청
     */
    public record AddressRequest(
        @NotBlank(message = "우편번호는 필수입니다")
        String zipCode,

        @NotBlank(message = "주소는 필수입니다")
        String address,

        String addressDetail
    ) {}
}

4) Bean Validation 규칙

필수 검증

public record CreateOrderApiRequest(
    @NotNull(message = "필수입니다")       // null 체크
    Long customerId,

    @NotBlank(message = "필수입니다")      // null, 공백 체크
    String customerName,

    @NotEmpty(message = "필수입니다")      // null, empty 체크
    List<OrderItemRequest> items
) {}

값 범위 검증

public record CreateOrderApiRequest(
    @Min(value = 1, message = "1 이상")
    @Max(value = 100, message = "100 이하")
    Integer quantity,

    @Positive(message = "양수여야 함")
    Long price,

    @PositiveOrZero(message = "0 이상")
    Long discount
) {}

패턴 검증

public record CreateOrderApiRequest(
    @Pattern(regexp = "^[A-Z]{2}[0-9]{10}$", message = "유효하지 않은 형식")
    String orderNumber,

    @Email(message = "유효하지 않은 이메일")
    String email,

    @Size(min = 10, max = 11, message = "10-11자리")
    String phoneNumber
) {}

Nested Record 검증

public record CreateOrderApiRequest(
    @NotNull @Valid              // ✅ @Valid 필수
    AddressRequest shippingAddress,

    @NotEmpty @Valid             // ✅ 컬렉션 내부도 검증
    List<OrderItemRequest> items
) {}

5) Compact Constructor 활용

추가 검증 로직

public record CreateOrderApiRequest(
    Long customerId,
    List<OrderItemRequest> items
) {
    public CreateOrderApiRequest {
        // ✅ null 방어
        if (customerId != null && customerId <= 0) {
            throw new IllegalArgumentException("유효하지 않은 고객 ID");
        }

        // ✅ 불변 리스트 변환
        items = items == null ? List.of() : List.copyOf(items);

        // ✅ 비즈니스 룰 검증 (간단한 것만)
        if (items.size() > 100) {
            throw new IllegalArgumentException("최대 100개까지 가능");
        }
    }
}

기본값 설정

public record CreateOrderApiRequest(
    Long customerId,
    String memo
) {
    public CreateOrderApiRequest {
        // ✅ 기본값 설정
        memo = memo == null ? "" : memo.trim();
    }
}

6) Do / Don’t

✅ Good

// ✅ Good: Record, Bean Validation, Compact Constructor
public record CreateOrderApiRequest(
    @NotNull Long customerId,
    @NotEmpty @Valid List<OrderItemRequest> items
) {
    public CreateOrderApiRequest {
        items = List.copyOf(items);
    }

    public record OrderItemRequest(
        @NotNull Long productId,
        @Min(1) Integer quantity
    ) {}
}

❌ Bad

// ❌ Bad: Lombok 사용
@Data
@Builder
public class CreateOrderApiRequest {
    private Long customerId;
    private List<OrderItemRequest> items;
}

// ❌ Bad: Domain 변환 메서드 포함
public record CreateOrderApiRequest(...) {
    public Order toDomain() {  // ❌ Mapper/Assembler 책임
        return Order.forNew(...);
    }
}

// ❌ Bad: Jackson 어노테이션
public record CreateOrderApiRequest(
    @JsonProperty("customer_id")  // ❌ 금지
    Long customerId
) {}

// ❌ Bad: 비즈니스 로직
public record CreateOrderApiRequest(...) {
    public boolean isVipOrder() {  // ❌ Domain 책임
        return totalAmount > 100000;
    }
}

7) 변환 흐름

[HTTP Request JSON]
    ↓
CreateOrderApiRequest (Command DTO)
    ↓
OrderApiMapper.toCommand()  ← @Component DI
    ↓
CreateOrderCommand (UseCase DTO)
    ↓
OrderAssembler.toDomain()  ← Application Layer
    ↓
Order (Domain Aggregate)

중요:


8) ArchUnit 검증

Command DTO의 아키텍처 규칙 자동 검증 (빌드 시 실행)은 별도 가이드를 참고하세요:

👉 Command DTO ArchUnit Guide

주요 검증 규칙 (10개):


9) 체크리스트


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