프로그래밍/spring boot

[스프링부트] 실전! 스프링 부트와 JPA 활용2 지연 로딩과 조회 성능 최적화 #1 간단한 주문 조회 V2: 엔티티를 DTO로 변환

aSpring 2023. 11. 17. 09:55
728x90
728x90

 

※ 본 포스팅은 김영한 강사님의 인프런 '실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화' 강의를 들으며 작성한 수강일지 입니다.

 
 

 

| API 개발 고급 - 지연 로딩과 조회 성능 최적화

1. 간단한 주문 조회 V1: 엔티티를 직접 노출
2. 간단한 주문 조회 V2: 엔티티를 DTO로 변환
3. 간단한 주문 조회 V3: 엔티티를 DTO로 변환 - 페치 조인 최적화
4. 간단한 주문 조회 V4: JPA에서 DTO로 바로 조회

 

주문을 기반으로, 주문 + 배송정보 + 회원을 조회하는 API를 만들자

지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해보자.

 

참고: 지금부터 설명하는 내용은 정말 중요합니다. 실무에서 JPA를 사용하려면 100% 이해해야 합니다.
안그러면 엄청난 시간을 날리고 강사를 원망하면서 인생을 허비하게 됩니다.

 

2. 간단한 주문 조회 V2: 엔티티를 DTO로 변환

OrderSimpleApiController - 추가

// V2 : 엔티티를 DTO로 변환
    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2() { // List로 반환하면 안됨, result로 한번 감싸주어야 함
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        List<SimpleOrderDto> result = orders.stream() // for문 돌려도 됨
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());

        // 위 코드를 이렇게 줄여도 됨 1
//        List<SimpleOrderDto> result = orderRepository.findAllByString(new OrderSearch()).stream()
//                .map(o -> new SimpleOrderDto(o))
//                .collect(Collectors.toList());

        // 위 코드를 이렇게 줄여도 됨 2
//        return orderRepository.findAllByString(new OrderSearch()).stream()
//                .map(o -> new SimpleOrderDto(o))
//                .collect(Collectors.toList());

        // 위 코드를 이렇게 줄여도 됨 3
//        return orderRepository.findAllByString(new OrderSearch()).stream()
//                .map(SimpleOrderDto::new)
//                .collect(Collectors.toList());

        // 위 코드를 이렇게 줄여도 됨 4
//        return orderRepository.findAllByString(new OrderSearch()).stream()
//                .map(SimpleOrderDto::new)
//                .collect(toList()); // Collectors 블록 지정 Option + Enter -> static import ...

        return result;
    }

    @Data
    static class SimpleOrderDto {
        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;

        public SimpleOrderDto(Order order) { // 생성자
            orderId = order.getId();
            name = order.getMember().getName();
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
        }
    }

 

  • 엔티티를 DTO로 변환하는 일반적인 방법이다.
  • 쿼리가 총 1 + N + N번 실행된다. (v1과 쿼리수 결과는 같다.) : 이번에는 1 + 2 + 2 => 쿼리 5번 나감
    • order 조회 1번(order 조회 결과 수가 N이 된다.) : 결과 주문 수가 2개이므로 2번 loop를 돔
    • order -> member 지연 로딩 조회 N 번 order -> delivery 지연 로딩 조회 N 번
    • 예) order의 결과가 4개면 최악의 경우 1 + 4 + 4번 실행된다.(최악의 경우)
      • 지연로딩은 영속성 컨텍스트에서 조회하므로, 이미 조회된 경우 쿼리를 생략한다.

 

-> 여기서 address는 Entity가 아닌 Value Object

그냥 address라는 데이터 타입을 하나 정의했다고 보면 됨

 

이제 Entity가 바뀌어도 api 스펙이 바뀔 일이 없음

-> 원래 name인데 username으로 변경 시, getName이 빨간색으로 표시되는 것처럼! 변경하면 문제가 된다는 것을 알 수 있음

 

Entity가 아니라 절대적으로 DTO로 다 바꿔서 보내기!!!

 

V1, V2가 모두 가지고 있는 문제점

- lazy loading으로 인한 DB query가 너무 많이 호출되는 문제

- order, member, delivery 3개의 table을 조회해야 하는 상황

 

-> 만약 같은 유저가 여러개 주문을 한 것이라면

1 + 1 + 2 => 쿼리 4번 나감

 

order -> 2개가 조회됨 -> loop를 member를 가져옴 -> 2번째 loop를 돌 때, member가 이미 영속성 컨텍스트에 있었으므로 member는 다시 조회하지 않음 -> 각각 다른 유저 2명이 주문했을 때보다 쿼리 1번 덜 조회함

 

다음 시간

- N + 1 문제를 페치 조인으로 해결할 것

728x90
728x90