Skip to the content.

Query DTO Guide — 조회 조건 요청 DTO

Query DTO는 HTTP GET 요청의 조회 조건을 담는 DTO입니다.

검색 필터, 페이징, 정렬 조건 등을 표현하며, Application Layer의 Query DTO로 변환됩니다.


1) 핵심 원칙

금지사항


2) 네이밍 규칙

패턴 접미사 예시 용도
검색 *SearchApiRequest OrderSearchApiRequest 복합 조건 검색
목록 *ListApiRequest OrderListApiRequest 목록 조회 (필터)
상세 *DetailApiRequest OrderDetailApiRequest 단건 조회 (조건 있을 때)
통계 *StatsApiRequest OrderStatsApiRequest 집계/통계 조회

핵심: 조회 의도(Search, List, Detail 등)가 명확히 드러나야 함.


3) 기본 패턴

단순 검색 (Flat Structure)

/**
 * Order 검색 요청
 *
 * @param customerId 고객 ID (Optional)
 * @param status 주문 상태 (Optional)
 * @param startDate 시작 날짜 (Optional)
 * @param endDate 종료 날짜 (Optional)
 * @param page 페이지 번호 (0부터 시작)
 * @param size 페이지 크기
 * @author ryu-qqq
 * @since 2025-11-13
 */
public record OrderSearchApiRequest(
    // ✅ Optional 필드 (null 허용)
    Long customerId,
    String status,
    LocalDate startDate,
    LocalDate endDate,

    // ✅ 페이징 필수 (기본값 제공)
    @Min(value = 0, message = "페이지 번호는 0 이상")
    Integer page,

    @Min(value = 1, message = "페이지 크기는 1 이상")
    @Max(value = 100, message = "페이지 크기는 100 이하")
    Integer size
) {
    // ✅ Compact Constructor (기본값 설정)
    public OrderSearchApiRequest {
        page = page == null ? 0 : page;
        size = size == null ? 20 : size;

        // 날짜 범위 검증
        if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
            throw new IllegalArgumentException("시작 날짜는 종료 날짜보다 이전이어야 합니다");
        }
    }
}

복잡한 검색 (Nested Record)

/**
 * Order 고급 검색 요청
 *
 * @param filter 검색 필터
 * @param sort 정렬 조건
 * @param page 페이징 조건
 * @author ryu-qqq
 * @since 2025-11-13
 */
public record OrderSearchApiRequest(
    FilterRequest filter,
    SortRequest sort,

    @NotNull(message = "페이징 조건은 필수입니다")
    @Valid
    PageRequest page
) {
    public OrderSearchApiRequest {
        filter = filter == null ? new FilterRequest(null, null, null, null) : filter;
        sort = sort == null ? new SortRequest("createdAt", "DESC") : sort;
    }

    /**
     * 검색 필터
     */
    public record FilterRequest(
        Long customerId,
        String status,
        LocalDate startDate,
        LocalDate endDate
    ) {}

    /**
     * 정렬 조건
     */
    public record SortRequest(
        @NotBlank(message = "정렬 필드는 필수입니다")
        String field,

        @Pattern(regexp = "ASC|DESC", message = "정렬 방향은 ASC 또는 DESC")
        String direction
    ) {
        public SortRequest {
            field = field == null ? "createdAt" : field;
            direction = direction == null ? "DESC" : direction.toUpperCase();
        }
    }

    /**
     * 페이징 조건
     */
    public record PageRequest(
        @Min(value = 0, message = "페이지 번호는 0 이상")
        Integer page,

        @Min(value = 1, message = "페이지 크기는 1 이상")
        @Max(value = 100, message = "페이지 크기는 100 이하")
        Integer size
    ) {
        public PageRequest {
            page = page == null ? 0 : page;
            size = size == null ? 20 : size;
        }
    }
}

4) 페이징 패턴

기본 페이징

public record OrderListApiRequest(
    Long customerId,

    @Min(0) Integer page,
    @Min(1) @Max(100) Integer size
) {
    public OrderListApiRequest {
        page = page == null ? 0 : page;
        size = size == null ? 20 : size;
    }
}

Cursor 기반 페이징

public record OrderListApiRequest(
    Long customerId,
    String cursor,  // ✅ 다음 페이지 커서 (Optional)

    @Min(1) @Max(100) Integer size
) {
    public OrderListApiRequest {
        size = size == null ? 20 : size;
    }
}

5) 날짜/시간 조건 패턴

기간 조회

public record OrderSearchApiRequest(
    LocalDate startDate,  // ✅ Optional
    LocalDate endDate     // ✅ Optional
) {
    public OrderSearchApiRequest {
        // 날짜 범위 검증
        if (startDate != null && endDate != null && startDate.isAfter(endDate)) {
            throw new IllegalArgumentException("시작 날짜는 종료 날짜보다 이전이어야 합니다");
        }
    }
}

시간 포함

public record OrderSearchApiRequest(
    LocalDateTime startDateTime,  // ✅ Optional
    LocalDateTime endDateTime     // ✅ Optional
) {
    public OrderSearchApiRequest {
        if (startDateTime != null && endDateTime != null && startDateTime.isAfter(endDateTime)) {
            throw new IllegalArgumentException("시작 시간은 종료 시간보다 이전이어야 합니다");
        }
    }
}

6) 검색 필터 패턴

Enum 필터

public record OrderSearchApiRequest(
    @Pattern(regexp = "PLACED|CONFIRMED|SHIPPED|DELIVERED|CANCELLED")
    String status  // ✅ Optional, Enum String
) {}

범위 필터

public record OrderSearchApiRequest(
    @Min(0) Long minPrice,  // ✅ Optional
    @Min(0) Long maxPrice   // ✅ Optional
) {
    public OrderSearchApiRequest {
        if (minPrice != null && maxPrice != null && minPrice > maxPrice) {
            throw new IllegalArgumentException("최소 가격은 최대 가격보다 작아야 합니다");
        }
    }
}

텍스트 검색

public record OrderSearchApiRequest(
    String keyword,  // ✅ Optional, 부분 검색

    @Pattern(regexp = "ORDER_NUMBER|CUSTOMER_NAME|PRODUCT_NAME")
    String searchField  // ✅ 검색 대상 필드
) {
    public OrderSearchApiRequest {
        keyword = keyword == null ? null : keyword.trim();
        searchField = searchField == null ? "ORDER_NUMBER" : searchField;
    }
}

7) Do / Don’t

✅ Good

// ✅ Good: Record, Optional 필드, 기본값, 페이징
public record OrderSearchApiRequest(
    Long customerId,        // Optional
    String status,          // Optional
    @Min(0) Integer page,   // 필수 (기본값)
    @Min(1) @Max(100) Integer size
) {
    public OrderSearchApiRequest {
        page = page == null ? 0 : page;
        size = size == null ? 20 : size;
    }
}

❌ Bad

// ❌ Bad: Lombok 사용
@Data
@Builder
public class OrderSearchApiRequest {
    private Long customerId;
    private Integer page;
}

// ❌ Bad: 모든 필드 @NotNull (조회 조건은 Optional)
public record OrderSearchApiRequest(
    @NotNull Long customerId,  // ❌ 조회 조건은 Optional
    @NotNull String status
) {}

// ❌ Bad: Domain 변환 메서드
public record OrderSearchApiRequest(...) {
    public OrderSearchCriteria toCriteria() {  // ❌ Mapper 책임
        return new OrderSearchCriteria(...);
    }
}

// ❌ Bad: 페이징 없음
public record OrderSearchApiRequest(
    Long customerId,
    String status
    // ❌ 페이징 필수!
) {}

8) 변환 흐름

[HTTP GET Query Params]
    ↓
OrderSearchApiRequest (Query DTO)
    ↓
OrderApiMapper.toQuery()  ← @Component DI
    ↓
OrderSearchQuery (UseCase DTO)
    ↓
OrderQueryAdapter.findByCriteria()  ← Persistence Layer
    ↓
List<Order> (Domain Aggregate)

중요:


9) 테스트

Query DTO의 단위 테스트 가이드는 별도 문서를 참고하세요:

👉 Query DTO Test Guide

주요 테스트 항목:


10) ArchUnit 검증

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

👉 Query DTO ArchUnit Guide

주요 검증 규칙 (10개):


11) 체크리스트


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