본문 바로가기

JPA

2월 27일 주문조회 V3.1

엔티티를 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