jnk1m
Foliage IT
jnk1m
전체 방문자
오늘
어제
  • 분류 전체보기 (209)
    • Today I Learned (34)
    • Java (47)
    • Database (15)
    • [NHN Academy] (27)
    • Spring (47)
    • HTML + CSS + JavaScript (11)
    • JSP (3)
    • Node.js (10)
    • React Native (2)
    • 기타 (8)
    • 스크랩 (5)

인기 글

최근 글

티스토리

hELLO · Designed By 정상우.
글쓰기 / 관리자
jnk1m

Foliage IT

Today I Learned

[TIL] 23/7/13 장바구니 리스트 쿼리문 성능 개선 고민

2023. 7. 13. 19:08

장바구니 리스트 쿼리문 성능 개선

 


개선 전:

사용자의 아이디를 기준으로 카트에 담은 아이템의 리스트를 가져와야 한다.

먼저 직관적으로... 장바구니에서 보여줘야 할 정보를 담은 UserCartDTO를 만들었다.

  private int cartId;

  private int menuId;

  private String menuName;

  private int quantity;

  private Integer optionId;

  private String optionName;

  private Double optionPrice;

위 DTO를 프로젝션하는 쿼리문은 다음과 같이 작성했다.

select c.cart_id, c.menu_id, m.menu_name_eng, c.quantity, co.option_id, o.option_name, o.option_price  from cart c
left join cart_option co on c.cart_id = co.cart_id
join menu m on c.menu_id = m.menu_id
left join options o on co.option_id = o.option_id
where c.user_id = 2;

join 연산을 세번하고, 심지어 left JOIN options o ON co.option_id = o.option_id 부분에서는 인덱스가 이미 생성이 되어 있는데도 풀스캔이 일어난다. (left join을 하는 이유는 메뉴마다 옵션이 있을 수도 없을 수도 있기 때문)

이걸 어떻게 개선해야하나 이틀 동안 고민에 고민을 계속했다...... 

 

👉 개선 방법

1. DTO를 나누자. 

고객에게 보여줘야 하는 정보는 메뉴 이름, 메뉴 가격, 수량  옵션 이름, 옵션 가격이다.

그리고 주문을 하기 위해 장바구니 페이지로부터 받아야 하는 정보는 메뉴 아이디, 옵션 아이디, 수량이다. 

즉, 필요한 DTO는 장바구니 아이템, 장바구니 메뉴, 장바구니 메뉴 옵션 세 개라고 생각했다.

 

@Getter
public class CartItemDTO {
  private int cartId;
  private int quantity;
  private CartMenuDTO cartMenuDTO;
  private List<CartOptionDTO> options = new ArrayList<>();

  public CartItemDTO(Integer cartId, Integer quantity, Integer menuId, String menuNameEng, BigDecimal menuPrice) {
    this.cartId = cartId;
    this.quantity = quantity;
    this.cartMenuDTO = new CartMenuDTO(menuId, menuNameEng, menuPrice);
  }
}

가장 핵심이 되는 CartItemDTO는 장바구니의 담겨있는 하나의 아이템에 해당한다. 필드에 CartMenuDTO와 options 리스트를 가지고 있다. 생성자로 초기화를 할 때 바로 CartMenuDTO 선언해주도록 했고 List<CartOptionDTO>를 통해 사용자가 고른 옵션의 값을 가지고 있도록 설계했다. 또한 options 리스트는 필드 초기화를 통해 만약에 메뉴에 해당하는 옵션이 없다면 언제나 빈 리스트로 초기화될 수 있도록 했다. (NP 방지)

 

@Getter
public class CartMenuDTO {
  private int menuId;
  private String menuNameEng;
  private BigDecimal menuPrice;

  public CartMenuDTO(int menuId, String menuNameEng, BigDecimal menuPrice) {
    this.menuId = menuId;
    this.menuNameEng = menuNameEng;
    this.menuPrice = menuPrice;
  }
}

CartMenuDTO

@Getter
public class CartOptionDTO {
  private int parentCartId;
  private int optionId;
  private String optionName;
  private BigDecimal optionPrice;

  public CartOptionDTO(int parentCartId, int optionId, String optionName, BigDecimal optionPrice) {
    this.parentCartId = parentCartId;
    this.optionId = optionId;
    this.optionName = optionName;
    this.optionPrice = optionPrice;
  }
}

CartOptionDTO

 

2. 쿼리문 나누기

기존에 한개의 쿼리문에서 하던 일을, 두 개로 나눠서 진행하도록 개선했다

@Query("SELECT new com.ohouse.ohouse.domain.CartItemDTO(" +
          "cart.cartId, cart.quantity, cart.menu.menuId, menu.menuNameEng, menu.menuPrice) " +
          "FROM Cart cart " +
          "JOIN cart.menu menu " +
          "WHERE cart.user.userId = :userId")
  List<CartItemDTO> findCartItemsByUserId(int userId);

먼저 장바구니에 담긴 아이템을 리스트로 가져오는 쿼리문을 작성했다. 

카트의 사용자 아이디와 파라미터로 받은 사용자 아이디가 일치하는 컬럼들을 가져온다. cart.menu.menuId, menu.menuNameEng, menu.menuPrice 를 받아서 CartItemDTO 필드에 있는 CartMenuDTO 객체가 초기화될 수 있도록 했다.

@Query("SELECT new com.ohouse.ohouse.domain.CartOptionDTO(" +
          "co.cart.cartId, co.option.optionId, o.optionName, o.optionPrice) " +
          "FROM CartOption co " +
          "JOIN co.option o " +
          "WHERE co.cart.cartId = :parentCartId")
  List<CartOptionDTO> findOptionByCartId(int parentCartId);

파라미터의 이름을 parentCartId로 한 이유는 헷갈리지 않기 위해서... 

 

3. 가져온 옵션을 CartItem 객체의 옵션 리스트에 추가하기

cartItemsByUserId.forEach(cartItemDTO -> {
      List<CartOptionDTO> optionByCartId = cartOptionRepository.findOptionByCartId(cartItemDTO.getCartId());
      optionByCartId.stream()
              .filter(optionDTO -> optionDTO.getCartId() == cartItemDTO.getCartId())
              .forEach(matchingOption -> cartItemDTO.getOptions().add(matchingOption));
    });

cartItemDTO 각 객체마다 순회를 하며 findOptionByCartId 메서드를 호출한다. 인자로는 cartItemDTO의 cartId를 넘겨준다.

그렇게 해서 만들어진 optionByCartId 리스트를 다시 cartItemDTO 객체 안에 리스트에 추가해준다.

중요한 건 모든 객체에 추가가 되는 게 아닌... optionDTO의 parentCartId와 cartItemDTO의 cartId가 일치하는 경우에만 추가해 준다는 것!!!

 

결론: 레프트 조인으로 인해 풀 스캔이 발생했던 쿼리문을 분리하여 인덱스 스캔으로 개선했다.

(DB는 아직도 너무나 어렵다.. 인덱스가 설정되어 있음에도 불구하고 왜 풀스캔이 발생했는지 아직 알아가는 중이다. 갈 길이 멀고도 멀구나~~)

    'Today I Learned' 카테고리의 다른 글
    • [TIL] 23/07/18 페이지 리로드 없이 HTML 폼 사용하여 서버로 데이터를 전송하고 응답 처리하기
    • [TIL] 23/07/17 장바구니 삭제 구현
    • [TIL] 23/7/12 JPQL DTO 프로젝션
    • [TIL] 23/07/11 장바구니 담기 기능 구현 중

    티스토리툴바