주문에 성공하면, 기존 장바구니에서 주문을 한 품목은 지워지는 기능을 구현하려고 했다.
이에 주문 성공 후 단순히 JPA 의 deleteAll 을 사용하면 되겠지만 싶었는데, 각 품목마다 delete 쿼리가 발생했고 이를 어떻게 개선하면 좋을까 생각했다.
@Transactional
public void deleteCartMenus(List<CartMenu> cartMenus) {
cartMenuRepository.deleteAll(cartMenus);
}
우선 개선 전에는 위와 같은 코드로 작성하였다.
데이터가 1,000개 있는 기준으로 삭제를 최종 주문까지 얼마나 시간이 걸리는지 측정해봤다.
1. Execution time: 562.77 ms
2. Execution time: 342.60 ms
3. Execution time: 324.56 ms
4. Execution time: 335.13 ms
5. Execution time: 357.71 ms
6. Execution time: 328.50 ms
7. Execution time: 343.28 ms
8. Execution time: 323.87 ms
9. Execution time: 321.43 ms
10. Execution time: 321.68 ms
평균 시간 약 356.15 ms 소요된다.
각 요소마다 delete 쿼리를 날리니 상당히 긴 시간 소요된 것을 앞서 확인할 수 있었다.
@Transactional
public void deleteCartMenus(List<Long> cartMenuIds) {
cartMenuRepository.deleteAllByIds(cartMenuIds);
}
이에 이번에는 Id 로 이루어진 List 를 받아서 관련된 것들을 한 번에 삭제할 것이다.
@Modifying
@Query("delete from CartMenu cm where cm.id in :cartMenuIds")
void deleteAllByIds(@Param("cartMenuIds") List<Long> cartMenuIds);
쿼리는 위와 같이 작성했다.
여기서 @Modifying 은 INSERT, UPDATE, DELETE 와 같은 DML 언어를 다룰 때 사용되는 어노테이션이라고 한다.
해당 어노테이션을 작성하지 않고 돌렸을 때는
org.hibernate.query.IllegalSelectQueryException:
Expecting a SELECT Query [org.hibernate.query.sqm.tree.select.SqmSelectStatement],
but found org.hibernate.query.sqm.tree.delete.SqmDeleteStatement [delete from CartMenu cm where cm in :cartMenus]
이와 같은 에러가 발생한다.
SELECT 쿼리가 필요하지만, 요청된 쿼리는 SELECT 가 아니라고 하는 거 같다.
이에 SELECT 를 제외한 DML 언어를 조작할 때는 @Modifying 어노테이션을 반드시 붙여줘야 한다.
돌아와서, 코드를 개선 한 후 실행해보니
1. Execution time: 326.90 ms
2. Execution time: 180.86 ms
3. Execution time: 180.63 ms
4. Execution time: 178.84 ms
5. Execution time: 184.93 ms
6. Execution time: 166.34 ms
7. Execution time: 183.74 ms
8. Execution time: 187.17 ms
9. Execution time: 187.91 ms
10. Execution time: 192.50 ms
평균 197.0 ms 소요되는 것을 확인할 수 있다.
delete 쿼리를 한 번으로 줄이니 약 44.69% 감소된 것을 확인할 수 있었다.
최종적으로 197.0 ms 소요되는 것으로 상당히 소요되는 것 같은 느낌인데, 이는 아마 insert 문이 또 각 요소마다 날라가서 발생하는 이슈인 거 같다.
우선 현재는 delete 문을 개선했다는 것에 의의를 두어 만족하지만, 추후 연구를 해서 insert 문 또한 개선할 수 있도록 해야겠다.
추가로 앞서 Id 값들을 받아서 처리했는데,
cartMenuQueryService.deleteCartMenus(cartMenus.stream().
map(CartMenu::getId).collect(Collectors.toList()));
이런식으로 List 를 따로 출력하는 건 뭔가 깔끔하지도 않아보이고 불필요 한 작업 같아서
@Modifying
@Query("delete from CartMenu cm where cm in :cartMenus")
void deleteAllByCartMenus(@Param("cartMenus") List<CartMenu> cartMenus);
id 를 파라미터로 받는 게 아니라 그냥 장바구니 목록 자체를 넘겨주는 것으로 하였다.
이렇게 하면 불필요한 반복문은 생성하지 않아도 된다.
deleteAll 메서드를 사용하여 각각의 엔티티에 대해 별도의 DELETE 쿼리를 실행하는 접근법은 대량의 데이터를 처리할 때 비효율적이며 성능 저하를 일으킬 수 있는 것을 확인할 수 있었다.
두 번째 시도에서는 대량의 데이터를 한 번의 쿼리로 삭제하기 위해 @Query 어노테이션과 함께 커스텀 DELETE 쿼리를 사용하여 성능을 크게 향상시켰다.
또한 @Modifying 어노테이션은 JPA에서 INSERT, UPDATE, DELETE 연산을 수행하는 쿼리 메서드에 필요하다. 이 어노테이션이 없으면, JPA는 SELECT 쿼리만을 기대하게 되며, DML 연산을 시도할 때 예외를 발생시킨다는 것을 알게되었다.
이제는 insert 문을 개선하도록 해야겠다.