ABOUT ME

Today
Yesterday
Total
  • SQL Query 최적화 (Spring JPA, Go gorm)
    스터디 2024. 3. 15. 23:34

    일반적으로 서버와 DB에서 Query를 만들때 

    성능적으로 가장 신경써야할 부분은 조회이다 

     

    왜 ? 

    가장 많이 사용될 뿐더러, 일반적으로 가장 많은 데이터들을 scan하고 fetch하기 때문이다

     

    그렇다면 ? 

    Query 성능 최적화를 하기 위해서는 select 쿼리를 신경쓰는것이 중요하다.

    특히 OneToMany, ManyToMany, ManyToOne 과 같은 연관관계가 존재하는 Table일 경우에 최적화의 중요성은 강조된다. 

     

    Spring

    Spring JPA에서는 '지연로딩'과 '즉시로딩' 이라는 방법이 존재한다. 

     

    @Data
        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(toList());
            }
        }

     

    현재 OrderDto는 OrderItemsDto와 일대다 매핑이 되어있는 상태이다. 

     

    즉시로딩이란 ? 

     

    OrderDto을 조회하면서 연관된 OrderITemDto 모두 조회한다

     

    이러한 방법은 편하긴 하지만 만약 Order Entity가 OrderItem이 필요하지 않을때도 조회를 진행한다. 만약 연관된 리스트의 값이 엄청 많으면 ? 그만큼 필요없는 query가 발생하고 이는 성능저하로 이어질 수 있다. 

     

    이러한 이유로 Spring JPA에서는 지연로딩을 많이 사용한다. 

     

     

    지연로딩이란 ? 

     

    연관된 List들을 가져올때 Proxy 형태로 가져온다. 따라서 OrderDto에서는 처음에 가져올때는 ORderItemDTO가 값이 없고 Proxy형태로 존재한다. 만약 OrderItemDto를 조회하려고 할때 그 시점에서 List Item에 대한 Fetch가 발생한다. 

     

    방법 1

     

     @GetMapping("/api/v2/orders")
        public List<OrderDto> ordersV2() {
            List<Order> orders = orderRepository.findAll();
            List<OrderDto> result = orders.stream()
                    .map(o -> new OrderDto(o))
                    .collect(toList());
    
            return result;
        }
    
        
           public List<Order> findAll() {
            return em.createQuery("select o from Order o", Order.class)
                    .getResultList();
        }

     

    일단 전부다 가져온 후에 OrderItem을 순회하며 Lazy 강제초기화 시켜주는 방식이다. 

    OrderItem의 수만큼 쿼리가 수행되기 때문에 비효율적이라고 볼 수 있다 .

     

    방법 2

     

    @GetMapping("/api/v3/orders")
        public List<OrderDto> ordersV3() {
            log.info("V3 orders");
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            List<Order> orders = orderRepository.findAllWithItem();
            List<OrderDto> result = orders.stream()
                    .map(o -> new OrderDto(o))
                    .collect(toList());
            stopWatch.stop();
            log.info(stopWatch.prettyPrint());
    
            return result;
        }
        
        public List<Order> findAllWithItem() {
            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();
        }

     

    FetchJoin을 사용하는 방법이다

     

    단 한번의 쿼리로 모든 데이터를 조회가 가능하다. 

    JPA가 신기한게 fetch로 모든데이터를 가져온 후에 Order와 OrderITem들을 자동적으로 매핑시켜준다. 

    하지만 OneToMany의 관계를 FetchJoin하는 경우에 페이징이 불가능하다.

    '스터디' 카테고리의 다른 글

    Spring Transactional 뜯어보기  (0) 2024.05.15
    Spring Tomcat 분석  (0) 2024.05.01
    Spring JPA  (0) 2024.01.24
    Spring - Annotation 정리  (2) 2023.12.01
    Spring - 기본 개념  (0) 2023.12.01
Designed by Tistory.