Back-End/JPA

API 학습 내용

Meluu_ 2024. 7. 15. 17:04

 

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

 

 

 

 

🔖 학습내용 출처


자바 ORM 표준 JPA 프로그래밍 - 기본편 / 김영한

'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