컬렉션 조회 최적화
앞에서는 toOne(OneToOne, ManyToOne)
관계만 있었다. 이번에는 일대다(OneToMany)
를 조회하고, 최적화하는 방법을 보자.
V1 엔티티 직접 노출
하지만 나는 Hibernate5
뭐시기를 안깔아서 오류가 뜬다! 하하
그래서 강의를 구경만했다.
V2 엔티티를 DTO로 변환
오옹..? 왜 오류가 뜨지
뭔가 orderItems
에서 오류가 터진모양
얘를 추가해줬다.
오옹...? 강의는 orderItems
가 null
이 나오던데 왜 나는 다 나오지?
원인을 알 수 없었다. 하지만 괜찮다. 아직 여기는 빌드업 과정이니까.
여기가 빌드업인 이유는 orderItems
도 엔티티이므로, 노출되면 안되기 때문이다.
이 orderItem
도 OrderItemDto
를 만들어 값을 넣어준다.
잘 출력된다.
하지만, 역시나 이전과 같이 쿼리가 엄청 많이 출력되는 것을 확인할 수 있다.
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 페치 조인 최적화
이전 강의와 비슷하게 페치조인을 이용해서 해결하려 했으나,
이 방법은 문제점이 있다.
Orders
와 OrderItem
은 orderId
로 조인을 한다.
이때, 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 보다 더 느릴 수도 있다.
애플리케이션에서 추가 작업이 크다.
Author And Source
이 문제에 관하여(컬렉션 조회 최적화), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@seungju0000/컬렉션-조회-최적화저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)