Two-Step Query Pattern 적용하기

Soyun_p·2025년 4월 6일
0

📖 지식

목록 보기
7/10
post-thumbnail

🐾 Left Join FetchPage를 함께 사용할 때 발생하는 문제와 해결법

Left Join FetchPage를 함께 사용하면 다음과 같은 Hibernate 경고를 보게 된다

"HHH000104: firstResult/maxResults specified with collection fetch"

이 오류는 페이징(firstResult / maxResults)컬렉션 페치 조인(collection fetch join)동시에 사용할 때 발생한다


✅ 해결 방법

  1. @EntityGraph 어노테이션 사용
  2. Two-Step Query Pattern 사용 ✅ (내가 사용한 방법!)

📌 Two-Step Query Pattern이란?

두 단계 쿼리 방식으로, 페이징과 Fetch Join을 분리해 Hibernate 경고를 회피하는 전략이다


📌 동작 방식

  1. 첫 번째 쿼리: 필요한 ID 목록을 페이징 처리하여 조회
  2. 두 번째 쿼리: 조회한 ID 목록을 기준으로 연관 데이터를 JOIN FETCH로 가져옴

🪼 예시 시나리오: 사용자가 작성한 게시글 목록 조회

1️⃣ 1차 쿼리: 게시글 ID 조회

SELECT id FROM posts WHERE user_id = 1;

2️⃣ 2차 쿼리: 게시글 상세 정보 조회

SELECT * FROM posts WHERE id IN (1,2,3...);

💡 복잡한 조인을 피하고, 필요한 경우에만 데이터를 로딩함으로써 성능을 최적화할 수 있다.


📌 Two-Step Query Pattern이 유용한 경우

  • 대용량 데이터 처리
  • Lazy Loading 대체
  • 보안 필터링 후 안전한 데이터 접근

⚠️ 단점

  • 두 번의 쿼리로 인해 DB 요청 수 증가
  • 네트워크 지연 발생 시 성능 저하 가능성

🧠 에러 회피 원리

Two-Step Query Pattern은 다음과 같이 작동하여 Hibernate 오류를 피한다:

  1. 첫 번째 쿼리: 페이징된 엔티티 ID 조회 (컬렉션 조인 없음)
  2. 두 번째 쿼리: ID 목록 기준으로 JOIN FETCH 실행

👉 페이징과 Fetch Join을 직접 결합하지 않기 때문에 안전하다.


🪼 실전 코드 흐름

1️⃣ 첫 번째 쿼리 – 페이징 ID 조회

@Query("SELECT o.id FROM Order o " +
       "WHERE o.userId = :userId " +
       "AND (:period = 'all' OR (o.createdAt >= :startDate AND o.createdAt <= :endDate))")
Page<Long> findOrderIdsByPeriod(@Param("userId") Long userId,
                                @Param("period") String period,
                                @Param("startDate") LocalDateTime startDate,
                                @Param("endDate") LocalDateTime endDate,
                                Pageable pageable);
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());

Page<Long> orderIdsPage = orderRepository.findOrderIdsByPeriod(
    user.getId(), period, startDate, today, pageable
);

📌 장점:

  • 복잡한 JOIN FETCH를 피할 수 있음
  • 메모리 사용량 감소
  • 정렬과 페이징 성능 최적화

2️⃣ 두 번째 쿼리 – 상세 데이터 조회

@Query("SELECT DISTINCT o FROM Order o " +
       "LEFT JOIN FETCH o.orderDetails od " +
       "LEFT JOIN FETCH od.saleProduct sp " +
       "LEFT JOIN FETCH sp.option " +
       "LEFT JOIN FETCH sp.product " +
       "LEFT JOIN FETCH sp.stock " +
       "WHERE o.id IN :ids " +
       "ORDER BY o.createdAt DESC")
List<Order> findOrdersWithDetailsByIds(@Param("ids") List<Long> ids);
List<Long> orderIds = orderIdsPage.getContent();
List<Order> orders = orderRepository.findOrdersWithDetailsByIds(orderIds);

📌 장점:

  • 연관 엔티티를 한 번에 로딩해 N+1 문제 해결
  • 데이터 중복 제거 (DISTINCT 사용)
  • 성능과 응답 속도 향상

🎯 왜 써야 할까?

목적설명
✅ 성능 최적화페이징 쿼리와 Fetch Join 분리로 효율적
✅ N+1 문제 방지연관 엔티티를 한 번에 조회
✅ 안정성 확보Hibernate 오류 회피
✅ 대규모 데이터 처리대용량에서도 효율적인 데이터 조회 가능

0개의 댓글