개발인생/Project

프로젝트 코드 리뷰: JPQL vs QueryDSL 최적화 및 리팩토링

forri 2025. 2. 21. 23:20

 

ERPre v2 코드 리팩토링

JPQL → QueryDSL 로 리팩토링 해보려고 한다

 

 

기존 JPQL 를 사용했을때 장점 2가지>

1. Fetch Join과 countQuery 활용

(N+1 문제 예방 및 페이징 최적화)

@Query(value = "SELECT d FROM Dispatch d " +
        "JOIN FETCH d.order o " +
        "JOIN FETCH o.customer c " +
        "JOIN FETCH d.orderDetail od " +
        "JOIN FETCH od.product p " +
        "LEFT JOIN FETCH d.warehouse w " +
        "WHERE d.dispatchStatus = :dispatchStatus " +
        "AND o.orderHStatus = 'approved' " +
        "AND d.dispatchDeleteYn = 'N'",
        countQuery = "SELECT COUNT(d) FROM Dispatch d " +
                "JOIN d.order o " +
                "WHERE d.dispatchStatus = :dispatchStatus " +
                "AND o.orderHStatus = 'approved' " +
                "AND d.dispatchDeleteYn = 'N'")
Page<Dispatch> findByDispatchStatus(@Param("dispatchStatus") String dispatchStatus, Pageable pageable);

 

  • JOIN FETCH를 활용하여 연관된 엔티티(order, customer, orderDetail, product, warehouse)를 한 번의 쿼리로 가져와 N+1 문제를 방지함.
  • countQuery를 별도로 작성하여 JOIN FETCH를 사용한 쿼리가 페이징과 함께 안정적으로 동작하도록 함.
    → 일반적으로 JOIN FETCH를 포함한 쿼리는 count 쿼리 실행 시 오류를 발생시킬 수 있기 때문에 별도로 countQuery를 작성하여 해결함.

 

2. 조회 성능 개선 및 대량 데이터 환경에서 일관된 성능 유지

@Query("SELECT d FROM Dispatch d " +
        "JOIN FETCH d.orderDetail od " +
        "JOIN FETCH od.product p " +
        "LEFT JOIN FETCH p.productDetails pd " +
        "LEFT JOIN FETCH p.category c " +
        "WHERE d.dispatchNo = :dispatchNo")
Dispatch findByDispatchDetails(@Param("dispatchNo") Integer dispatchNo);

 

 

  • JOIN FETCH를 통해 orderDetail, product, productDetails, category 데이터를 한 번에 가져와 조회 시 성능을 최적화함.
  • LEFT JOIN FETCH를 활용하여 productDetails와 category가 없을 경우에도 데이터를 정상적으로 조회하도록 처리함.
  • 특정 dispatchNo를 기반으로 상세 정보를 가져오도록 설계하여, 특정 출고 정보를 조회할 때 최소한의 쿼리 실행을 보장함.

 

QueryDSL 로 리팩토링>

이전에는 @Query를 사용하여 정적인 JPQL을 작성했는데, 쿼리 확장성과 유지보수성 차원에서 QueryDSL을 사용하면 좋을것 같다.

 

QueryDSL을 활용한 동적 쿼리 작성

public Page<Dispatch> findByDispatchStatusWithQueryDSL(String dispatchStatus, Pageable pageable) {
    QDispatch dispatch = QDispatch.dispatch;
    QOrder order = QOrder.order;
    QCustomer customer = QCustomer.customer;
    QOrderDetail orderDetail = QOrderDetail.orderDetail;
    QProduct product = QProduct.product;
    QWarehouse warehouse = QWarehouse.warehouse;

    // ✅ 조회 쿼리 - fetchJoin 최적화
    List<Dispatch> dispatches = queryFactory
        .selectFrom(dispatch)
        .join(dispatch.order, order).fetchJoin()
        .join(order.customer, customer).fetchJoin()
        .join(dispatch.orderDetail, orderDetail).fetchJoin()
        .join(orderDetail.product, product).fetchJoin()
        .leftJoin(dispatch.warehouse, warehouse)  // fetchJoin 제거 (불필요한 데이터 로딩 방지)
        .where(dispatch.dispatchStatus.eq(dispatchStatus)
                .and(order.orderHStatus.eq("approved"))
                .and(dispatch.dispatchDeleteYn.eq("N")))
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize())
        .fetch();

    // ✅ count 쿼리 - fetchJoin 제거하여 최적화
    long total = queryFactory
        .select(dispatch.count())
        .from(dispatch)
        .join(dispatch.order, order)  // 불필요한 조인 제거하여 성능 향상
        .where(dispatch.dispatchStatus.eq(dispatchStatus)
                .and(order.orderHStatus.eq("approved"))
                .and(dispatch.dispatchDeleteYn.eq("N")))
        .fetchOne();

    return new PageImpl<>(dispatches, pageable, total);
}

 

 


📌 비교

개선점 기존 코드 수정 코드
불필요한 fetchJoin() 제거 모든 JOIN에 fetchJoin() 적용 warehouse는 leftJoin()만 적용하여 최적화
countQuery에서 불필요한 JOIN 제거 모든 관계를 조인 order만 조인하여 성능 향상
Slice<T> 사용 countQuery 실행 countQuery 없이 next 여부만 체크

📌 결론

✅ QueryDSL 코드에서 fetchJoin()을 최적화하여 메모리 사용량 감소&성능 향상

✅ count 쿼리 최적화를 통해 JOIN을 줄여 성능 개선.

✅ Slice<T>를 활용하면 countQuery 없이 페이징 가능.

✅ 불필요한 데이터 로드를 줄여 대량 데이터에서도 안정적인 성능 유지 가능