[JPA] lazyLoading 및 조회 성능 최적화 ( XTOMany )

DevHwan·2022년 12월 2일
0

컬렉션 조회 최적화

먼저 해당 최적화 기술을 이해하려면 lazyLoading이 무엇인지, 왜 발생하는 지에 대해서 알아야 할 필요가 있다. 해당 글에서는 지연 로딩 및 조회 최적화 파트 중에서도 컬렉션 조회에 해당하는 내용이다.

이번 글에서는 이해하기 쉽게 쇼핑몰 서비스를 상상하면서 해보자.

쇼핑몰에서는 물건을 사고 팔기 때문에 각 물건, 주문, 회원, 배송 등 다양한 관계들이 서로 얽혀있다. 그 중에서 주문 조회 API 설계에 대해 생각해보자. 한 주문에서는 당연히 여러 물품을 주문할 수 있다. 그렇다면 주문과 물건의 관계는 OneToMany 관계가 된다. 그렇지만 한 주문은 보통은 한 명의 회원이 하게 된다. OneToOne 관계이고 배송 역시 한 가지 상태를 갖게 된다.

Step 1. 단순 전체 조회

@GetMapping("/api/v2.0/orders")
public List<OrderDto> ordersV2_0() {
    List<Order> orders = orderRepositoryIn.findAll();
    List<OrderDto> collect = orders.stream()
            .map(o -> new OrderDto(o))
            .collect(toList());

    return collect;
}

가장 단순한 방식의 Order를 모두 찾아온 후, 이를 Dto로 변환하는 방식이다.
select
        order0_.order_id as order_id1_6_,
        order0_.delivery_id as delivery4_6_,
        order0_.member_id as member_i5_6_,
        order0_.order_date as order_da2_6_,
        order0_.status as status3_6_ 
    from
        orders order0_

select
        member0_.member_id as member_i1_4_0_,
        member0_.city as city2_4_0_,
        member0_.street as street3_4_0_,
        member0_.zipcode as zipcode4_4_0_,
        member0_.name as name5_4_0_ 
    from
        member member0_ 
    where
        member0_.member_id in (
            ?, ?
        )

select
        orderitems0_.order_id as order_id5_5_1_,
        orderitems0_.order_item_id as order_it1_5_1_,
        orderitems0_.order_item_id as order_it1_5_0_,
        orderitems0_.count as count2_5_0_,
        orderitems0_.item_id as item_id4_5_0_,
        orderitems0_.order_id as order_id5_5_0_,
        orderitems0_.order_price as order_pr3_5_0_ 
    from
        order_item orderitems0_ 
    where
        orderitems0_.order_id in (
            ?, ?
        )

select
        item0_.item_id as item_id2_3_0_,
        item0_.name as name3_3_0_,
        item0_.price as price4_3_0_,
        item0_.stock_quantity as stock_qu5_3_0_,
        item0_.artist as artist6_3_0_,
        item0_.etc as etc7_3_0_,
        item0_.author as author8_3_0_,
        item0_.isbn as isbn9_3_0_,
        item0_.actor as actor10_3_0_,
        item0_.director as directo11_3_0_,
        item0_.dtype as dtype1_3_0_ 
    from
        item item0_ 
    where
        item0_.item_id in (
            ?, ?, ?, ?
        )

~~~ 그 외 ~~~

Order를 조회했을 뿐인데 총 4번 이상의 쿼리가 발생했다. JPA를 이용하여 구현하다보면 가장 많이 발생하는 N+1 문제이다.

각각 Order, Order→Member, Order→OrderItem, OrderItem→ Item 이러한 관계를 참조하면서 지연 로딩이 발생하여 여러 개의 쿼리가 발생하게 된다. 이를 Fetch Join을 통해서 해결한다.


Step 2. Collection을 제외한 Fetch join 조회 with 지연로딩 최적화

@GetMapping("/api/v3.2/orders")
public List<OrderDto> ordersV3_2_page(
        @PageableDefault(size = 20) Pageable pageable) {

    List<Order> orders = orderRepositoryIn.findAllByDeliveryAndMember(pageable);
    List<OrderDto> collect = orders.stream()
            .map(o -> new OrderDto(o))
            .collect(toList());

    return collect;
}
@Repository
public interface OrderRepositoryIn extends JpaRepository<Order, Long> {

    @Query("select o from Order o" + " join fetch o.member m" + " join fetch o.delivery d")
    List<Order> findAllWithMemberAndDelivery(Pageable pageable);
}

이번에는 Order를 조회하면서 JPQL의 특수한 것 중 하나인 fetch join을 이용해서 member와 delivery에 해당하는 데이터를 한번에 가져온다.

select
        order0_.order_id as order_id1_6_0_,
        member1_.member_id as member_i1_4_1_,
        delivery2_.delivery_id as delivery1_2_2_,
        order0_.delivery_id as delivery4_6_0_,
        order0_.member_id as member_i5_6_0_,
        order0_.order_date as order_da2_6_0_,
        order0_.status as status3_6_0_,
        member1_.city as city2_4_1_,
        member1_.street as street3_4_1_,
        member1_.zipcode as zipcode4_4_1_,
        member1_.name as name5_4_1_,
        delivery2_.city as city2_2_2_,
        delivery2_.street as street3_2_2_,
        delivery2_.zipcode as zipcode4_2_2_,
        delivery2_.status as status5_2_2_ 
    from
        orders order0_ 
    inner join
        member member1_ 
            on order0_.member_id=member1_.member_id 
    inner join
        delivery delivery2_ 
            on order0_.delivery_id=delivery2_.delivery_id limit ?

기존에 있던 쿼리들보다 길이가 상당히 길어진 것을 알 수 있다. 이것을 제외하고 한 두개의 쿼리가 더 발생하는 데 그 이유는 바로 XtoMany 관계에 있는 것들은 fetch join하지 않았기 때문이다. 그 이유는 toMany 테이블에 조인을 하게되면 데이터가 수 없이 많이 늘어나서 페이징 처리를 할 수 없기 때문이다. 해당 방식으로 처리하게 되면 페이징처리가 가능하다.


근데 어쨋든 지연로딩 문제는 해결 못한 거 아닌가?

라고 생각할 수 있다. 대신 스프링에서는 이 문제를 최적화 하기 위해서 다음과 같은 옵션을 제공한다.

jpa.properties.hibernate.default_batch_fetch_size: 100

이 옵션들을 사용하면 컬렉션이나 프록시 객체를 한꺼번에 설정한 size만큼 IN 쿼리로 조회한다. → 쿼리 호출의 양이 효율적으로 줄어든다.

다음과 같은 개선을 통해서 조회 쿼리의 성능을 상당 부분 개선할 수 있다. 만약 서비스에서 이런 방식을 통해서도 조회 쿼리 개선에 향상이 없거나 더 빠른 속도를 요구하는 서비스라면 DB 자체를 RDB가 아닌 Redis나 MongoDB 등으로 교체할 필요가 있다.

profile
달리기 시작한 치타

0개의 댓글