• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    미해결

조건(where)을 포함한 일대다(1:N) 페이징 쿼리 질문

23.02.12 23:46 작성 23.02.14 23:58 수정 조회수 843

2

안녕하세요. 영한님

20만 수강생 진심으로 축하드립니다.^^🎉

프로젝트 도중에 막혀서 강의를 👀복습하던 중 질문드립니다..!

(영한님 강의 덕분에 저도 이러한 고민을 하게되다니... 감사의 말씀 드립니다.😊) 

 

페이징이 가능하고 item.name으로 order를 조회하려고 하면 어떠한 방식으로 코드를 작성 해야할까요?

 

 

일단 제가 생각한 방법은 다음과 같습니다.

V3.1 적용

요구사항은 회원(member), 결제(payment), 배송(delievery), 주문 상품(orderItem)을 포함해서 페이징이 되도록 주문(order)을 조회하고, 조건으로 상품 이름(item.name)으로 조회가 가능해야 하는 것 입니다.

orderItem은 일대다 관계 이기 때문에 페이징이 불가합니다.

그래서 V3.1의 방법인 hibernate.default_batch_fetch_size 을 이용하여 IN 쿼리로 orderItem을 조회하려고 합니다.

이렇게 하면 페이징이 가능하도록 order는 조회할수는 있지만, item.name을 조건으로 조회 할 수는 없습니다.

 

V5 적용

그래서 V5에서 알려주신 방법을 사용했습니다.

  1. orderorderItem을 분리한다.

  2. order을 이용하여 orderItem을 조회한다.(추가로 item.name 조건도 넣는다.)

  3. 조회한 결과를 하나의 List으로 만든다.

  4. List을 다시 Page로 변환한다.

public Page<AdminOrderListQueryDto> findOrdersByAdmin(Pageable pageable, OrderSearchCondition condition) {

    // 주문 전체 조회
    List<AdminOrderListQueryDto> content = queryFactory
            .select(new QAdminOrderListQueryDto(order.id,
                    order.status.stringValue(),
                    order.safeKingPayment.amount,
                    order.createDate,
                    order.merchantUid,
                    new QAdminOrderListPaymentQueryDto(order.safeKingPayment.status.stringValue()),
                    new QAdminOrderListMemberQueryDto(order.member.name),
                    new QAdminOrderListDeliveryQueryDto(order.delivery.receiver, order.delivery.status.stringValue()))
            )
            .from(order)
            .leftJoin(order.safeKingPayment, safekingPayment)
            .leftJoin(order.delivery, delivery)
            .leftJoin(order.member, member)
            .where(
                    orderBetweenDate(condition.getFromDate(), condition.getToDate()),
                    deliveryStatusEq(condition.getDeliveryStatus()),
                    paymentStatusEq(condition.getPaymentStatus())
            )
            .orderBy(order.createDate.desc())
            .fetch();

    // 주문 아이디 저장
    List<Long> orderIds = content.stream()
            .map(o -> o.getId())
            .collect(Collectors.toList());

    // 상품명으로 검색 조건
    Map<Long, List<AdminOrderListOrderItemQueryDto>> orderItemMap = findOrderItemMap(orderIds, condition.getKeyword());

    // 주문객체에 주문 상품컬렉션 저장
    content.forEach(o -> o.setOrderItems(orderItemMap.get(o.getId())));

    // 주문상품이 null이 아닌 컬렌션으로 구성
    List<AdminOrderListQueryDto> resultContent = content.stream()
            .filter(o -> o.getOrderItems() != null)
            .collect(Collectors.toList());

    // List를 Page로 변환
    PageRequest pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize());
    int start = (int) pageRequest.getOffset();
    int end = Math.min(start + pageRequest.getPageSize(), resultContent.size());

    if(start > end) {
        throw new OrderException("데이터가 없습니다. 관리자에게 문의하세요.");
    }

    return new PageImpl<>(resultContent.subList(start, end), pageRequest, resultContent.size());
}

private Map<Long, List<AdminOrderListOrderItemQueryDto>> findOrderItemMap(List<Long> orderIds, String keyword) {
    // 주문 상품 검색(item.name 조건 포함)
    List<AdminOrderListOrderItemQueryDto> orderItems = queryFactory.select(new QAdminOrderListOrderItemQueryDto(orderItem.order.id, orderItem.id, orderItem.item.name))
            .from(orderItem)
            .leftJoin(orderItem.item, item)
            .where(
                    orderItem.order.id.in(orderIds),
                    keywordContains(keyword)
            )
            .fetch();

    // Map 으로 변환
    Map<Long, List<AdminOrderListOrderItemQueryDto>> orderItemMap = orderItems.stream()
            .collect(Collectors.groupingBy(orderItemQueryDto -> orderItemQueryDto.getOrderId()));

    return orderItemMap;
}

 // 아이템이름 포함 조건
private BooleanExpression keywordContains(String keyword) {
        return hasText(keyword) ? item.name.contains(keyword) : null;
}

 

이러한 방식을 사용하는게 맞을까요? 🤔

 

감사합니다.^^

 

답변 1

답변을 작성해보세요.

3

안녕하세요. 개발하는쿼카님

스스로 잘 해결하셨습니다^^

복잡한 쿼리는 결국 DTO로 조회하면서도 많은 코드가 들어가는 것이 어쩔수 없는 것 같아요.

감사합니다.

감사합니다.^^

ysw0623님의 프로필

ysw0623

2024.01.09

김영한님 안녕하세요.
해당 쿼리 보면서 궁금한게 있습니다.
order 데이터가 100만 건 인 경우 & order 관련 where조건이 null값인 경우
pageNum=3, size=100으로 order의 list를 조회할 경우

위에 처럼 쿼리를 작성하면,
1. order table은 fullscan 되며, 결과를 서비스 서버의 메모리에 로딩되는걸로 추측됩니다. 맞나요?
1-1. 1번에서 100만 건을 DB에 넣어놓고, heap memory를 모니터링 해보니, 메모리가 1기가여도 GC?가 동작하면서, 주기적으로 heap memory를 정리하더라고요. 이는 GC의 영향이 맞나요?
(모든 데이터가 메모리에 올라와야하니, 당연히 OOM이 발생할줄 알았더니 아니더라고요. GC가 참조되지 않는 객체들이라 stop world상태에서 할당해지해서 메모리 확보한건지 확인하고싶은데, 어떻게 확인해야하는지 방법을 모르겠습니다...)


2. 질문자님의 쿼리 기준으로 order 데이터가 100만 건 이상을 넘어갈 경우, 모든 100만 건 order 데이터를 메모리에 올려서 서버에서 처리해야하므로, 페이징 목적에 맞지 않게 매우 불리한 쿼리 아닌가요?

3. 제가 생각한 해결방안은 페이징 처리는 첫번째 order 쿼리(findOrdersByAdmin에서 만들어지는 sql문)에서 offset, limit 조건과 함께 나가는 것이 좋을거 같습니다.

그렇다면, 첫번째 order 쿼리(findOrdersByAdmin에서 만들어지는 sql문) 에 where문에서 서브쿼리를 사용하여 orderitem keywordContains가 포함된 orderIds를 조회를 한다면,
쿼리에서 offset값과 limit가 적용된 첫번째 order 쿼리를 만들 수 있고 이 페이징 처리는 결국 db에서 처리됩니다.(서비스 서버의 메모리는 orderIds만을 반환받아 List만 할당)

.where(
       order.in(
               selectDistinct(orderitem.order)
               .from(orderitem)
               .where(
                      keywordContains(keyword)
                      )
                  ),
...
)

두번째 쿼리에서는 페이징 적용된 orderIds 값을 가지고, 필요한 정보들을 join해서 QueryDto를 반환받는다면, 훨씬 좋지않을까요?

안녕하세요. ysw0623님

생각하신 내용이 맞습니다. 데이터를 많이 조회하는 경우 페이징 처리를 DB에서 처리해야 합니다.

지금 코드는 단순한 예시라는 점을 참고해주세요.

감사합니다.