실전! 스트링 부트와 JPA 활용 2 - 컬렉션 조회 최적화 1

이태휘·2022년 11월 6일
0
post-custom-banner

주문 조회 V1 : 엔티티 직접 노출

1:다 조회를 해볼 것

-> 주문 내역에서 추가로 주문한 상품 정보 추가로 조회하기
-> Order기준으로 컬렉션인 OrderItem 과 Item 필요
-> 이땐 최적화가 어려워지는게 컬렉션이어서 디비 입장에서 데이터 뻥튀기된 느낌!

-> 보면 오더아이템이 리스트로 되어있음!

꿀팁 : iter 하고 tab하면 인텔리제이가 자동으로 포문변경해줌

  • 엔티티 직접 노출 코드
 private final OrderRepository orderRepository;
    
    @GetMapping("/api/v1/orders")
    public List<Order> ordersV1(){
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        //다 한번에 뿌리면 문제 생겨서 지연로딩 터치해주기
        for (Order order : all) {
            order.getMember().getName();    //Lazy 강제 초기화
            order.getDelivery().getAddress();
            List<OrderItem> orderItems = order.getOrderItems();
            for (OrderItem orderItem : orderItems) {
                //각 오더 아이템의 이름도 초기화시켜주기
                orderItem.getItem().getName();
            }
        }
        return all;
    }

-> 엔티티를 직접 노출하기 때문에 좋지 않은 방법!

주문 조회 v2 : 엔티티를 DTO로 변환

  • 엔티티 DTO 변환 코드
    -> 내부 클래스 선언해서 생성자에 초기값 넣어주는 코드
@GetMapping("/api/v2/orders")
    public List<OrderDto> ordersV2(){
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        //orders 를 orderDto로 변환
        List<OrderDto> collect = orders.stream()
                .map(o-> new OrderDto(o))
                .collect(Collectors.toList());
        return collect;
    }

    //내부 객체 생성
    static class OrderDto{

        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;
        //전에비해 추가된 부분
        private List<OrderItem> orderItems;

        public OrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName();
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
            order.getOrderItems().stream().forEach(o->o.getItem().getName());
            orderItems = order.getOrderItems();
        }
    }

근데 사실 DTO로 변환하라 했을 때는 DTO가 wrapping 해서도 안되고 안에 entity가 있어도 안돼!
-> 왜냐면 orderItems 의스펙이 다 외부에 노출이 되기 때문!
-> 엔티티 노출 시키지 말라는게 이렇게 DTO로 한번 감싸지말라는게 아니라 엔티티에 대한 의존을 완전히 끊으라는 뜻!

-> 그래서 orderItem에 대한 DTO도 새로 만들어 줘야해 !

  • 재 wrapping
@GetMapping("/api/v2/orders")
    public List<OrderDto> ordersV2(){
        List<Order> orders = orderRepository.findAllByString(new OrderSearch());
        //orders 를 orderDto로 변환
        List<OrderDto> collect = orders.stream()
                .map(o-> new OrderDto(o))
                .collect(Collectors.toList());
        return collect;
    }

    //내부 객체 생성
    @Getter
    static class OrderDto{

        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;
        //전에비해 추가된 부분
        private List<OrderItemDto> orderItems;


        public OrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName();
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
            orderItems = order.getOrderItems().stream()
                    .map(orderItem -> new OrderItemDto(orderItem))
                    .collect(Collectors.toList());
        }
    }

    //OrderItem을 위한 DTO
    @Getter
    static class OrderItemDto{

        //생성자에서 orderItem안에서 내가 노출하고 싶은 부분을 결정하기
        //내가 상품명만 필요하다면 그 정보만 노출시키는것!
        //클라이언트의 요구사항에 맞게 짜면되는거

        private String itemName;    //상품명
        private int orderPrice; //주문가격
        private int count;      //주문 수량

        public OrderItemDto(OrderItem orderItem) {
            itemName = orderItem.getItem().getName();
            orderPrice = orderItem.getOrderPrice();
            count = orderItem.getCount();
        }
        //이렇게하면 외부로는 내가 원하는 정보만 보여줄수있어
    }

-> 원하는 정보만 나옴!

껍데기뿐만이 아니라 속에 있는거까지 엔티티를 외부에 노출해선 안된다는 것!!!!!!!
-> 내가 원하는 정보만 노출하자!

  • 근데 이렇게 바꿔도 SQL은 어마어마하게 많이 실행됨
    -> 지연로딩이 많아서!!!
    -> N+1 문제 발생

주문 조회 V3 : 패치조인 최적화

  • 엔티티를 DTO로 변경하는 과정을 패치조인으로 최적화하는 과정
//매핑 코드
@GetMapping("/api/v3/orders")
    public List<OrderDto> ordersV3() {
        List<Order> orders = orderRepository.findAllWithItem();
        List<OrderDto> collect = orders.stream()
                .map(o-> new OrderDto(o))
                .collect(Collectors.toList());
        return collect;
    }
    
//OrderRepository에 만든 함수
public List<Order> findAllWithItem() {
        //실무에선느 query ds 로 훨씬 편하게 짤 수 있다!
        //기존이랑 똑같은데 order이랑 orderitems를 조인해
        return em.createQuery(
                "select 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();
    }

-> 이렇게하면 조인을 하면서 데이터가 뻥튀기돼서 각 아이디 별 order 가 2개씩 나오게돼,,!
-> 우리가 의도한거랑 다른 결과물이 나오게 된 것임
-> 우리가 뻥튀기된것에 대한 기준을 제시해줘야해. Order에 대해서
-> 디비가 1:다 관계에서 다 에 맞춰서 즉, order 에 맞춰서 뻥튀기가 돼서!
-> 따라서 sql문 앞에 select distinct, 즉 distinct 키워드 추가

-> 이 distinct 키워드는 SQL에서의 distinct랑 다른점이 sql은 모든 속성값이 일치해야 중복처리 되는데, 여기서는 JPA에서 자체적으로 id값이 같으면 중복이라고 보고 중복처리를 함
-> 이렇게하면 쿼리가 1번 나가! 패치조인으로 인해!

이전에 패치조인을 배웠는데, 컬렉션에서 왜 또 언급할까?

1대다에서는 패치조인하면 페이징이 불가능해 !!!!!!!

-> 디비에서 몇번째부터 몇개 가져와~~ 이런거임!
-> 패치조인을 하면 sql에서 페이징을 위한 limit를 안써서 안되는것!
-> 뻥튀기된결과물 기준으로해서 안하게 되는것임

주문조회V3.1 : 엔티티를 DTO로 변환 - 패치 조인 최적화

즉, row수를 증가시키는 상황에 페이징이 안되는것!

1) ToOne(OneToOne, ManyToOne) 관계를 모두 패치조인함
-> ToOne 관게는 Row수를 증가시키지 않으므로 페이징 쿼리에 영향 주지 않음
-> 오더 입장에서 맴버랑 딜리버리
2) 컬렉션은 지연로딩으로 조회
3) 지연 로딩 성능 최적화를 위해 'hibernate.default_batch_fetch_size', '@BatchSize'를 적용한다
-> hibernate.default_batch_fetch_size : 글로벌 설정
ㄴ 웬만해선 켜두는게좋아
ㄴ 적어놓은 개수만큼 미리 가져오는 친구
-> @BatchSize : 개별 최적화
-> 이 옵션들을 사용하면 컬렉션이나, 프록시 객체를 한꺼번에 설정한 사이즈만큼 IN 쿼리로 조회함

  • 페이징 전에 짜본 코드
@GetMapping("/api/v3.1/orders")
    public List<OrderDto> ordersV3_page( {
        //order,member,delivery 페치조인하는 함수 쓴거임
        //toOne관계여서 패치조인해서 가져옴 !
        //쿼리한번사용
        List<Order> orders = orderRepository.findAllWithMemberDelivery();
        //orderitem은 컬렉션 조회할 때 아이템 개수가 2개인데 그 안에는 각각 아이템 2개가 있어
        //레이지로딩 걸려서 아이템 그때 조회돼서 가져와져
        //그래서 쿼리가 총 6번 실행돼. orderitem+각아이템2개 * 2 -> 1+N+N
        //오더가 많다면 성능이 안나올것!
        //그래서 @RequestParam(value = "offset", defaultVaule = 0 ...등등 옵션추가)
        List<OrderDto> collect = orders.stream()
                .map(o-> new OrderDto(o))
                .collect(Collectors.toList());
        return collect;
    }
  • 페이징 시도 코드 1
    -> 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();
    }

-> 이렇게하면 페이징 잘할수이씀
-> 오프셋 1로해서 첫번째꺼 날리고 두번째꺼만 보여줌
-> 오프셋 0이면 오프셋 없는것처럼 행동

이걸 쉽게하는게 application.yml에 속성 추가하기!

-> 이렇게하면 쿼리문이 in 키워드를 써서 날아감
-> 한번에 in 쿼리로 디비에 있는 orderItem을 한번에 가져온 것임
-> 컬렉션과 관련된 애들을 jpa가 한번에 가져온거
-> 100이란 숫자가 in 쿼리에 들어간 애들 숫자 설정한거
-> 만약 데이터 100갠데 사이즈 10이면 10번 sql문 실행되는것

  • 각 orderItem에 있던 아이템2개도 한번에 땡겨옴.

쿼리1개로 다 해결되는 것!

-> 1+N+N이 1+1+1이됨
-> 근데 사실 쿼리는 한번이지만 디비가 어플리케이션에 전체 다 전송하는거라 용량이 커진다는 이슈가 있음.

+ 페치 조인 방식과 비교해서 쿼리 호출 수가 약간 감소하지만, DB 데이터 전송량이 증가한다.
+ 컬렉션 페치 조인은 페이징이 불가능 하지만 이 방법은 페이징이 가능하다.

-> 페이징이 필요할 땐 이 방식으로 쓰고 ToOne관계는 패치조인해도 문제 없으니까 그냥하기!
-> ToOne관계도 저 설정 영향받아서 최적화됨

@BatchSize
-> 각 속성위에 적어서 개별적으로 설정 가능한데, 굳이 ? 그냥 application에 추가해서 쓰는거로 하라고 한다!

profile
풀스택 개발자 가보자구~
post-custom-banner

0개의 댓글