조회 쿼리를 진행 준 N + 1 문제가 발생했다.
// ============= OptionIds 조회 ============= //
Hibernate:
select
cmo1_0.option_id
from
cart_menu_option cmo1_0
where
cmo1_0.cart_menu_id=?
Hibernate:
select
cmo1_0.option_id
from
cart_menu_option cmo1_0
where
cmo1_0.cart_menu_id=?
Hibernate:
select
cmo1_0.option_id
from
cart_menu_option cmo1_0
where
cmo1_0.cart_menu_id=?
여러 개의 option 의 id 값을 조회하는데 쿼리가 세 개씩이나 나가는 것이었다.
Map<Long, Set<Long>> optionidsMap = new HashMap<>();
System.out.println("// ============= OptionIds 조회 ============= //");
for(CartMenu cartMenu : findCartMenus) {
optionidsMap.put(cartMenu.getId(), cartMenuOptionRepository.findOptionIds(cartMenu.getId()));
}
// optionIds 와 dto 의 options 가 같은지 확인
System.out.println("// ============= optionIds 와 dto 의 options 가 같은지 확인 ============= //");
for(CartMenu cartMenu : findCartMenus) {
if (optionidsMap.get(cartMenu.getId()).equals(createCartRequestDto.getOptions())) {
cartMenu.increaseCount(createCartRequestDto.getCount());
return;
}
}
사용자가 메뉴를 추가할 때 메뉴 option 의 조합이 기존 장바구니에 동일한 조합이 있는지 확인하기 위해
장바구니를 조회하고 해당 menu 의 option 들을 추출하여 이를 비교할 필요가 있었다.
이 과정에서 mwnu 하나와 연관된 option 을 조회하려면, menu 의 개수만큼 select 쿼릴 날려야 했던 것이다.
얼마전에 N + 1 문제를 해결했던 경험이 있기도 했고, 이에 이전 경험을 떠올리며 개선시켜 보기로 했다.
// Cart Menu Id List 저장
List<Long> cartMenuIds = cartMenus.stream()
.map(CartMenu::getId)
.collect(Collectors.toList());
먼저 option 을 조회하기 전 부모 객체의 Id 값들을 List 형식으로 담아준다.
// Cart Menu Id 를 Key, Option Id 를 Set 형식의 Value 를 가지는 Map 생성
System.out.println("// ============= Cart Menu Option 조회 ============= //");
Map<Long, Set<CartMenuOption>> cartMenuIdToOptionsMap = mapCartMenuIdsToOptionsSet(cartMenuIds);
위에서 생성한 List 를 이용하여 한 번에 연관된 option 을 가져오는 것이다.
private Map<Long, Set<CartMenuOption>> mapCartMenuIdsToOptionsSet(List<Long> menuIds) {
Set<CartMenuOption> cartMenuOptions = cartMenuOptionRepository.findCartMenuOptionByMenuIds(menuIds);
return cartMenuOptions
.stream()
.collect(Collectors.groupingBy(cmo -> cmo.getCartMenu().getId(),
Collectors.toSet()));
}
@Query("select cmo from CartMenuOption cmo where cmo.cartMenu.id in :cartMenuIds")
Set<CartMenuOption> findCartMenuOptionByMenuIds(@Param("cartMenuIds")List<Long> cartMenuIds);
이처럼 in 절을 통해 장바구니에 담겨있는 menu 의 option 테이블을 조회하고, 장바구니의 Id 를 Key, optipn 의 조합을 Set 형식으로 value 에 담는 Map 을 생성하였다.
이때 Set 으로 받는 이유는, 사용자의 메뉴 옵션이 어떤 순서로 들어오는지 정확하게 예측할 수 없다고 생각하였고, 이를 코드 상으로 구축한다고 하더라도 혹시나 모를 변수에 의해 무작위로 옵션 순서가 바뀔 거라고 생각했다.
이에 순서에 상관 없이 사용자가 보낸 option 조합과 DB 에 저장된 option 조합의 정합성을 판단하려면 Set 형식이 적절하다고 판단하였다.
물론 List 로 받고, Sort 로 정렬 후 비교해도 되겠지만 그냥 코드 한 줄이라도 더 줄여보고자 하는 것도 있었다.
결국 위에서 작성한 거 처럼 in 절을 통해 한 번에 가져오니
Hibernate:
select
cmo1_0.id,
cmo1_0.cart_menu_id,
cmo1_0.option_id
from
cart_menu_option cmo1_0
where
cmo1_0.cart_menu_id in (?, ?, ?, ?, ?, ?)
이와 같이 select 쿼리가 한 번만 나가게 되면서 N + 1 문제를 해결할 수 있었다.
이전에는 잘 보이지 않았던 또는 크게 신경쓰지 않았던 N + 1 문제가, 언젠가 한 번 개선하고 나서부터 잘 보이기 시작했다.
75% 가량 조회 속도를 늘렸던 경험이 있기에, 그 중요도를 꺠달았던 것도 있고, 앞으로는 쿼리를 작성할 때 성능 개선에 좀 더 몰두해야겠단 생각이 들었다.