주문 탐색(페이지) API 리팩토링

허석진·2023년 3월 31일
0
post-thumbnail

관련 ERD 구조와 기능 설명

관련 ERD 이미지
주문 탐색(페이지) API는 고객에게 주문에 담긴 주문 상세, 상품 정보와 이미지 그리고 환불 정보까지 보여줄 필요가 있기 때문에 현재 로그인한 유저의 Profile_id를 이용해 Orders, OrderDetail, Product, Product_Image, Refund 정보를 가져와야한다.
그런데 Orders와 OrderDetail은 one-to-many 이기 때문에 fetch join을 통해서 가져올 경우 리턴되는 table의 record가 Orders의 갯수만큼 존재하는게 아니라 OrderDetail의 갯수만큼 존재하기 때문에 limit와 offset이 적용되지 않고 모든 데이터를 메모리로 가져온 후에 처리한다는 것을 이전 포스팅에서 언급한적이 있다.
따라서 이번에도 그 부분을 중점적으로 개선해보려고한다. 하는 김에 로직도 조금 바꾸고...

이전코드

// OrdersRepositoryCustomImpl.java
@Override
public Page<Orders> findAllCreatedAfterAndProfile_Id(LocalDateTime createdAfterDateTime, Long profileId, Pageable pageable) {
    QOrders order = QOrders.orders;
    QOrderDetail orderDetail = QOrderDetail.orderDetail;
    QPayment payment = QPayment.payment;
    QProfile profile = QProfile.profile;
    QRefund refund = QRefund.refund;

    JPQLQuery<Orders> query =
            from(order)
                    .innerJoin(order.profile, profile).fetchJoin()
                    .where(order.createdAt.gt(createdAfterDateTime)
                            .and(profile.id.eq(profileId))
                            .and(order.id.in(JPAExpressions.select(payment.order.id).from(payment).where(payment.paySuccessTf.eq(true)))))
                    .innerJoin(order.orderDetails, orderDetail).fetchJoin()
                    .where(orderDetail.id.notIn(JPAExpressions.select(refund.orderDetail.id).from(refund)))
                    .where(order.orderDetails.isNotEmpty());

    List<Orders> orders = getQuerydsl().applyPagination(pageable, query).fetch();

    return new PageImpl<>(orders, pageable, query.fetchCount());
}

위에 Query문을 간결하게 해설하자면 아래와 같다
"결제 성공한 Orders 중에 환불하지 않은 OrderDetail만을 join 해서, OrderDetail을 모두 환불한 경우는 제외하고 가져온다."

발생 Query

2023-03-29T04:13:12.311+09:00  WARN 10424 --- [nio-8080-exec-3] org.hibernate.orm.query                  : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
Hibernate: 
    select
        o1_0.id,
        o1_0.address,
        o1_0.created_at,
        o1_0.created_by,
        o1_0.modified_at,
        o1_0.modified_by,
        o2_0.orders_id,
        o2_0.id,
        o2_0.created_at,
        o2_0.created_by,
        o2_0.modified_at,
        o2_0.modified_by,
        o2_0.product_id,
        o2_0.product_review_id,
        o2_0.quantity,
        o2_0.status_type,
        o1_0.payment_id,
        p1_0.id,
        p1_0.birth_date,
        p1_0.email,
        p1_0.gender,
        p1_0.name,
        p1_0.phone_number,
        p1_0.point,
        o1_0.receiver_name,
        o1_0.receiver_phone_number 
    from
        orders o1_0 
    join
        profile p1_0 
            on p1_0.id=o1_0.profile_id 
    join
        order_detail o2_0 
            on o1_0.id=o2_0.orders_id 
    where
        o1_0.created_at>? 
        and o1_0.profile_id=? 
        and o1_0.id in(select
            p2_0.order_id 
        from
            payment p2_0 
        where
            p2_0.pay_success_tf=?) 
        and o2_0.id not in(select
            r1_0.order_detail_id 
        from
            refund r1_0) 
        and exists(select
            1 
        from
            order_detail o5_0 
        where
            o1_0.id=o5_0.orders_id) 
    order by
        o1_0.created_at desc


Hibernate: 
    select
        count(o1_0.id) 
    from
        orders o1_0 
    join
        order_detail o2_0 
            on o1_0.id=o2_0.orders_id 
    where
        o1_0.created_at>? 
        and o1_0.profile_id=? 
        and o1_0.id in(select
            p2_0.order_id 
        from
            payment p2_0 
        where
            p2_0.pay_success_tf=?) 
        and o2_0.id not in(select
            r1_0.order_detail_id 
        from
            refund r1_0) 
        and exists(select
            1 
        from
            order_detail o5_0 
        where
            o1_0.id=o5_0.orders_id)

덕지덕지 붙어 있는 것이 누더기 골렘같다.. 실무에서도 이런 Query가 날아가는 일이 있을까?
있을 수도 있지만 내가 일하게 될 곳에서는 없었으면 좋겠다.

왜?

  1. one-to-many 관계에서 one 쪽에 있는 것을 기준으로 fetch join 해 pagination이 정상적으로 적용되지 않음
  2. 쓸때 없이 fetch join을 적용한 Profile
  3. 내가 소비자라면 환불한 주문도 주문 목록 조회에서 확인하고 싶을 것 같음

어떻게?

이번에도 batch_fetch를 통해 one-to-many의 many 쪽의 Lazy Loading을 모아서 처리할 생각이다. 다만 이번에는 이렇게 할 경우 발생하는 Query문이 최종적으로 4개가 된다.
(Orders select 1개 + Orders count 1개 + OrderDetail join Product, Refund select 1개 + ProductImage select 1개)
여기서 OrderDetailProduct, Refund는 one-to-one 관계들이기 때문에 join해서 1번에 가져오고 ProductProductImage는 one-to-many 이기 때문에 join 하지 않는다.


리팩토링 이후 코드

// OrdersRepositoryCustomImpl.java
@Override
public Page<Orders> findAllCreatedAfterAndProfile_Id(LocalDateTime createdAfterDateTime, Long profileId, Pageable pageable) {
    QOrders order = QOrders.orders;

    JPQLQuery<Orders> query =
            from(order)
                    .where(order.createdAt.gt(createdAfterDateTime)
                            .and(order.profile.id.eq(profileId))
                            .and(order.payment.paySuccessTf.eq(true)));

    List<Orders> orders = getQuerydsl().applyPagination(pageable, query).fetch();

    return new PageImpl<>(orders, pageable, query.fetchCount());
}

필요없다고 생각되는 join과 로직을 제거했다. 바뀐 Query문에서는 로그인한 유저의 id와 검색 기간, 결제 성공 여부만 체크한다.

// OrderDetail.java
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class OrderDetail extends AuditingFields {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    ...

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    private Orders orders;

    @ManyToOne(optional = false)
    @JoinColumn(name = "productId")
    private Product product;

    @Setter
    @OneToOne(mappedBy = "orderDetail")
    private Refund refund;
    
    ...
}

OrderDetail을 조회할 때는 관련 정보인ProductRefund를 반드시 조회 할 것이다. 때문에 이 2개에 연관관계에 대해서는 OrderDetail을 조회할 때 위 코드와 같이 join 해서 조회하게 끔한다. (@JoinColumn@OneToOne의 FK가 없는 쪽을 활용)

리팩토링 이후 발생하는 Query

Hibernate: 
    select
        o1_0.id,
        o1_0.address,
        o1_0.created_at,
        o1_0.created_by,
        o1_0.modified_at,
        o1_0.modified_by,
        o1_0.payment_id,
        o1_0.profile_id,
        o1_0.receiver_name,
        o1_0.receiver_phone_number 
    from
        orders o1_0 
    join
        payment p2_0 
            on p2_0.id=o1_0.payment_id 
    where
        o1_0.created_at>? 
        and o1_0.profile_id=? 
        and p2_0.pay_success_tf=? 
    order by
        o1_0.created_at desc offset ? rows fetch first ? rows only
Hibernate: 
    select
        count(o1_0.id) 
    from
        orders o1_0 
    join
        payment p2_0 
            on p2_0.id=o1_0.payment_id 
    where
        o1_0.created_at>? 
        and o1_0.profile_id=? 
        and p2_0.pay_success_tf=?
Hibernate: 
    select
        o1_0.orders_id,
        o1_0.id,
        o1_0.created_at,
        o1_0.created_by,
        o1_0.modified_at,
        o1_0.modified_by,
        p1_0.id,
        p1_0.allergy_info,
        p1_0.brand,
        p1_0.category_detail_code,
        p1_0.content,
        p1_0.country_of_origin,
        p1_0.created_at,
        p1_0.created_by,
        p1_0.modified_at,
        p1_0.modified_by,
        p1_0.name,
        p1_0.packaging,
        p1_0.price,
        p1_0.seller,
        p1_0.shipping,
        p1_0.unit,
        p1_0.weight,
        o1_0.product_review_id,
        o1_0.quantity,
        r1_0.id,
        r1_0.cancel_reason,
        r1_0.created_at,
        r1_0.created_by,
        r1_0.modified_at,
        r1_0.modified_by,
        r1_0.payment_key,
        o1_0.status_type 
    from
        order_detail o1_0 
    left join
        product p1_0 
            on p1_0.id=o1_0.product_id 
    left join
        refund r1_0 
            on o1_0.id=r1_0.order_detail_id 
    where
        o1_0.orders_id in(?,?)
Hibernate: 
    select
        p1_0.product_id,
        p1_0.id,
        p1_0.img_url 
    from
        product_image p1_0 
    where
        p1_0.product_id in(?,?,?)

앞에서 설명했듯 Orders select 1개 + Orders count 1개 + OrderDetail join Product, Refund select 1개 + ProductImage select 1개로 총 4개의 Qeury가 발생하고 이는 조회할 Orders가 몇개든 동일하다.


마치며

사실 처음에는 Query문에서 환불 여부까지 체크해서 환불 안된 주문만 받아오려고 굉장히 오래 고민했다. 하지만 방법을 찾지못하고 고민하다가 반환하는 클라이언트에서 Refund에 대한 정보를 포함하면 된다고 결론을 냈다.
일반적인 사이트에서 주문한 내역을 환불했다고 무조건 삭제하는 것이 정의는 아니니까, 또 그래야한다고 하더라도 클라이언트에서 Refund 존재 여부에 따라 보여줄지 말지를 결정하면된다.
그리고 이번 리팩토링에서 추가적으로 날아가는 select 문에 join을 적용하는 법(@JoinColumn이나 @OneToOne FK가 없는 쪽 등등)을 알아내 적용한 부분이 만족스러웠다.

0개의 댓글