deleteAll 대신 deleteAllInBatch를 사용해 2N개의 쿼리를 2개로 (delete vs deleteById vs deleteAll vs deleteAllInBatch)

초록·2023년 11월 23일
0
post-thumbnail

요약

리스트 데이터를 조회해 forEach처리하는 로직에서 1개의 조회 쿼리 이외에 N개의 조회 쿼리가 발행되는 문제가 있었습니다. 디버그모드로 확인한 결과, 롤백을 위해 사용했던 deleteAll 메서드가 delete를 N번 실행하는 게 문제였습니다. 각 delete는 해당 엔티티가 DB에 존재하는지 조회쿼리를 보내고, 존재한다면 삭제쿼리를 보내는 식으로 작동됩니다. 그래서 deleteAllInBatch를 씀으로써 DB 존재 확인을 생략하고 여러개를 한번에 삭제해서 1 + N(조회) + N(삭제)개의 쿼리를 2개(조회, 삭제)로 줄였습니다.

이 과정을 거치며 두 가지를 느꼈는데, 첫 번째는 간단한 코드라도 꼭 쿼리를 보면서 성능적으로 문제가 없을지 확인하는 자세가 필요하다고 느껴졌습니다. 두 번째는 여러가지 delete의 작동방식을 더 세심히 보고 적절한 때 써야함을 느꼈습니다.

문제

테스트 중 N + 1 문제로 의심되는 쿼리가 발견되었습니다. DB에서 리스트 데이터를 조회한 뒤 forEach로 각 엔티티의 getXX 메서드를 호출하는 서비스 코드를 테스트 중이었습니다. 1개의 쿼리만 발행되어야 하는데, 쿼리를 살펴보니 리스트를 조회하는 쿼리 이후에 각 엔티티 id별로 다시 DB에 조회하는 쿼리가 발행됐습니다.

시도 1 : 지연로딩 문제인가?

처음엔 forEach문에서 entity.getXX() 하는 부분에서 지연로딩이 일어난 것인가 의심했지만 즉시로딩을 하게끔 만들어도 문제가 발생했습니다.

시도 2 : 디버그 모드로 들여보기

디버그모드로 브레이크 포인트를 걸고 들여다보니, forEach문이 아닌, 테스트가 종료된 후에 N개의 select 쿼리와 함께 N개의 delete 쿼리가 나가는 것을 볼 수 있었습니다. 알고보니, 테스트를 롤백하기 위해 사용했던 @AfterEach에서 deleteAll()을 사용한 게 원인이었습니다.

delete vs deleteById vs deleteAll vs deleteAllInBatch

deleteAll에 대해 설명하기 전에 먼저 delete부터 시작해서 delete 메서드들을 하나씩 설명해드리겠습니다.

delete

delete는 DB 데이터를 삭제하기 전에, 전달받은 id의 데이터가 DB에 존재하는지 먼저 조회하는 과정을 거친뒤 삭제 쿼리를 날려 삭제를 실행합니다. 즉 조회 1 + 삭제 1 쿼리가 나갑니다.

deleteById

deleteById는 해당 아이디에 해당하는 데이터가 DB에 존재하는지 확인하기 위해 해당 엔티티를 불러옵니다. 그리고 delete를 호출합니다. 이 때 delete에선 위에서 설명했듯이 삭제 전에 조회를 먼저 진행합니다. 그런데 이미 해당 id에 해당하는 엔티티가 영속성 컨텍스트에 있기 때문에 이 조회는 쿼리를 DB에 보내지 않고 1차캐시를 통해 진행하게 됩니다. 그래서 결과적으로 deleteById에서의 조회쿼리 1 + delete에서의 삭제쿼리 1개가 나가게 됩니다.

deleteAll

deleteAll은 전달받은 Iterable의 모든 원소를 delete하는 방식으로 작동합니다. 즉 원소가 n개라면 (조회 1 + 삭제 1) 쿼리를 n번 보내는 식으로 총 2n개의 쿼리가 보내지게 됩니다.

그래서 위와 같은 문제가 발생했던 것입니다.

deleteAllInBatch

대신 deleteAllInBatch를 쓰면 조회쿼리 없이 바로 Batch Delete 쿼리를 보내서 1개의 쿼리로 삭제됩니다.

느낀 점

이 과정을 거치며 두 가지를 느꼈는데, 첫 번째는 간단한 코드라도 꼭 쿼리를 보면서 성능적으로 문제가 없을지 확인하는 자세가 필요하다고 느껴졌습니다. 두 번째는 여러가지 delete의 작동방식을 더 세심히 보고 적절한 때 써야함을 느꼈습니다.

profile
몰입하고 성장하는 삶을 동경합니다

0개의 댓글