[ T I L ] 2024.03.11

오세창·2024년 3월 11일

TIL

목록 보기
8/18

문제

장바구니 기능 중 장바구니 내에 존재하는 메뉴의 내용을 수정하는 기능을 구현하였다.

    public void updateCart(User user, Long cartMenuId, UpdateCartRequestDto updateCartRequestDto) {
        System.out.println("// ========== 장바구니 목록 조회 ========== //");
        CartMenu findCartMenu = cartMenuQueryService.findCartMenuById(cartMenuId);

        System.out.println("// =========== 장바구니 검증 ========== //");
        if(!findCartMenu.getCart().getUser().getId().equals(user.getId())) {
            throw new IllegalArgumentException("본인 장바구니가 아닙니다.");
        }

       /*

       요청한 옵션에서 기존 cart menu option 에 존재하지 않다면, cart menu option 에서 제거

       만약에 cart menu option 에 1, 2, 3, 4 의 값을 가지는 option 이 존재하고,
       요청 dto 는 2, 3, 4, 5 의 옵션을 가지고 있다면,
       사용자 측에서 기존 option 에서 " 1 " 은 취소하고, " 5 " 만 추가하길 바란다는 것으로 인지
       이에 장바구니 메뉴 옵션을 담는 DB 에서 해당 option 은 제거한다.

       */

        System.out.println("// ==== 장바구니 옵션 조회 ==== //");
        findCartMenu.getCartMenuOptions().removeIf(cartMenuOption ->
                !updateCartRequestDto.getOptions().contains(cartMenuOption.getOption().getId()));

        /*
        요청한 메뉴와 옵션이 기존 장바구니에 존재한다면 개수만 증가

        1. 현재 수정하고자 하는 Cart menu 의 menu 아이디를 가지는 Cart Menu 를 모두 조회
        2. 해당 Cart Menu 를 Cart Menu 의 Id 를 key, option Id 를 Value 로 가지는 Map 생성
        3. 현재 Request Dto 의 options 를 이용하여 비교
        4. 만약에 옵션 구성이 동일하다면 개수 증가, 아니면 아래 로직 수행

        */

        System.out.println("// ======== cart menu 조회 ========== //");
        List<CartMenu> findCartMenus = cartMenuQueryService.findCartMenusByCartAndMenu(user.getCart(), findCartMenu.getMenu());
        Map<Long, List<CartMenuOption>> cartOptionMap = findCartOptionMap(findCartMenus);
        if(checkAndIncreaseMatchingCartMenuCount(updateCartRequestDto.getOptions(), updateCartRequestDto.getCount() + findCartMenu.getCount(), findCartMenus, cartOptionMap)) {
            System.out.println("// ======== 기존 장바구니 삭제 ========== //");
            cartMenuQueryService.deleteCartMenu(findCartMenu);
            return;
        }


        /*

        요청한 옵션에서 기존 cart menu option 에 값이 존재한다면, 요청 request dto 에서 해당 값은 제거

        만약에 cart menu option 에 2, 3, 4 가 존재하고 ( 1은 위에 단계에서 지워진 상태다. )
        request dto 는 2, 3, 4, 5 를 요청했다면,
        cart menu option 과 사용자가 요청한 옵션이 2, 3, 4 가 겹치니 이는 수정할 필요가 없다고 판단
        요청 dto 에서 해당 2, 3, 4 값을 제거하고, 최종적으로 option 5 만 새로 추가하고자 하는 것으로 판단한다.

        */


        // 기존 메뉴에 존재하던 option 의 Id 를 List 에 저장
        List<Long> cartMenuOptionIds = findCartMenu.getCartMenuOptions().stream()
                .map(cmo -> cmo.getOption().getId()).toList();

        updateCartRequestDto.getOptions().removeIf(cartMenuOptionIds::contains);

        System.out.println("// ======== 개수 증가 ========== //");
        findCartMenu.updateCount(updateCartRequestDto.getCount());

        System.out.println("// ====== 옵션 조회 ====== //");
        List<Option> optionList = optionManagementService.findOptions(updateCartRequestDto.getOptions());

        List<CartMenuOption> cartMenuOptions = optionList.stream()
                .map(option -> CartMenuOption.builder()
                        .cartMenu(findCartMenu)
                        .option(option)
                        .build())
                .collect(Collectors.toList());
        cartMenuOptionQueryService.saveAllCartMenuOption(cartMenuOptions);
    }

어지저찌 기능 구현까지는 성공하였지만, 가독성도 그렇고 성능에서 자신이 없었다.

탐탁지 않은 부분은 다음 부분이었는데

      CartMenu findCartMenu = cartMenuQueryService.findCartMenuById(cartMenuId);

먼저 최초에 수정하고자 하는 장바구니의 메류를 조회하여 가져온다,

이후에 코드가 진행되고, 최종 수정 전에

     System.out.println("// ======== cart menu 조회 ========== //");
        List<CartMenu> findCartMenus = cartMenuQueryService.findCartMenusByCartAndMenu(user.getCart(), findCartMenu.getMenu());
        Map<Long, List<CartMenuOption>> cartOptionMap = findCartOptionMap(findCartMenus);


        if(checkAndIncreaseMatchingCartMenuCount(updateCartRequestDto.getOptions(), updateCartRequestDto.getCount() + findCartMenu.getCount(), findCartMenus, cartOptionMap)) {
            System.out.println("// ======== 기존 장바구니 삭제 ========== //");
            cartMenuQueryService.deleteCartMenu(findCartMenu);
            return;
        }

해당 코드를 수행한다.

해당 코드는 장바구니에 담겨있던 메뉴를 수정할 때, 만약에 기존에 장바구니에 담겨있던 메뉴와 그 옵션의 구성이 동일하다면, 수정 요청한 메뉴를 수정하는 게 아니라 기존에 있던 메뉴의 개수만 증가시키고, 최초 수정하고자 했던 데이터는 지우는 로직이다.

예를 들어,

  1. 내가 수정하고자 하는 메뉴는 짜장면이고 옵션은 곱배기, 단무지 추가다
  2. 여기서 나는 곱배기는 유지하고, 단무지는 지우고 싶다.
  3. 그렇다면 짜장면에 곱배기가 장바구니에 최종 수정돼서 담길 예정이다.
  4. 만약에 기존 장바구니에 짜장면, 곱배기가 있다면, 해당하는 데이터의 개수만 증가시킨다.
  5. 수정하고자 했던 데이터는 삭제한다.

이와 같은 프로세스로 진행되는 것을 의도하였다.

각설하고, 저 부분이 왜 탐탁지 않았냐면 최초에 수정 대상 메뉴를 조회하는데, 이후 이것이 기존 장바구니 메뉴에 동일한 조합으로 존재하는지 안하는지 증명하기 위해 또 같은 테이블을 전체적으로 조회하기 때문이다.

그래서 그냥 애초에 처음부터 장바구니에서 짜장면에 해당하는 메뉴를 가진 데이터를 싹 가져오고, 여기서 내가 수정하고자 하는 메뉴만 가져온 후, 이거를 수정하는 게 어떨까.

그리고 최초에 짜장면에 해당하는 메뉴를 가진 데이터를 다 가져왔으니, 이를 이용해서 중복 검증을 거치는 게 어떨까 생각했다.

이에 최초 코드의 성능과 수정 코드의 성능을 비교하여 결정하기로 하였다.

시도 ( 1 )

우선 기존 코드에서 수정하지 않고 그대로 진행해보았다.

Query

Hibernate: 
    select
        u1_0.id,
        u1_0.cart_id,
        u1_0.created_at,
        u1_0.deleted_at,
        u1_0.email,
        u1_0.is_deleted,
        u1_0.login_id,
        u1_0.modified_at,
        u1_0.password,
        u1_0.role 
    from
        users u1_0 
    where
        u1_0.login_id=?
// ========== 장바구니 목록 조회 ========== //
Hibernate: 
    select
        cm1_0.id,
        cm1_0.cart_id,
        cm1_0.count,
        cm1_0.menu_id 
    from
        cart_menu cm1_0 
    where
        cm1_0.id=?
// =========== 장바구니 검증 ========== //
Hibernate: 
    select
        c1_0.id,
        c1_0.created_at,
        c1_0.deleted_at,
        c1_0.modified_at,
        u1_0.id,
        u1_0.created_at,
        u1_0.deleted_at,
        u1_0.email,
        u1_0.is_deleted,
        u1_0.login_id,
        u1_0.modified_at,
        u1_0.password,
        u1_0.role 
    from
        cart c1_0 
    left join
        users u1_0 
            on c1_0.id=u1_0.cart_id 
    where
        c1_0.id=?
        
// ======== 장바구니 옵션 조회 ========== //
Hibernate: 
    select
        cmo1_0.cart_menu_id,
        cmo1_0.id,
        cmo1_0.option_id 
    from
        cart_menu_option cmo1_0 
    where
        cmo1_0.cart_menu_id=?
        
// ======== 장바구니 목록들 조회 ========== //
Hibernate: 
    select
        cm1_0.id,
        cm1_0.cart_id,
        cm1_0.count,
        cm1_0.menu_id 
    from
        cart_menu cm1_0 
    left join
        cart c1_0 
            on c1_0.id=cm1_0.cart_id 
    left join
        menu m1_0 
            on m1_0.id=cm1_0.menu_id 
    where
        c1_0.id=? 
        and m1_0.id=?
// ======= 장바구니 옵션 조회 ======= //
Hibernate: 
    delete 
    from
        cart_menu_option 
    where
        id=?
Hibernate: 
    select
        cmo1_0.id,
        cmo1_0.cart_menu_id,
        o1_0.id,
        o1_0.content,
        o1_0.menu_id,
        o1_0.price 
    from
        cart_menu_option cmo1_0 
    left join
        menu_option o1_0 
            on o1_0.id=cmo1_0.option_id 
    where
        cmo1_0.cart_menu_id in (?, ?)

쿼리는 위와 같이 출력되었으며,

  1. Execution time: 73.00 milliseconds
  2. Execution time: 49.00 milliseconds
  3. Execution time: 39.00 milliseconds
  4. Execution time: 51.00 milliseconds
  5. Execution time: 33.00 milliseconds
  6. Execution time: 26.00 milliseconds
  7. Execution time: 26.00 milliseconds
  8. Execution time: 24.00 milliseconds
  9. Execution time: 23.00 milliseconds
  10. Execution time: 14.00 milliseconds

평균 35.8 ms 정도 소요된 것을 확인할 수 있었다.

시도 ( 2 )

코드를 다음과 같이 수정하였다.

        System.out.println("// ======= menu 조회 ========= //");
        Menu oneMenu = menuManagementService.findOneMenu(updateCartRequestDto.getMenuId());

        System.out.println("// ======== cart menu 조회 ========== //");
        List<CartMenu> findCartMenus = cartMenuQueryService.findCartMenusByCartAndMenu(user.getCart(), oneMenu);
        Map<Long, List<CartMenuOption>> cartOptionMap = findCartOptionMap(findCartMenus);

        Optional<CartMenu> result = findCartMenus.stream()
                .filter(cartMenu -> cartMenu.getId().equals(cartMenuId))
                .findFirst();

        CartMenu findCartMenu = result.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 항목입니다."));

주요 변경 사항은 다음과 같다.

  1. 최초 cart_menu (장바구니 목록) 을 바로 조회하지 않음
  2. 변경하고자 하는 장바구니 목록의 메뉴 id 를 dto 로 받은 후 해당 메뉴 조회
  3. 이를 이용하여 장바구니 내 해당 메뉴가 존재하는 장바구니 목록들을 조회
  4. 해당 List 에서 filter 를 통해 수정하고자 하는 장바구니 목록을 추출함

단순히 하단에 존재하던, 장바구니 내에서 변경하고자 하는 메뉴가 속한 목록들을 모두 조회하는 것을 코드를 상단으로 옮겼고, 해당 List 에서 수정하고자 하는 목록을 추출한 것 뿐이다.

이를 통해 cart_menu 에서 중복적으로 조회하는 상황을 개선할 수 있었다.

Query

Hibernate: 
    select
        u1_0.id,
        u1_0.cart_id,
        u1_0.created_at,
        u1_0.deleted_at,
        u1_0.email,
        u1_0.is_deleted,
        u1_0.login_id,
        u1_0.modified_at,
        u1_0.password,
        u1_0.role 
    from
        users u1_0 
    where
        u1_0.login_id=?
// ======= menu 조회 ========= //
Hibernate: 
    select
        m1_0.id,
        m1_0.content,
        m1_0.created_at,
        m1_0.deleted_at,
        m1_0.image,
        m1_0.is_deleted,
        m1_0.is_menu_status,
        m1_0.modified_at,
        m1_0.name,
        m1_0.price,
        m1_0.restaurant_id,
        m1_0.stack_quantity 
    from
        menu m1_0 
    where
        m1_0.id=? 
        and m1_0.is_deleted=0
// ======== 장바구니 목록 조회 ========== //
Hibernate: 
    select
        cm1_0.id,
        cm1_0.cart_id,
        cm1_0.count,
        cm1_0.menu_id 
    from
        cart_menu cm1_0 
    left join
        cart c1_0 
            on c1_0.id=cm1_0.cart_id 
    left join
        menu m1_0 
            on m1_0.id=cm1_0.menu_id 
    where
        c1_0.id=? 
        and m1_0.id=?
// ======= 장바구니 옵션 조회 ======= //
Hibernate: 
    select
        cmo1_0.id,
        cmo1_0.cart_menu_id,
        o1_0.id,
        o1_0.content,
        o1_0.menu_id,
        o1_0.price 
    from
        cart_menu_option cmo1_0 
    left join
        menu_option o1_0 
            on o1_0.id=cmo1_0.option_id 
    where
        cmo1_0.cart_menu_id in (?, ?)
// =========== 장바구니 검증 ========== //
Hibernate: 
    select
        c1_0.id,
        c1_0.created_at,
        c1_0.deleted_at,
        c1_0.modified_at,
        u1_0.id,
        u1_0.created_at,
        u1_0.deleted_at,
        u1_0.email,
        u1_0.is_deleted,
        u1_0.login_id,
        u1_0.modified_at,
        u1_0.password,
        u1_0.role 
    from
        cart c1_0 
    left join
        users u1_0 
            on c1_0.id=u1_0.cart_id 
    where
        c1_0.id=?
// ==== 장바구니 옵션 조회 ==== //
Hibernate: 
    select
        cmo1_0.cart_menu_id,
        cmo1_0.id,
        cmo1_0.option_id 
    from
        cart_menu_option cmo1_0 
    where
        cmo1_0.cart_menu_id=?

쿼리를 확인 해보니 장바구니 목록 즉, cart_menu 를 딱 한 번만 조회하였다.

  1. Execution time: 37.00 milliseconds
  2. Execution time: 22.00 milliseconds
  3. Execution time: 22.00 milliseconds
  4. Execution time: 20.00 milliseconds
  5. Execution time: 13.00 milliseconds
  6. Execution time: 14.00 milliseconds
  7. Execution time: 19.00 milliseconds
  8. Execution time: 14.00 milliseconds
  9. Execution time: 16.00 milliseconds
  10. Execution time: 15.00 milliseconds

평균 19.2 ms 가 소요되는 것을 확인할 수 있었다.

해결

코드 개선 후 약 46.73 퍼센트 정도 감소된 것을 확인할 수 있었다.

총 select 쿼리 호출 횟수는 동일하지만, 중복 select 쿼리 호출을 개선하니 생각보다 큰 차이로 개선된 것을 확인할 수 있었다.

알게된 점

불필요한 중복 select 쿼리 호출은 서비스 성능에 큰 영향을 미치는 거 같다.

이를 주의하면서 개발을 이어나가야겠다.

0개의 댓글