V1. 엔티티 직접 노출
V2. 엔티티를 DTO로 반환
public List<Order> findAllV3() {
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();
페치 조인을 사용하여 쿼리 1번 실행
# 2021-04-21 02:03:14.705 DEBUG 24650 --- [nio-8080-exec-9] org.hibernate.SQL :
select
distinct order0_.order_id as order_id1_6_0_,
member1_.member_id as member_i1_4_1_,
delivery2_.delivery_id as delivery1_2_2_,
orderitems3_.order_item_id as order_it1_5_3_,
item4_.item_id as item_id2_3_4_,
order0_.delivery_id as delivery4_6_0_,
order0_.member_id as member_i5_6_0_,
order0_.order_date as order_da2_6_0_,
order0_.status as status3_6_0_,
member1_.city as city2_4_1_,
member1_.street as street3_4_1_,
member1_.zipcode as zipcode4_4_1_,
member1_.name as name5_4_1_,
delivery2_.city as city2_2_2_,
delivery2_.street as street3_2_2_,
delivery2_.zipcode as zipcode4_2_2_,
delivery2_.status as status5_2_2_,
orderitems3_.count as count2_5_3_,
orderitems3_.item_id as item_id4_5_3_,
orderitems3_.order_id as order_id5_5_3_,
orderitems3_.order_price as order_pr3_5_3_,
orderitems3_.order_id as order_id5_5_0__,
orderitems3_.order_item_id as order_it1_5_0__,
item4_.name as name3_3_4_,
item4_.price as price4_3_4_,
item4_.stock_quantity as stock_qu5_3_4_,
item4_.artist as artist6_3_4_,
item4_.etc as etc7_3_4_,
item4_.author as author8_3_4_,
item4_.isbn as isbn9_3_4_,
item4_.actor as actor10_3_4_,
item4_.director as directo11_3_4_,
item4_.dtype as dtype1_3_4_
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
inner join
order_item orderitems3_
on order0_.order_id=orderitems3_.order_id
inner join
item item4_
on orderitems3_.item_id=item4_.item_id
일대다 연관관계가 있어 동일한 row가 출력됨(예를 들어 order은 2건이지만 각각 주문마다 orderItem이 2개일 때 4개의 row가 출력됨) → distinct 로 해결
원래 데이터베이스에서는 row의 모든 속성이 같아야 distinct가 적용되지만 jpa가 자체적으로 아이디값을 비교하여 중복되는 값을 버림으로써 객체단에서 중복 해결
문제 1) 페이징 불가 → 메모리단에서 페이징이 이뤄져 경고 발생 (WARN) firstResult/maxResult specified with collection fetch, applied in memory
문제 2) 컬렉션 페치 조인은 1개만 사용 가능 - 일대다 대 다 조인으로 데이터가 부정합하게 조회될 수 있음
OrderRepository
public List<Order> findAllV3_1(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();
}
컬렉션 페치 조인시 페이징 처리 방법
- XToOne(OneToOne, ManyToOne) 관계를 모두 페치 조인 (XToOne관계는 row수에 영향을 주지 않음)
- 컬렉션은 지연로딩으로 초기화
- 지연로딩 성능 최적화를 위해 BatchSize 적용(*default_batch_fetch_size의 크기는 100~1000사이가 적당)
application.yml
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
배치 사이즈 전역 설정
@BatchSize
배치 사이즈를 설정하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회
# 2021-04-21 01:36:23.872 DEBUG 24650 --- [nio-8080-exec-2] 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 (
? , ? , ? , ?
)
쿼리 호출 수가 1 + N 에서 1 + 1로 최적화되어 조인 쿼리보다 데이터 전송량 최적화되었다. 페이징 또한 가능하다.
OrderQueryRepository
public List<OrderQueryDto> findOrderQueryDtos() {
// 1
List<OrderQueryDto> result = findOrders();
// 2
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
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();
}
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();
}
- XToOne 관계는한번에 조회 (row수에 영향을 주지 않음)
- XToMany 관계부터 별도로 조회 (row수에 영향을 줌)
v5.1.
OrderQueryRepository
public List<OrderQueryDto> findAllByDto_optimization_1() {
// 1
List<OrderQueryDto> result = findOrders();
// 2
List<Long> orderIds = result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
// 3
List<OrderItemQueryDto> orderItems = 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 in :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList();
// 4
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
.collect(Collectors.groupingBy(orderItemsQueryDto -> orderItemsQueryDto.getOrderId()));
// 5
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
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();
}
1) 루트(XToOne) 조회 1번
# 2021-04-21 01:36:23.858 DEBUG 24650 --- [nio-8080-exec-2] 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
2) OrderItem을 조회하기 위해 Order의 Id만 리스트로 받음
3) XToMany를 위한 별도 조회 - OrderItem 조회
# 2021-04-21 01:36:23.872 DEBUG 24650 --- [nio-8080-exec-2] 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 (
? , ? , ? , ?
)
4) Order와 OrderItem 컬렉션을 Map으로 변환
5) 루프를 돌며 컬렉션 추가 (추가 쿼리 실행하지 않음)
v5.2.
OrderQueryRepository
public List<OrderQueryDto> findAllByDto_optimization() {
// 1
List<OrderQueryDto> result = findOrders();
Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));
// 4
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
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();
}
// 3
private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
List<OrderItemQueryDto> orderItems = 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 in :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList();
return orderItems.stream()
.collect(Collectors.groupingBy(orderItemsQueryDto -> orderItemsQueryDto.getOrderId()));
}
// 2
private List<Long> toOrderIds(List<OrderQueryDto> result) {
return result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
}
위와 동일한 원리이나 로직 모두 메서드화
1) 루트 조회 1번 (XToOne)
2) Order의 아이디조회하여 리스트 반환
3) 아이디 리스트로 OrderItem(OneToMany) 조회하여 Map으로 반환
4) 루프를 돌며 컬렉션에 추가
쿼리는 총 두번 : 루트 1번, 컬렉션 1번 + Map을 사용해서 매칭 성능 향상(O(1))
OrderQueryRepository
public List<OrderFlatDto> findAllByDto_flat() {
List<OrderFlatDto> flats = em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate, o.status, d.address, i.name, oi.orderPrice, oi.count)" +
" from Order o" +
" join o.member m" +
" join o.delivery d" +
" join o.orderItems oi" +
" join oi.item i", OrderFlatDto.class)
.getResultList();
return flats;
}
OrderApiController
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6() {
List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
return flats.stream()
.collect(groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(), o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(), o.getOrderPrice(), o.getCount()), toList())
)).entrySet().stream()
.map(e -> new OrderQueryDto(e.getKey().getOrderId(),e.getKey().getName(), e.getKey().getOrderDate(), e.getKey().getOrderStatus(), e.getKey().getAddress(), e.getValue()))
.collect(toList());
}
컨트롤러에서 flatDTO를 다시 QueryDTO로 변환
쿼리 한번에 실행
# 2021-04-21 01:55:23.585 DEBUG 24650 --- [nio-8080-exec-5] 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_,
item4_.name as col_5_0_,
orderitems3_.order_price as col_6_0_,
orderitems3_.count as col_7_0_
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
inner join
order_item orderitems3_
on order0_.order_id=orderitems3_.order_id
inner join
item item4_
on orderitems3_.item_id=item4_.item_id
데이터 중복 제거로 인해 v5보다 속도가 느릴 수 있다. 애플리케이션에서의 추가 작업이 증가하고 페이징이 불가하다.
그 이후 성능 최적화는 캐시 사용를 사용한다.(* 엔티티에서는 절대 캐시하면 안되고 DTO에서 캐시 사용해야 함)
- 엔티티 조회 -
V1. 엔티티를 조회해서 그대로 반환
V2. 엔티티 조회 후 DTO로 변환
V3. 페치 조인으로 쿼리 수 최적화
V3.1. 컬렉션 페이징과 한계 돌파 - BatchSize
- DTO 직접 조회 -
V4. JPA에서 DTO를 직접 조회
V5. 컬렉션 조회 최적화 - 일대다 관계인 컬렉션은 IN 절을 활용해서 메모리에 미리 조회해서 최적화
V6. 플랫 데이터 최적화 - JOIN 결과를 그대로 조회 후 애플리케이션에서 원하는 모양으로 직접 변환
※ API 성능 최적화 권장 순서
1) 엔티티 조회 방식으로 접근
1-1) 페치 조인으로 쿼리 수 최적화
1-2) 컬렉션 최적화 (페이징 필요 / 불필요)
2) DTO 조회 방식 사용
3) NativeSQL이나 스프링 JdbcTemplate
[SPRING] Querydsl | queryDSL이란 (0) | 2021.04.29 |
---|---|
[SPRING] JPA 활용2 | OSIV와 성능 최적화 (0) | 2021.04.21 |
[SPRING] JPA 활용2 | API 개발과 성능 최적화(1) (0) | 2021.04.20 |
[SPRING] JPA 활용1 | 웹 애플리케이션 개발 (0) | 2021.04.16 |
[SPRING] JPA 프로그래밍 기본 | 객체지향 쿼리 언어 (0) | 2021.04.08 |
댓글 영역