api를 엔티티 그대로 반환하면 양방향 매핑시 무한 루프에 빠진다.
@Jsonignore 을 사용하는것도 좋은 방법은 아니다. (항상 사용하지 않는것이 아니기에)
DTO로 반환하자
💠 DTO로 조회하는 V4 ~ V6
✔️ V3
컬렉션 패치 조인 + 페이징
@BatchSize
// 쿼리 1번
@GetMapping("api/v3.1/orders")
public List<OrderDto> ordersV3_page(@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "100") int limit) {
return orderRepsitory.findAllWithMemberDelivery(offset, limit).stream()
.map(OrderDto::new)
.collect(Collectors.toList());
}
public class Order {
// ...
@BatchSize(size = 1000)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
}
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();
}
컬렉션 페치 조인 시 페이징이 불가능 하다.
일 대 다에서 일을 기준으로 페이징 하는 것이 목적인데 데이터는 다를 기준으로 row가 생성
다 인 OrderItem을 조인하면 OrderItem이 기준이 되버림
@BatchSize 이나 아래의 설정을 통해서 지연 로딩 성능 최적화 한다.
이 옵션은 컬렉션이나 프록시 객체를 한꺼번에 설정한 size만큼 IN 쿼리로 조회
// application.yml
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
사이즈 크기는 순간 부하가 견딜 수 있는 만큼 설정
보통 100 ~ 1000
✔️ V4 JPA에서 DTO 직접 조회
public List<OrderQueryDto> findOrderQueryDtos() {
// 루트 조회 (toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders();
// 루프를 돌면서 컬렉션 추가 (추가 쿼리 실행)
// setter로 orderitems를 넣음
result.forEach(o -> {
List<OrderItemQueryDto> orderItems = findOrderItems(o.getOrderId());
o.setOrderItems(orderItems);
});
return result;
}
/**
* 1:N 관계(컬렉션)를 제외한 나머지를 한번에 조회
*/
private List<OrderQueryDto> findOrders() {
return em.createQuery(
"select new jpabook.jpashop.repository.order.simplequery.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:N 관계인 orderItems 조회
*/
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
return em.createQuery(
"select new jpabook.jpashop.repository.order.simplequery.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();
}
생성자를 통하여 DTO를 직접 조회한다.
루트 1번, 컬렉션 N번 실행
참고
DTO는 바로 조회시 LAZY, fetch join 등 사용 불가
처음부터 원하는 데이터를 모두 선택해서 조회하기에 1번의 쿼리만 나감
결국엔 오래걸린다.
✔️ V5 JPA에서 DTO 직접 조회 - 컬렉션 조회 최적화
public List<OrderQueryDto> findAllByDto_optimization() {
// 루트 조회 (toOne 코드를 모두 한번에 조회)
List<OrderQueryDto> result = findOrders();
//orderItem 컬렉션을 Map 한방에 조회
Map<Long, List<OrderItemQueryDto>> orderItemMap = findOrderItemMap(toOrderIds(result));
// 루프를 돌면서 컬렉션 추가 (추가 쿼리 실행 x)
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
private List<Long> toOrderIds(List<OrderQueryDto> result) {
return result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
}
// 현재 여기서는 DTO 직접 조회이기에 fetch 조인이 안된다.
// 때문에 배치 사이즈도 안먹히는 것
// 따라서 in 연산자를 사용
private Map<Long, List<OrderItemQueryDto>> findOrderItemMap(List<Long> orderIds) {
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new " +
"jpabook.jpashop.repository.order.simplequery.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(OrderItemQueryDto::getOrderId));
}
ToOne N : 1, 1 :1 관계들을 먼저 조회 (1번 쿼리)
in 연산자를 사용하여 컬렉션을 1번의 쿼리로 조회
단 2번의 쿼리
Map을 사용해서 매칭 성능 향상 O(1)
- collectors.groupingBy(Function<T, K> classifier) : Map 리턴
- groupingByConcurrent : ConcurrentMap 리턴
T를 K로 매핑하고 K를 key로 가짐
{ OrderId : OrderItemQueryDto } 이런식으로 Map이 만들어짐
자세한건 찾아보자
✔️ V6 JPA에서 DTO로 직접 조회, 플랫 데이터 최적화
/** V6 DTO 직접 조회, 플랫 데이터 최적화
* 2번의 stream을 사용
* 1번의 쿼리로 처리
*/
@GetMapping("/api/v6/orders")
public List<OrderQueryDto> ordersV6() {
List<OrderFlatDto> flats = orderQueryRepository.findAllByDto_flat();
return flats.stream()
.collect(Collectors.groupingBy(o -> new OrderQueryDto(o.getOrderId(), o.getName(),
o.getOrderDate(), o.getOrderStatus(), o.getAddress()),
Collectors.mapping(o -> new OrderItemQueryDto(o.getOrderId(), o.getItemName(),
o.getOrderPrice(), o.getCount()), Collectors.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(Collectors.toList());
}
public List<OrderFlatDto> findAllByDto_flat() {
return em.createQuery(
"select new " +
"jpabook.jpashop.repository.order.simplequery.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();
}
필요한 요소들을 FlatDto로 모두 조회한 다음
stream을 통해서 처리한다.
처음 stream()
- Function<T, K> classifier
- OrderQueryDto 생성하면서 key로 설정한다.
- Collector<T, A, D> downstream
- OrderFlatDto에서 OrderItemQueryDto를 생성하고 그 결과를 Collectors.toList를 통해 수집한다.
Collector< T, A , D> downstream
- 스트림의 요소 T를 누적해서 중간 결과 A를 만들고,
- 이 중간 결과를 통해 최종적으로 D 타입의 결과를 생성한다
Map의 형태는 { OrderQueryDto : List<OrderItemQueryDto> } 이런 식이다.
마지막 stream()
이를 set으로 변경 후 stream을 사용하여 한 번 더 가공한다.
OrderQueryDto로 컬렉션 부분은 e.getValue()로 List<OrderItemQueryDto>를 넣어준다.
단 1번의 쿼리지만
매우 복잡하다.
✔️ OSIV ( Open Session In View )
spring.jpa.open-in-view : true 기본값
OSIV 전략은 트랜잭션 시작처럼
최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트와 데이터베이스 커넥션을 유지
OSIV OFF
spring.jpa.open-in-view: false OSIV 종료
이렇게 하면 view template에서 지연로딩이 동작하지 않는다.
성능 최적화
OSIV를 끄고 Command와 Query를 분리한다.
Command : 상태를 변경하는 메서드/ 값 반환 X
Query : 상태를 반환하는 메서드 / 상태 변경 X, 값 반환 O
OrderService OrderService: 핵심 비즈니스 로직
OrderQueryService: 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)
이 메서드를 호출 했을 때,
내부에서 변경(사이드 이펙트)가 일어나는 메서드인지,
내부에서 변경이 전혀 일어나지 않는 메서드인지
명확히 분리하는 것
출처 : 인프런 질문 CQS
🔖 학습내용 출처
'Back-End > JPA' 카테고리의 다른 글
스프링 데이터 JPA - 페이징과 정렬, 벌크 수정 (0) | 2024.07.29 |
---|---|
스프링 데이터 JPA - 기본 (0) | 2024.07.29 |
JPA - JPQL (0) | 2024.07.12 |
JPA - 값타입 (0) | 2024.07.12 |
JPA - 프록시와 연관관계 관리 (0) | 2024.07.10 |