장바구니 기능 중 장바구니 내에 존재하는 메뉴의 내용을 수정하는 기능을 구현하였다.
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;
}
해당 코드를 수행한다.
해당 코드는 장바구니에 담겨있던 메뉴를 수정할 때, 만약에 기존에 장바구니에 담겨있던 메뉴와 그 옵션의 구성이 동일하다면, 수정 요청한 메뉴를 수정하는 게 아니라 기존에 있던 메뉴의 개수만 증가시키고, 최초 수정하고자 했던 데이터는 지우는 로직이다.
예를 들어,
- 내가 수정하고자 하는 메뉴는 짜장면이고 옵션은 곱배기, 단무지 추가다
- 여기서 나는 곱배기는 유지하고, 단무지는 지우고 싶다.
- 그렇다면 짜장면에 곱배기가 장바구니에 최종 수정돼서 담길 예정이다.
- 만약에 기존 장바구니에 짜장면, 곱배기가 있다면, 해당하는 데이터의 개수만 증가시킨다.
- 수정하고자 했던 데이터는 삭제한다.
이와 같은 프로세스로 진행되는 것을 의도하였다.
각설하고, 저 부분이 왜 탐탁지 않았냐면 최초에 수정 대상 메뉴를 조회하는데, 이후 이것이 기존 장바구니 메뉴에 동일한 조합으로 존재하는지 안하는지 증명하기 위해 또 같은 테이블을 전체적으로 조회하기 때문이다.
그래서 그냥 애초에 처음부터 장바구니에서 짜장면에 해당하는 메뉴를 가진 데이터를 싹 가져오고, 여기서 내가 수정하고자 하는 메뉴만 가져온 후, 이거를 수정하는 게 어떨까.
그리고 최초에 짜장면에 해당하는 메뉴를 가진 데이터를 다 가져왔으니, 이를 이용해서 중복 검증을 거치는 게 어떨까 생각했다.
이에 최초 코드의 성능과 수정 코드의 성능을 비교하여 결정하기로 하였다.
우선 기존 코드에서 수정하지 않고 그대로 진행해보았다.
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 (?, ?)
쿼리는 위와 같이 출력되었으며,
- Execution time: 73.00 milliseconds
- Execution time: 49.00 milliseconds
- Execution time: 39.00 milliseconds
- Execution time: 51.00 milliseconds
- Execution time: 33.00 milliseconds
- Execution time: 26.00 milliseconds
- Execution time: 26.00 milliseconds
- Execution time: 24.00 milliseconds
- Execution time: 23.00 milliseconds
- Execution time: 14.00 milliseconds
평균 35.8 ms 정도 소요된 것을 확인할 수 있었다.
코드를 다음과 같이 수정하였다.
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("존재하지 않는 항목입니다."));
주요 변경 사항은 다음과 같다.
- 최초 cart_menu (장바구니 목록) 을 바로 조회하지 않음
- 변경하고자 하는 장바구니 목록의 메뉴 id 를 dto 로 받은 후 해당 메뉴 조회
- 이를 이용하여 장바구니 내 해당 메뉴가 존재하는 장바구니 목록들을 조회
- 해당 List 에서 filter 를 통해 수정하고자 하는 장바구니 목록을 추출함
단순히 하단에 존재하던, 장바구니 내에서 변경하고자 하는 메뉴가 속한 목록들을 모두 조회하는 것을 코드를 상단으로 옮겼고, 해당 List 에서 수정하고자 하는 목록을 추출한 것 뿐이다.
이를 통해 cart_menu 에서 중복적으로 조회하는 상황을 개선할 수 있었다.
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 를 딱 한 번만 조회하였다.
- Execution time: 37.00 milliseconds
- Execution time: 22.00 milliseconds
- Execution time: 22.00 milliseconds
- Execution time: 20.00 milliseconds
- Execution time: 13.00 milliseconds
- Execution time: 14.00 milliseconds
- Execution time: 19.00 milliseconds
- Execution time: 14.00 milliseconds
- Execution time: 16.00 milliseconds
- Execution time: 15.00 milliseconds
평균 19.2 ms 가 소요되는 것을 확인할 수 있었다.
코드 개선 후 약 46.73 퍼센트 정도 감소된 것을 확인할 수 있었다.
총 select 쿼리 호출 횟수는 동일하지만, 중복 select 쿼리 호출을 개선하니 생각보다 큰 차이로 개선된 것을 확인할 수 있었다.
불필요한 중복 select 쿼리 호출은 서비스 성능에 큰 영향을 미치는 거 같다.
이를 주의하면서 개발을 이어나가야겠다.