Skip to the content.

Query UseCase (Port-In) — 조회 추상화

Query UseCase는 조회(Read)를 추상화하는 Inbound Port입니다.

Query, Response별도 DTO 패키지로 분리하여 관리합니다.


1) 핵심 역할


2) 핵심 원칙

원칙 1: DTO 패키지 분리

원칙 2: 단일 조회 책임

원칙 3: Assembler 사용

원칙 4: 읽기 전용 Transaction

원칙 5: Domain 노출 금지


3) 패키지 구조

application/order/
├── dto/
│   ├── query/
│   │   ├── GetOrderQuery.java
│   │   └── SearchOrdersQuery.java
│   └── response/
│       ├── OrderDetailResponse.java
│       └── OrderSummaryResponse.java
└── port/
    └── in/
        └── query/
             ├── GetOrderUseCase.java
             └── SearchOrdersUseCase.java

4) 템플릿 코드

Query DTO

UseCase Interface

package com.ryuqq.application.{bc}.port.in;

import com.ryuqq.application.{bc}.dto.query.Get{Bc}Query;
import com.ryuqq.application.{bc}.dto.response.{Bc}DetailResponse;

/**
 * Get {Bc} UseCase (Query)
 *
 * <p>조회를 담당하는 Inbound Port</p>
 *
 * @author development-team
 * @since 1.0.0
 */
public interface Get{Bc}UseCase {

    /**
     * {Bc} 조회
     *
     * @param query 조회 조건
     * @return 조회 결과
     */
    {Bc}DetailResponse execute(Get{Bc}Query query);
}

5) 실전 예시 (GetOrder)

UseCase Interface

package com.ryuqq.application.order.port.in;

import com.ryuqq.application.order.dto.query.GetOrderQuery;
import com.ryuqq.application.order.dto.response.OrderDetailResponse;

/**
 * Get Order UseCase (Query)
 *
 * <p>주문 조회를 담당하는 Inbound Port</p>
 *
 * @author development-team
 * @since 1.0.0
 */
public interface GetOrderUseCase {

    /**
     * 주문 조회
     *
     * @param query 조회 조건
     * @return 주문 상세 정보
     */
    OrderDetailResponse execute(GetOrderQuery query);
}

UseCase Interface (Pagination)

package com.ryuqq.application.order.port.in.query;

import com.ryuqq.application.order.dto.query.SearchOrdersQuery;
import com.ryuqq.application.order.dto.response.OrderPageResponse;

/**
 * Search Orders UseCase (Query)
 *
 * <p>주문 목록 페이징 조회를 담당하는 Inbound Port</p>
 *
 * <p><strong>필수 규칙:</strong> Search*UseCase는 반드시
 * {@code *PageResponse} 또는 {@code *SliceResponse}를 반환해야 합니다.</p>
 *
 * @author development-team
 * @since 1.0.0
 */
public interface SearchOrdersUseCase {

    /**
     * 주문 목록 페이징 조회
     *
     * @param query 검색 조건 (page, size 포함)
     * @return 페이징된 주문 목록
     */
    OrderPageResponse execute(SearchOrdersQuery query);
}

⚠️ Zero-Tolerance: Search*UseCase는 반드시 *PageResponse 또는 *SliceResponse를 반환해야 합니다. List<T> 반환은 ArchUnit 테스트 실패의 원인이 됩니다.


6) Pagination 패턴

목적: 대량 데이터 조회 시 효율적인 페이징 처리

Pagination 유형 선택 가이드

패턴 사용 시기 특징
PageResponse 전체 개수가 필요한 경우 (관리자 페이지) 총 페이지 수, 총 건수 제공
SliceResponse 무한 스크롤 (다음 페이지 존재 여부만 필요) COUNT 쿼리 생략, 성능 우수
CursorResponse 실시간 데이터, 대용량 처리 일관된 결과, 중복/누락 방지

PageResponse UseCase (Offset 기반)

package com.ryuqq.application.{bc}.port.in.query;

import com.ryuqq.application.{bc}.dto.query.Search{Bc}Query;
import com.ryuqq.application.{bc}.dto.response.{Bc}SummaryResponse;
import com.ryuqq.application.common.dto.PageResponse;

/**
 * Search {Bc} UseCase (Pagination)
 *
 * <p>페이징 조회를 담당하는 Inbound Port</p>
 *
 * @author development-team
 * @since 1.0.0
 */
public interface Search{Bc}UseCase {

    /**
     * {Bc} 목록 페이징 조회
     *
     * @param query 검색 조건 (page, size 포함)
     * @return 페이징된 결과 (총 개수, 총 페이지 포함)
     */
    PageResponse<{Bc}SummaryResponse> execute(Search{Bc}Query query);
}

SliceResponse UseCase (무한 스크롤)

package com.ryuqq.application.{bc}.port.in.query;

import com.ryuqq.application.{bc}.dto.query.Search{Bc}Query;
import com.ryuqq.application.{bc}.dto.response.{Bc}SummaryResponse;
import com.ryuqq.application.common.dto.SliceResponse;

/**
 * Search {Bc} UseCase (Infinite Scroll)
 *
 * <p>무한 스크롤 조회를 담당하는 Inbound Port</p>
 * <p>COUNT 쿼리를 생략하여 성능 최적화</p>
 *
 * @author development-team
 * @since 1.0.0
 */
public interface Search{Bc}UseCase {

    /**
     * {Bc} 목록 조회 (무한 스크롤)
     *
     * @param query 검색 조건 (page, size 포함)
     * @return 슬라이스 결과 (hasNext 여부 포함)
     */
    SliceResponse<{Bc}SummaryResponse> execute(Search{Bc}Query query);
}

CursorResponse UseCase (커서 기반)

package com.ryuqq.application.{bc}.port.in.query;

import com.ryuqq.application.{bc}.dto.query.Search{Bc}CursorQuery;
import com.ryuqq.application.{bc}.dto.response.{Bc}SummaryResponse;
import com.ryuqq.application.common.dto.CursorResponse;

/**
 * Search {Bc} UseCase (Cursor-based)
 *
 * <p>커서 기반 조회를 담당하는 Inbound Port</p>
 * <p>실시간 데이터, 대용량 처리에 적합</p>
 *
 * @author development-team
 * @since 1.0.0
 */
public interface Search{Bc}UseCase {

    /**
     * {Bc} 목록 조회 (커서 기반)
     *
     * @param query 검색 조건 (cursor, size 포함)
     * @return 커서 결과 (nextCursor 포함)
     */
    CursorResponse<{Bc}SummaryResponse> execute(Search{Bc}CursorQuery query);
}

Pagination Response DTO 필드 규칙 (Zero-Tolerance)

⚠️ ArchUnit 강제: 아래 필드 패턴을 따르지 않으면 빌드가 실패합니다.

Response 타입 필수 필드
*PageResponse content, page, size, totalElements, totalPages, first, last
*SliceResponse content, size, hasNext
// application/common/dto/response/PageResponse.java
public record PageResponse<T>(
    List<T> content,       // 데이터 목록 (필수)
    int page,              // 현재 페이지 번호 (필수)
    int size,              // 페이지 크기 (필수)
    long totalElements,    // 전체 요소 수 (필수)
    int totalPages,        // 전체 페이지 수 (필수)
    boolean first,         // 첫 페이지 여부 (필수)
    boolean last           // 마지막 페이지 여부 (필수)
) {
    public PageResponse {
        content = content != null ? List.copyOf(content) : List.of();
    }
}

// application/common/dto/response/SliceResponse.java
public record SliceResponse<T>(
    List<T> content,       // 데이터 목록 (필수)
    int size,              // 페이지 크기 (필수)
    boolean hasNext,       // 다음 페이지 존재 여부 (필수)
    String nextCursor      // 다음 커서 (선택)
) {
    public SliceResponse {
        content = content != null ? List.copyOf(content) : List.of();
    }
}

// application/common/dto/CursorResponse.java
public record CursorResponse<T>(
    List<T> content,
    String nextCursor,
    boolean hasNext,
    int size
) {
    public static <T> CursorResponse<T> of(List<T> content, String nextCursor, int size) {
        return new CursorResponse<>(content, nextCursor, nextCursor != null, size);
    }
}

Pagination 선택 기준:


7) Do / Don’t

❌ Bad Examples

// ❌ UseCase 내부에 Query/Response Record 정의
public interface GetOrderUseCase {
    Response execute(Query query);
    
    record Query(...) {}  // 금지!
    record Response(...) {}  // 금지!
}

// ❌ 여러 조회를 하나의 UseCase에
public interface OrderQueryUseCase {
    OrderDetailResponse getOrder(Long id);  // 금지!
    List<OrderSummaryResponse> searchOrders(SearchOrdersQuery query);  // 금지!
}

// ❌ Domain Entity 직접 반환
public interface GetOrderUseCase {
    Order execute(GetOrderQuery query);  // 금지! Domain 노출
}

// ❌ UseCase 인터페이스에 @Transactional
@Transactional(readOnly = true)  // 금지!
public interface GetOrderUseCase {
    OrderDetailResponse execute(GetOrderQuery query);
}

// ❌ readOnly 없는 Transaction
@Service
@Transactional  // 금지! readOnly = true 필수
public class GetOrderService implements GetOrderUseCase {
    // ...
}

✅ Good Examples

// ✅ 별도 DTO 패키지
// dto/query/GetOrderQuery.java
public record GetOrderQuery(...) {}

// dto/response/OrderDetailResponse.java
public record OrderDetailResponse(...) {}

// port/in/GetOrderUseCase.java
public interface GetOrderUseCase {
    OrderDetailResponse execute(GetOrderQuery query);
}

// ✅ 단일 조회 책임
public interface GetOrderUseCase {
    OrderDetailResponse execute(GetOrderQuery query);
}

// ✅ Search UseCase는 PageResponse 또는 SliceResponse 반환 필수
public interface SearchOrdersUseCase {
    OrderPageResponse execute(SearchOrdersQuery query);
}


8) 체크리스트

Query UseCase 작성 시:

Search UseCase 필수 규칙 (Zero-Tolerance):

Pagination 유형 선택:


📖 관련 문서


작성자: Development Team 최종 수정일: 2026-01-05 버전: 2.2.0 (Search UseCase 반환타입 규칙 Zero-Tolerance 추가)