주문 조회 V1: 엔티티 직접 노출
- orderItem , item 관계를 직접 초기화하면 Hibernate5Module 설정에 의해 엔티티를 JSON으로 생성한다.
- 양방향 연관관계면 무한 루프에 걸리지 않게 한곳에 @JsonIgnore 를 추가해야 한다.
- 엔티티를 직접 노출하므로 좋은 방법은 아니다.
주문 조회 V2: 엔티티를 DTO로 변환
OrderApiController
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2() {
List<Order> orders = orderRepository.findAllByCriteria(new OrderSearch());
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
- 지연 로딩으로 너무 많은 SQL 실행
- SQL 실행 수
- order 1번
- member , address N번(order 조회 수 만큼)
- orderItem N번(order 조회 수 만큼)
- item N번(orderItem 조회 수 만큼)
주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화
OrderRepository
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();
}
OrderApiController
@GetMapping("/api/v3/orders")
public List<OrderDto> ordersV3() {
List<Order> orders = orderRepository.findAllWithItem();
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
- 페치 조인으로 SQL이 1번만 실행됨
- 1대다 조인 → 데이터베이스 row 증가 → 같은 order 엔티티 조회 수 증가! JPA의 distinct는 SQL에 distinct를 추가하고, 같은 엔티티가 조회되면 중복을 걸러준다.
- 컬렉션 페치 조인을 사용하면 페이징이 불가능 → 하이버네이트는 경고 로그를 남기면서 모든 데이터를 DB에서 읽어오고, 메모리에서 페이징 해버린다!
- 컬렉션 페치 조인은 1개만 사용가능 → 컬렉션 둘 이상에 페치 조인을 사용하면 안됨(데이터가 부정합하게 조회될 수 있음)
주문 조회 V3.1: 엔티티를 DTO로 변환 - 페이징과 한계 돌파
컬렉션을 페치 조인하면 페이징이 불가능
- 컬렉션을 페치 조인하면 일대다 조인이 발생하므로 데이터가 예측할 수 없이 증가
- 일대다에서 일(1)을 기준으로 페이징을 하는 것이 목적이지만, 데이터는 다(N)를 기준으로 row 가 생성
- 하이버네이트는 경고 로그를 남기고 모든 DB 데이터를 읽어서 메모리에서 페이징을 시도 → 장애 발생 가능
한계 돌파
페이징 + 컬렉션 엔티티를 함께 조회하려면?
단순한 코드, 성능 최적화 방법
- ToOne(OneToOne, ManyToOne) 관계를 모두 페치조인 → row 수를 증가시키지 않아서 페이징에 영향 x
- 컬렉션은 지연 로딩으로 조회
- 지연 로딩 성능 최적화를 위해 hibernate.default_batch_fetch_size , @BatchSize 를 적용
- hibernate.default_batch_fetch_size: 글로벌 설정 - IN 쿼리의 개수
- @BatchSize: 개별 최적화
- 이 옵션을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회
OrderRepository
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();
}
OrderApiController
@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> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(toList());
return result;
}
최적화 옵션 - application.yml
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
장점
- 쿼리 호출 수가 1 + N 1 + 1 로 최적화
- 조인보다 DB 데이터 전송량이 최적화
- 페치 조인 방식과 비교해서 쿼리 호출 수가 약간 증가하지만, DB 데이터 전송량이 감소
- 컬렉션 페치 조인은 페이징이 불가능 하지만 이 방법은 페이징이 가능
결론
ToOne 관계는 페치 조인해도 페이징에 영향을 주지 않는다. 따라서 ToOne 관계는 페치조인으로 쿼리 수를 줄이고 해결하고, 나머지는 hibernate.default_batch_fetch_size 로 최적화 하자.
참고 강의:
실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 - 인프런 | 강의
스프링 부트와 JPA를 활용해서 API를 개발합니다. 그리고 JPA 극한의 성능 최적화 방법을 학습할 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
'JPA > 스프링 부트와 JPA 활용 2 - API 개발과 성능 최적화' 카테고리의 다른 글
[스프링 부트와 JPA 활용2 - API 개발과 성능 최적화] 3. API 개발 고급 - 컬렉션 조회 최적화(2) (0) | 2023.04.29 |
---|---|
[스프링 부트와 JPA 활용2 - API 개발과 성능 최적화] 3. API 개발 고급 - 지연 로딩과 조회 성능 최적화 (0) | 2023.01.11 |
[스프링 부트와 JPA 활용2 - API 개발과 성능 최적화] 2. API 개발 고급 - 준비 (0) | 2023.01.10 |
[스프링 부트와 JPA 활용2 - API 개발과 성능 최적화] 1. API 개발 기본 - 회원 API (0) | 2023.01.09 |