[ T I L ] 2024.03.14

오세창·2024년 3월 14일

TIL

목록 보기
11/18

문제

주문에 성공하면, 기존 장바구니에서 주문을 한 품목은 지워지는 기능을 구현하려고 했다.

이에 주문 성공 후 단순히 JPA 의 deleteAll 을 사용하면 되겠지만 싶었는데, 각 품목마다 delete 쿼리가 발생했고 이를 어떻게 개선하면 좋을까 생각했다.

시도 ( 1 )

    @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 소요된다.

시도 ( 2 )

각 요소마다 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 문을 개선하도록 해야겠다.

0개의 댓글