Skip to the content.

Read Manager Guide — 단일 Port 조회

ReadManager는 단일 Query Port를 통해 조회를 수행합니다.

UseCase 또는 QueryFacade에서 호출합니다.


1) 핵심 역할


2) 필수 규칙 (Zero-Tolerance)

규칙 설명
단일 Query Port 의존 2개 이상이면 QueryFacade로
@Component 어노테이션 @Service 아님
@Transactional(readOnly=true) 메서드 단위 읽기 전용 트랜잭션
find*() / get*() 메서드 네이밍 persist/save 등 금지
영속화 작업 금지 TransactionManager 책임
비즈니스 로직 금지 Domain Layer 책임
Lombok 금지 생성자 직접 작성

3) 패키지 구조

application/{bc}/
├─ manager/
│  ├─ command/
│  │  └─ {Bc}TransactionManager.java  ← Command용 (persist)
│  └─ query/
│     └─ {Bc}ReadManager.java         ← Query용 (find) - 이 문서
└─ port/
   └─ out/
      └─ query/
         └─ {Bc}QueryPort.java        ← ReadManager가 사용

4) TransactionManager vs ReadManager

구분 TransactionManager ReadManager
역할 영속화 (저장/수정/삭제) 조회
의존성 Command Port 1개 Query Port 1개
트랜잭션 @Transactional @Transactional(readOnly=true)
메서드 persist() findBy*(), get*()
위치 manager/command/ manager/query/

5) 구현 예시

기본 구조

package com.ryuqq.application.order.manager.query;

import com.ryuqq.application.port.out.query.OrderQueryPort;
import com.ryuqq.domain.order.aggregate.Order;
import com.ryuqq.domain.order.criteria.OrderSearchCriteria;
import com.ryuqq.domain.order.vo.CustomerId;
import com.ryuqq.domain.order.vo.OrderId;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

/**
 * Order Read Manager
 * - 단일 Query Port 조회
 * - 읽기 전용 트랜잭션
 */
@Component
public class OrderReadManager {

    private final OrderQueryPort orderQueryPort;

    public OrderReadManager(OrderQueryPort orderQueryPort) {
        this.orderQueryPort = orderQueryPort;
    }

    /**
     * ID로 단건 조회
     */
    @Transactional(readOnly = true)
    public Order findById(OrderId orderId) {
        return orderQueryPort.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
    }

    /**
     * ID로 Optional 조회
     */
    @Transactional(readOnly = true)
    public Optional<Order> findByIdOptional(OrderId orderId) {
        return orderQueryPort.findById(orderId);
    }

    /**
     * 고객 ID로 목록 조회
     */
    @Transactional(readOnly = true)
    public List<Order> findByCustomerId(CustomerId customerId) {
        return orderQueryPort.findByCustomerId(customerId);
    }

    /**
     * Criteria 기반 검색
     */
    @Transactional(readOnly = true)
    public List<Order> findBy(OrderSearchCriteria criteria) {
        return orderQueryPort.findBy(criteria);
    }

    /**
     * Criteria 기반 페이징 검색
     */
    @Transactional(readOnly = true)
    public Page<Order> findPageBy(OrderSearchCriteria criteria) {
        return orderQueryPort.findPageBy(criteria);
    }

    /**
     * 존재 여부 확인
     */
    @Transactional(readOnly = true)
    public boolean existsById(OrderId orderId) {
        return orderQueryPort.existsById(orderId);
    }

    /**
     * 개수 조회
     */
    @Transactional(readOnly = true)
    public long countBy(OrderSearchCriteria criteria) {
        return orderQueryPort.countBy(criteria);
    }
}

통계 조회 예시

/**
 * 통계 조회
 */
@Transactional(readOnly = true)
public OrderStatistics getStatistics(DateRange dateRange) {
    return orderQueryPort.getStatistics(dateRange);
}

/**
 * 집계 조회
 */
@Transactional(readOnly = true)
public List<OrderSummary> getSummaryByStatus(CustomerId customerId) {
    return orderQueryPort.getSummaryByStatus(customerId);
}

/**
 * 최근 N개 조회
 */
@Transactional(readOnly = true)
public List<Order> findRecent(int limit) {
    return orderQueryPort.findRecent(limit);
}

6) 메서드 네이밍 규칙

패턴 용도 반환 타입
findById() 단건 조회 (필수) Domain / throw Exception
findByIdOptional() 단건 조회 (선택) Optional<Domain>
findBy*() 조건 조회 List<Domain>
findPageBy() 페이징 조회 Page<Domain>
existsById() 존재 여부 boolean
countBy() 개수 long
get*() 통계/집계 Statistics, Summary 등

7) Do / Don’t

✅ Good

// ✅ Good: @Component 어노테이션
@Component
public class OrderReadManager { ... }

// ✅ Good: 단일 Query Port 의존
@Component
public class OrderReadManager {
    private final OrderQueryPort orderQueryPort;
}

// ✅ Good: @Transactional(readOnly = true) 메서드 단위
@Transactional(readOnly = true)
public Order findById(OrderId orderId) { ... }

// ✅ Good: find*/get* 메서드 네이밍
public Order findById(OrderId orderId) { ... }
public List<Order> findByCustomerId(CustomerId customerId) { ... }
public OrderStatistics getStatistics(DateRange range) { ... }

// ✅ Good: Criteria 기반 조회
public List<Order> findBy(OrderSearchCriteria criteria) {
    return orderQueryPort.findBy(criteria);
}

❌ Bad

// ❌ Bad: @Service 어노테이션
@Service
public class OrderReadManager { ... }

// ❌ Bad: @Transactional 클래스 레벨
@Component
@Transactional(readOnly = true)
public class OrderReadManager { ... }

// ❌ Bad: 2개 이상 Port 의존
@Component
public class OrderReadManager {
    private final OrderQueryPort orderQueryPort;
    private final CustomerQueryPort customerQueryPort;  // ❌ QueryFacade로!
}

// ❌ Bad: persist/save 메서드
public Order persist(Order order) { ... }  // ❌ TransactionManager 역할

// ❌ Bad: 쓰기 작업
@Transactional(readOnly = true)
public Order updateStatus(OrderId orderId, OrderStatus status) {  // ❌
    Order order = findById(orderId);
    order.updateStatus(status);  // ❌ 쓰기 작업
    return order;
}

// ❌ Bad: 비즈니스 로직 포함
@Transactional(readOnly = true)
public Order findById(OrderId orderId) {
    Order order = orderQueryPort.findById(orderId)
        .orElseThrow(...);
    if (order.isExpired()) {  // ❌ 비즈니스 로직
        throw new OrderExpiredException();
    }
    return order;
}

// ❌ Bad: readOnly=true 누락
@Transactional  // ❌ readOnly = true 필요
public Order findById(OrderId orderId) { ... }

8) 체크리스트


9) 관련 문서


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