컬렉션 조회 최적화

28343 단어 JPASpringJPA

앞에서는 toOne(OneToOne, ManyToOne)관계만 있었다. 이번에는 일대다(OneToMany)를 조회하고, 최적화하는 방법을 보자.

V1 엔티티 직접 노출

하지만 나는 Hibernate5 뭐시기를 안깔아서 오류가 뜬다! 하하
그래서 강의를 구경만했다.

V2 엔티티를 DTO로 변환

오옹..? 왜 오류가 뜨지

뭔가 orderItems에서 오류가 터진모양

얘를 추가해줬다.

오옹...? 강의는 orderItemsnull이 나오던데 왜 나는 다 나오지?

원인을 알 수 없었다. 하지만 괜찮다. 아직 여기는 빌드업 과정이니까.

여기가 빌드업인 이유는 orderItems도 엔티티이므로, 노출되면 안되기 때문이다.

orderItemOrderItemDto를 만들어 값을 넣어준다.

잘 출력된다.

하지만, 역시나 이전과 같이 쿼리가 엄청 많이 출력되는 것을 확인할 수 있다.

2022-03-19 00:53:17.185 DEBUG 18539 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    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_ 
    inner join
        member member1_ 
            on order0_.member_id=member1_.member_id limit ?
Hibernate: 
    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_ 
    inner join
        member member1_ 
            on order0_.member_id=member1_.member_id limit ?
2022-03-19 00:53:17.209 DEBUG 18539 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    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=?
Hibernate: 
    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=?
2022-03-19 00:53:17.212 DEBUG 18539 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    select
        delivery0_.delivery_id as delivery1_2_0_,
        delivery0_.city as city2_2_0_,
        delivery0_.street as street3_2_0_,
        delivery0_.zipcode as zipcode4_2_0_,
        delivery0_.status as status5_2_0_ 
    from
        delivery delivery0_ 
    where
        delivery0_.delivery_id=?
Hibernate: 
    select
        delivery0_.delivery_id as delivery1_2_0_,
        delivery0_.city as city2_2_0_,
        delivery0_.street as street3_2_0_,
        delivery0_.zipcode as zipcode4_2_0_,
        delivery0_.status as status5_2_0_ 
    from
        delivery delivery0_ 
    where
        delivery0_.delivery_id=?
2022-03-19 00:53:17.219 DEBUG 18539 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    select
        orderitems0_.order_id as order_id5_5_0_,
        orderitems0_.order_item_id as order_it1_5_0_,
        orderitems0_.order_item_id as order_it1_5_1_,
        orderitems0_.count as count2_5_1_,
        orderitems0_.item_id as item_id4_5_1_,
        orderitems0_.order_id as order_id5_5_1_,
        orderitems0_.order_price as order_pr3_5_1_ 
    from
        order_item orderitems0_ 
    where
        orderitems0_.order_id=?
Hibernate: 
    select
        orderitems0_.order_id as order_id5_5_0_,
        orderitems0_.order_item_id as order_it1_5_0_,
        orderitems0_.order_item_id as order_it1_5_1_,
        orderitems0_.count as count2_5_1_,
        orderitems0_.item_id as item_id4_5_1_,
        orderitems0_.order_id as order_id5_5_1_,
        orderitems0_.order_price as order_pr3_5_1_ 
    from
        order_item orderitems0_ 
    where
        orderitems0_.order_id=?
2022-03-19 00:53:17.224 DEBUG 18539 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    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=?
Hibernate: 
    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=?
2022-03-19 00:53:17.226 DEBUG 18539 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    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=?
Hibernate: 
    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=?
2022-03-19 00:53:17.227 DEBUG 18539 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    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=?
Hibernate: 
    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=?
2022-03-19 00:53:17.230 DEBUG 18539 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    select
        delivery0_.delivery_id as delivery1_2_0_,
        delivery0_.city as city2_2_0_,
        delivery0_.street as street3_2_0_,
        delivery0_.zipcode as zipcode4_2_0_,
        delivery0_.status as status5_2_0_ 
    from
        delivery delivery0_ 
    where
        delivery0_.delivery_id=?
Hibernate: 
    select
        delivery0_.delivery_id as delivery1_2_0_,
        delivery0_.city as city2_2_0_,
        delivery0_.street as street3_2_0_,
        delivery0_.zipcode as zipcode4_2_0_,
        delivery0_.status as status5_2_0_ 
    from
        delivery delivery0_ 
    where
        delivery0_.delivery_id=?
2022-03-19 00:53:17.235 DEBUG 18539 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    select
        orderitems0_.order_id as order_id5_5_0_,
        orderitems0_.order_item_id as order_it1_5_0_,
        orderitems0_.order_item_id as order_it1_5_1_,
        orderitems0_.count as count2_5_1_,
        orderitems0_.item_id as item_id4_5_1_,
        orderitems0_.order_id as order_id5_5_1_,
        orderitems0_.order_price as order_pr3_5_1_ 
    from
        order_item orderitems0_ 
    where
        orderitems0_.order_id=?
Hibernate: 
    select
        orderitems0_.order_id as order_id5_5_0_,
        orderitems0_.order_item_id as order_it1_5_0_,
        orderitems0_.order_item_id as order_it1_5_1_,
        orderitems0_.count as count2_5_1_,
        orderitems0_.item_id as item_id4_5_1_,
        orderitems0_.order_id as order_id5_5_1_,
        orderitems0_.order_price as order_pr3_5_1_ 
    from
        order_item orderitems0_ 
    where
        orderitems0_.order_id=?
2022-03-19 00:53:17.240 DEBUG 18539 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    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=?
Hibernate: 
    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=?
2022-03-19 00:53:17.241 DEBUG 18539 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    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=?
Hibernate: 
    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=?

최적화에 대해 고민을 해봐야 한다.

V3 페치 조인 최적화

이전 강의와 비슷하게 페치조인을 이용해서 해결하려 했으나,
이 방법은 문제점이 있다.

OrdersOrderItemorderId로 조인을 한다.
이때, Orders는 1개이고, OrderItem은 2개가 존재하게 되고,
join을 통해 2개가 조회된다.

그렇기 때문에 쿼리에서 모든 Orders 쿼리가 2개씩 생성된다.

[
    {
        "orderId": 4,
        "name": "userA",
        "orderDate": "2022-03-19T01:01:21.605782",
        "orderStatus": "ORDER",
        "address": {
            "city": "서울",
            "street": "1",
            "zipcode": "1111"
        },
        "orderItems": [
            {
                "itemName": "JPA1 BOOK",
                "orderPrice": 10000,
                "count": 1
            },
            {
                "itemName": "JPA2 BOOK",
                "orderPrice": 20000,
                "count": 2
            }
        ]
    },
    {
        "orderId": 4,
        "name": "userA",
        "orderDate": "2022-03-19T01:01:21.605782",
        "orderStatus": "ORDER",
        "address": {
            "city": "서울",
            "street": "1",
            "zipcode": "1111"
        },
        "orderItems": [
            {
                "itemName": "JPA1 BOOK",
                "orderPrice": 10000,
                "count": 1
            },
            {
                "itemName": "JPA2 BOOK",
                "orderPrice": 20000,
                "count": 2
            }
        ]
    },
    {
        "orderId": 11,
        "name": "userB",
        "orderDate": "2022-03-19T01:01:21.660405",
        "orderStatus": "ORDER",
        "address": {
            "city": "진주",
            "street": "2",
            "zipcode": "2222"
        },
        "orderItems": [
            {
                "itemName": "SPRING1 BOOK",
                "orderPrice": 20000,
                "count": 3
            },
            {
                "itemName": "SPRING2 BOOK",
                "orderPrice": 40000,
                "count": 4
            }
        ]
    },
    {
        "orderId": 11,
        "name": "userB",
        "orderDate": "2022-03-19T01:01:21.660405",
        "orderStatus": "ORDER",
        "address": {
            "city": "진주",
            "street": "2",
            "zipcode": "2222"
        },
        "orderItems": [
            {
                "itemName": "SPRING1 BOOK",
                "orderPrice": 20000,
                "count": 3
            },
            {
                "itemName": "SPRING2 BOOK",
                "orderPrice": 40000,
                "count": 4
            }
        ]
    }
]

그래서 결과가 위처럼 2개씩 생성되는 것을 확인할 수 있다.

이를 어떻게 해결하느냐

distinct : 2가지의 기능이 있다.

  • DB에 distinct를 날려준다.
  • 조회했을때 엔티티가 중복인게 있으면 걸러준다.

이를 이용하면 중복되는 것 없이 하나씩만 출력된다.

그런데 이건 단점이 있다.
페이징이 불가능해진다.

그 이유를 설명하자면,
페이징을 할때 보통 쿼리에서 100개의 데이터만 가져온다고 했을때, 딱 DB에서 100개만 가져오는 동작이 맞다.
그런데, 페치 조인을 하고난 뒤에 페이징을 하게 되면, 모든 데이터를 전부 가져온 뒤에 100개만 가져오는 방식으로 처리된다.

하이버네이트가 이런 선택을 한 이유는 중복되는 데이터가 뽑히고, 이에 대한 제거를 하고 페이징까지는 경우가 겹쳐 사이즈가 정확하지 않아, 모든 작업을 마친 뒤에 메모리에서 페이징을 시행하는 방법을 택한 것이다.

결국 일대다 페치 조인을 사용하면 페이징이 불가능해진다.(정말 위험하다.)

V3.1 페이징과 한계 돌파

페이징 + 컬렉션 엔티티를 함께 조회하려면 어떻게 해야 할까?

  • 일단 ToOne(OneToOne, ManyToOne) 관계는 모두 페치조인한다. ToOne 관계는 페이징에 영향을 안주기 때문에 상관없다.
  • 컬렉션은 지연 로딩으로 조회한다.
  • 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size, @BatchSize를 적용한다.
    • hibernate.default_batch_fetch_size: 글로벌 설정
    • @BatchSize: 개별 최적화
    • 이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size만큼 IN 쿼리로 조회한다.

다대일 부분은 페치 조인으로 값을 가져오면서 페이징까지 완료한다.
그 후에 나머지는 LAZY를 이용해 getter 로 값을 가져온다.

default_batch_fetch_size:100

위 설정을 했을 경우, 이런 IN 쿼리가 나가게 된다. 이는 어떻게 사용되냐면,
반복문을 돌면서 getter와 같은 걸 이용해 계속 쿼리를 날릴때, 이 부분들을 모아서 한번에 조회하는 것이다. 100으로 설정했으니 한번에 최대 100개까지 조회한다.

Orders가 2개 있고, 이 Orders안에 OrderItem이 각각 2개씩 있다면,
Orders 2개 -> Orders1->OrderItem 2개 -> Orders2 -> OrderItem 2개 로 나가야 하는 쿼리가
Orders 2개 -> OrdersItem 4개 로 나가게 되는 것이다.

개별로 설정하려면 @BatchSize를 적용하면 된다.(컬렉션은 컬렉션 필드에, 엔티티는 엔티티 클래스에)

장점

  • 쿼리 호출수가 1 + N -> 1 + 1로 최적화
  • 조인보다 DB 데이터 전송량이 최적화
  • 컬렉션 페치 조인은 페이징이 불가능하지만, 이건 가능

결론

  • ToOne 관계는 hibernate.default_batch_fetch_size

참고: default_batch_fetch_size 의 크기는 적당한 사이즈를 골라야 하는데, 100~1000 사이를 선택하는 것을 권장한다. 이 전략을 SQL IN 절을 사용하는데, 데이터베이스에 따라 IN 절 파라미터를 1000으로 제한하기도 한다. 1000으로 잡으면 한번에 1000개를 DB에서 애플리케이션에 불러오므로 DB 에 순간 부하가 증가할 수 있다. 하지만 애플리케이션은 100이든 1000이든 결국 전체 데이터를 로딩해야 하므로 메모리 사용량이 같다. 1000으로 설정하는 것이 성능상 가장 좋지만, 결국 DB든 애플리케이션이든 순간 부하를 어디까지 견딜 수 있는지로 결정하면 된다.

V4 - JPA에서 DTO 직접 조회

OrderQueryDto

OrderItemQueryDto

OrderQueryRepository

package jpabook.jpashop.repository.order.query;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;

@Repository
@RequiredArgsConstructor
public class OrderQueryRepository {

    private final EntityManager em;

    public List<OrderQueryDto> findOrderQueryDtos() {
        List<OrderQueryDto> result = findOrders();
        result.forEach( o-> {
            List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
            o.setOrderItems(orderItems);
        });
        return result;
    }

    private List<OrderItemQueryDto> findOrderItems(Long orderId) {
        return em.createQuery(
                "select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
                        " from OrderItem oi" +
                        " join oi.item i" +
                        " where oi.order.id = :orderId", OrderItemQueryDto.class)
                .setParameter("orderId", orderId)
                .getResultList();
    }

    private List<OrderQueryDto> findOrders() {
        return em.createQuery(
                "select new jpabook.jpashop.repository.order.query.OrderQueryDto(o.id, m.name, o.orderDate, o.status, d.address)" +
                        " from Order o" +
                        " join o.member m" +
                        " join o.delivery d", OrderQueryDto.class)
                .getResultList();
    }
}

ToOne 관계들을 먼저 조회하고 ToMany 관계는 각각 별도로 처리
findOrderItems()같은 메서드로 따로 처리

그런데 이것도 결국
Orders 1번 , OrderItem N번으로
N+1 문제가 있다.

V5 컬렉션 조회 최적화

이전의 findOrders()까지는 똑같이 실행하고,
그 이후에 Orders 개수만큼 IN으로 개수를 넣어 한방에 전부 조회하고
그걸 Map을 통해 oderItem을 채워넣어주는 것이다.

2022-03-19 02:27:32.039  INFO 25454 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-03-19 02:27:32.040  INFO 25454 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2022-03-19 02:27:32.041  INFO 25454 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2022-03-19 02:27:32.166 DEBUG 25454 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        order0_.order_id as col_0_0_,
        member1_.name as col_1_0_,
        order0_.order_date as col_2_0_,
        order0_.status as col_3_0_,
        delivery2_.city as col_4_0_,
        delivery2_.street as col_4_1_,
        delivery2_.zipcode as col_4_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
2022-03-19 02:27:32.307 DEBUG 25454 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        orderitem0_.order_id as col_0_0_,
        item1_.name as col_1_0_,
        orderitem0_.order_price as col_2_0_,
        orderitem0_.count as col_3_0_ 
    from
        order_item orderitem0_ 
    inner join
        item item1_ 
            on orderitem0_.item_id=item1_.item_id 
    where
        orderitem0_.order_id in (
            ? , ?
        )

이렇게 하면, 총 2번의 쿼리로 정리가 된다.

직접 DB로 접근해 값을 가져와야한다는 불편한 점이 있지만,
쿼리를 줄일 수 있다는 이점이 있다.

갓적화...

다...다음은 쿼리 1번으로 한다고 한다.

V6 플랫 데이터 최적화

??

쉽게 말하자면 조인을 통해 직접 모든 데이터를 다 가져와 한 Dto에 전부 집어 넣은 후 세팅을 일일이 다 해준다는 뜻.

장점

쿼리 단 한번

단점

쿼리는 한번이지만 조인으로 인해 DB에서 애플리케이션에 전달하는 데이터에 중복 데이터가 추가되므로 상황에 따라 V5 보다 더 느릴 수도 있다.
애플리케이션에서 추가 작업이 크다.

좋은 웹페이지 즐겨찾기