[ T I L ] 2024.03.18

오세창·2024년 3월 18일

TIL

목록 보기
12/18

문제

주문 목록을 조회하는 과정에서 성능 문제를 경험했다.

초기 구현에서는 각 주문에 대해 메뉴 정보를 조회하고, 각 메뉴에 대해 옵션 정보를 조회하는 방식으로 구성되어 있었다.

이 과정에서 발생하는 중첩 반복문은 O(N M K)의 시간 복잡도를 가지며(N은 주문의 수, M은 메뉴의 수, K는 옵션의 수), 많은 수의 데이터베이스 쿼리를 발생시켜 성능 저하의 주요 원인이 되었다.

시도 ( 1 )

개선 전

        List<OrderForm> orderForms = findOrders.stream()
                .map(order -> {
                    Delivery delivery = order.getDelivery();
                    OrderForm orderForm = new OrderForm(order, delivery);

                    List<MenuForm> menuForms = order.getOrderMenus().stream()
                            .map(orderMenu -> {
                                MenuForm menuFrom = new MenuForm(orderMenu);

                                List<OptionForm> optionForms = orderMenu.getOrderMenuOptions().stream()
                                        .map(orderMenuOption -> new OptionForm(orderMenuOption))
                                        .toList();

                                menuFrom.setOptions(optionForms);
                                return menuFrom;
                            })
                            .collect(Collectors.toList());

                    orderForm.setMenus(menuForms);

                    return orderForm;
                })
                .toList();

최초에는 Java Stream을 활용하여 데이터를 처리하는 로직을 구현하는 것이었다.

이 방법은 코드의 가독성을 높이는 데에는 성공했으나, 시간 복잡도를 개선하는 데에는 한계가 있었다.

또한 데이터를 순차적으로 처리함으로써 발생하는 여러 쿼리로 인해 N+1 쿼리 문제가 여전히 발생했다.

시도 ( 2 )

개선 후

  public List<OrderForm> readAllOrder(User user) {

        // 주문 조회;
        System.out.println("// ============ 주문 조회 ============ //");
        List<Order> findOrders = findAllOrders(user);
        
        // 주문 폼 생성
        List<OrderForm> orderForms = findOrders.stream()
                .map(order -> {
                    Delivery delivery = order.getDelivery();
                    return new OrderForm(order, delivery);
                }).collect(Collectors.toList());

        // 주문 메뉴 폼 생성
        System.out.println("// ============ 주문 목록 조회 ============ //");
        List<OrderMenu> findOrderMenus = orderMenuRepository.findAllOrderMenu(findOrderIds(orderForms));

        List<MenuForm> menuForms = findOrderMenus.stream()
                .map(MenuForm::new)
                .toList();

        Map<Long, List<MenuForm>> orderMenusMap = menuForms.stream()
                .collect(Collectors.groupingBy(MenuForm::getOrderId));


        // 주문 옵션 폼 생성
        System.out.println("// ============ 주문 옵션 조회 ============ //");
        List<OrderMenuOption> findOrderMenuOptions = orderMenuOptionRepository.findAllOrderOption(findOrderMenuIds(menuForms));

        List<OptionForm> optionForms = findOrderMenuOptions.stream()
                .map(OptionForm::new)
                .toList();

        Map<Long, List<OptionForm>> orderOpionsMap = optionForms.stream()
                .collect(Collectors.groupingBy(OptionForm::getOrderMenuId));


        // 최종 Dto 매핑
        menuForms.forEach(mf -> mf.setOptions(orderOpionsMap.get(mf.getOrderMenuId())));
        orderForms.forEach(of -> of.setMenus(orderMenusMap.get(of.getOrderId())));
        
        return orderForms;
    }

성능 문제를 근본적으로 해결하기 위해, 쿼리 최적화에 초점을 맞춘 두 번째 접근 방식을 선택했다.

이 접근 방식은 다음과 같은 변경을 포함한다.

  1. fetchJoin을 사용하여 주문과 관련된 배송 정보를 한 번의 쿼리로 가져오는 방법을 도입.

  2. 주문 ID를 기반으로 모든 주문 메뉴를 한 번의 쿼리로 조회하여, 각 주문별 메뉴 정보를 효율적으로 가져옴.

  3. 주문 메뉴 ID를 기반으로 모든 옵션을 한 번의 쿼리로 조회하여, 각 메뉴별 옵션 정보를 효율적으로 가져옴.

이를 통해 중첩 반목문을 피함으로써 시간 복잡도를 개선할 수 있었고, 쿼리 성능 또한 개선되었다.

해결

주문 10개, 주문 목록 100개, 주문 옵션 1,000개를 설정하였다.

개선 전 응답 시간

1. Execution time: 96.21 ms
2. Execution time: 53.76 ms
3. Execution time: 48.38 ms
4. Execution time: 44.93 ms
5. Execution time: 52.64 ms
6. Execution time: 43.12 ms
7. Execution time: 49.06 ms
8. Execution time: 49.99 ms
9. Execution time: 51.63 ms
10. Execution time: 51.53 ms

평균 응답 시간은 54.13ms 정도 소요됐다

개선 후 응답 시간

1. Execution time: 75.21 ms
2. Execution time: 42.02 ms
3. Execution time: 37.67 ms
4. Execution time: 37.66 ms
5. Execution time: 32.22 ms
6. Execution time: 31.27 ms
7. Execution time: 38.43 ms
8. Execution time: 37.16 ms
9. Execution time: 34.71 ms
10. Execution time: 26.26 ms

최적화 후에는 39.26ms로 줄어들어, 약 27.46%의 성능 향상을 달성했다.

알게된 점

복잡한 쿼리를 최적화하고, 중첩 반복문을 피함으로써 시간 복잡도를 대폭 줄일 수 있음을 배웠다.

이번 학습을 통해 성능 최적화의 중요성과 시간 복잡도에 대한 이해가 깊어졌으며, 앞으로 개발 과정에서 이러한 지식을 적극적으로 활용할 계획이다.

0개의 댓글