엔티티를 DTO로 변환 - 페이징과 한계돌파
fetch join을 하면 페이징을 사용하지 못하는 문제를 해결해보자.
문제
-컬렉션을 페치조인하면 페이징이 불가능하다.
-개발자 관점에서 from 기준의 결과를 원하는데 fetch join이 기준이 되어 버린다.
데이터가 예측할수 없이 증가되어 버린다.
-from을 기준으로 페이징을 생성하는게 목적인데 collection fetch join을 기준으로 row가 생성되는게 문제이다.(페이징이 안되는 이유)
-이 경우 하이버네이트는 경고로그를 남기고
모든 DB데이터를 읽어서 메모리에서 페이징을 시도한다. 최악의 경우 장애 발생
해결방법
-먼저 ~ToOne(@ManyToOne , @OneToOne) 관계를 모두 페치조인한다.
~ToOne 관계는 row수를 증가시키지 않으므로 페이징 쿼리에 영향을 주지 않는다.
-~ToMany(Collection)는 지연로딩으로 가져온다 (fetch = FetchType = LAZY)
즉 ~ToMany는 fetch join을 하지 않는다.
(JPA 페치조인 한계와 돌파 참조)
-지연로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size/ @BatchSize를 적용
hibernate.default_batch_fetch_size : 글로벌 설정
@BatchSize : 개별 최적화
이 설정을 사용하면 컬렉션이나 프록시 객체를 한꺼번에 설정한 size만큼 IN처리로 조회한다.
***
엔티티의 n+1 문제는 fatch Join으로 해결
컬렉션의 n+1 문제는 Batch Size로 해결
@BatchSize(size = 100)
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
해당 필드에 BatchSize를 옵션으로 주고
쿼리에서 Collection을 조회하게 되면
결과값을 여러개 얻게 되는데
그 결과값이 객체 그래프 탐색을 할경우 (Collection을 fetchJoin으로 찾을 경우)
BatchSize에 설정해둔 만큼 In절을 사용해 한번에 PK값들을 넣어서 쿼리를 최소화 한다.
GetMapping("/api/v3.1/orders")
public List<orderDTO> orderV3_page(
@RequestParam(value = "offset" , defaultValue = "0") int offset
@RequestParam(value = "limit" , defaultValue = "100") int limit
){
List<order> orders = orderRepository.findAllWithMemberDelivery(offset , limit);
List<orderDTO> collect = orders.stream()
.map(o -> new orderDTO(o))
.collect(Collectors.toList())
return collect;
}
public List<order> findAllWithMemberDelivery(int offset , int limit){
return em.createQuery(
"select o from Order o" +
"join fetch o.member m " +
"join fetch o.delivery d" , Order.class
)
.setFirstResult(offset)
.setMaxResult(limit)
.fetResultList();
}
이 상태에서는 페이징을 써도 상관없다.
Collection필드를 fetch join하진 않았기 때문에 가능.
///
application.yam
hibernate.default-batch_fetch_size:100
V3까지는 toOne 필드들을 fetch로 한번에 가져오고
DTO로 매핑할때 Collection필드를 건드려 지연로딩을 각각 호출하는 형태 (데이터 뻥튀기)
V3.1은 앞서 application.yam에 "default-batch_fetch_size:100" 옵션을 넣는다.
이후 쿼리를 보면
toOne 필드들은 fetch로 한번에 가져온다.
(toOne은 fetch 되어도 페이징에 아무런 영향이 없다.)
거기에 페이징 함수를 넣어준다.
결과를 보면 order_id IN(4,11)같이 IN쿼리가 나가는 걸 볼수 있는데
이는 위 yam에 옵션에 의해 나오는 것으로
동작 원리는
1.쿼리에 결과를 List로 받는다.
2.그 결과에 지연로딩시 발생할때 List로 받은 결과값의 PK값은 IN절에 넣어 한번에 실행 지연로딩을 한번만 실행시킨다.
3.만약 지연로딩 결과에 또 지연로딩이 있을대도 IN절에 PK값을 모두 넣는다.
결론적으로 각각 생각하면 지연로딩을 한번에 실행시키는 방법
1:N:M 관계인 쿼리를 1:1:1 관계로 만들어준다.
V3는 쿼리가 한번 나가지만 데이터가 뻥튀기 되고
V3.1은 기본쿼리 + 지연로딩 수만큼 쿼리가 나가지만 중복데이터 없는 최적화 된 결과를 받는다.
default-batch_fetch_size:100 의 숫자는 한번에 In절에 넣을 PK수를 의미한다.
toOne 도 fetch조인없이 지연로딩(In)시켜도 되지만
toOne은 fetch조인이 더 효율적이다.
default-batch_fetch_size:100 는 yam에 하는 글로벌 설정으로
특정 collection필드에 적용시킬때는 해당 필드위에 @BatchSize(size = 100)
collection이 아닌 필드에 적용시 @Entity 위에 @BatchSize(size = 100)을 명시한다.
어지간 하면 글로벌 설정으로 해결해준다.
결론
toOne 관계는 패치조인을 해도 페이징에 영향을 주지 않는다.
따라서 ToOne관계는 페치조인으로 쿼리수를 줄이고
나머지 ToMany 관계는 default-batch_fetch_size:100 로 최적화 하자.
설정상 최대값은 1000개 이다.
100이던 1000이던 메모리 소모량은 같다.
**페이징 사용시 제일 중요
실무에서 이 방법을 많이 사용.
'JPA' 카테고리의 다른 글
| 3월 6일 JPA 주문조회 V5 (0) | 2023.03.06 |
|---|---|
| 3월 3일 JPA 주문조회 v4 (0) | 2023.03.03 |
| 2월 27일 JPA 주문조회 V3 (0) | 2023.02.27 |
| 2월 27일 JPA 주문조회 V2 (0) | 2023.02.27 |
| 2월 24일 JPA API개발 고급 , 주문조회 v1 (0) | 2023.02.24 |