최적화에 들어가기 전
XtoX 의 관계에 있어서 정리해보자
[
{
"orderId": 1,
"name": "userA", (N:1 order : Member)
"address": { 1:1 order : Delivery
"city": "서울",
"street": "1",
"zipcode": "1111"
},
"orderItems": [ (1:N order : orderItems)
{
"itemName": "JPA1 Book",
"orderPrice": 10000,
"count": 1
},
{
"itemName": "JPA2 Book",
"orderPrice": 20000,
"count": 1
}
]
}
]
보이는 것처럼 ToOne일때는 조회하는데 아무런 상관이 없지만
ToMany로 가는순간 데이터가 확 늘어나고 잘못하면 Many를 기점으로 DB에서 값을 불러와
1의 값이 중복되면서 N값만 다르는 데이터를 불러와 성능 저하가 될 수 있다
-> order, member값은 중복되면서 orderItems 값만 다른 성능에 좋지않은 데이터를 가져올 수 있다.
우리는 1을 기준으로 잡고 N을 조회하는 방법을 생각해야한다.
toOne(OneToOne , ManyToOne) 과 다르게 컬렉션인 일대다 관계를 최적화하고 DTO로 변환하는 방법 (OneToMany)
Controller
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> collect = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
repository
public List<Order> findAllWithItem() {
return em.createQuery(
"select distinct o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d" +
" join fetch o.orderItems oi" +
" join fetch oi.item i" , Order.class
).getResultList();
}
다음과 같이 쿼리를 통해서 fetch join을 통해서 여러번 쿼리가 날라가게 하지않고 order을 호출 햇을때
memer , delivery ,orderItems 등을 한번에 쿼리로 가져 올 수 있게 한다.
여기서 처음보는 문법이 distinct인데
distinct 사용한 이유가 1 대 다 조인이 있으므로 DB의 row가 증가하게 되고 같은 order Entity의 조회수도 증가하게 된다.
하지만 SQL에 distinct을 추가하고 같은 Entity가 조회되면 애플리케이션에서 중복을 걸러주게 된다.
결국 distinct을 사용해서 중복 조회되는것을 막아준다.
하지만 단점으로 2가지가 있는데
페이징 불가
-> 하이버네이트는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고 메모리에서 페이징을해서 위험한 상황이 일어난다.
컬렉션 페치 조인은 1개만
->JPA에서 알아서 DB를 조합하는건데 데이터가 부정확하게 조회 될 수 있다
그렇다면 페이징 쓸 수 있는 방법
컬렉션을 페치 조인하면 페이징이 불가능한다는 말이
우리는 order <-> orderItem이 있을때 (1:N) Order 즉 , 1 을 기준으로 데이터를 생성해야되는데
orderItem(N) 을 패치조인하게 되면서 데이터가 1 이 아닌 N을 기준으로 데이터가 생성된다.
이 부분에서 문제가 발생한다.
1. ToX (OnetoOne , ManyToOne)관계는 row수를 증가 시키지않기 때문에 페이지 쿼리에 영향을 안주니
단순히 페치 조인한다.
2.컬렉션은 지연 로딩으로 조회한다.
3. 지연로딩을 최적화하기 위해 hibernate.default_batch_fetch_size , @BatchSzie를 적용한다.
- hibernate.default_batch_fetch_size : 글로벌 설정
- @BatchSzie : 개별 최적화
- @BatchSzie을 사용하면 컬렉션이나 프록시 객체를 한꺼번에 설정한 size 만큼 IN쿼리로 조회함
- ToOne은 Entity class위에 @BatchSize , ToMany는 필드 위에 @BatchSize 선언
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_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)
.setMaxResults(limit)
.getResultList();
}
}
다음과 같이 ToX에 대해서는 패치 조인을 해주었지만
컬렉션에 대해서는 지연로딩으로 조회한다.
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
글로벌 batch size 선언 방법
10 ~ 1000 사이 선택하는게 일반적 이 이상하면 DB순간 부하 증가
하지만 전체 데이터 로딩해야하기에 메모리 사용량 같음
결국
장점
쿼리 수가 1+N -> 1+1로 최적화 된다.
조인보다 DB데이터 전송량이 최적화 된다.
-> 조인하면 같이 조회되어서 중복된 데이터가 있는데 각각 조회한다면 불필요한 데이터 중복X
패치 조인 방식과 비교해서 쿼리 호출수가 증가하지만 DB데이터 전송량 감소.
컬렉션 패치 조인을 사용하면 페이징이 불가능하지만 이 방법은 가능하다.
'Spring' 카테고리의 다른 글
[Spring] 지연 로딩 과 프록시 연관관계 (1) | 2024.01.07 |
---|---|
[Spring] 컬렉션 조회 최적화 방향성 (0) | 2024.01.06 |
[Spring] Entity를 DTO로 반환 후 성능 최적화 (fetch join) (1) | 2024.01.04 |
[Spring] Entity를 직접 노출 안하는 이유 (0) | 2024.01.03 |
[Spring] @RequestBody HTTP 메시지 바디 처리하는 어노테이션 (0) | 2023.08.18 |