[JPA] deleteAll() 사용 시 N + 1 문제 해결

joheera·2024년 1월 25일
0

JPA

목록 보기
9/9
post-thumbnail

프로젝트를 진행하면서 여러 개의 데이터를 삭제하는 기능을 구현할 일이 있었다. 처음에는 deleteAllByXXX()을 사용했으나, 테스트를 하면서 데이터들을 삭제하기 위해 DELETE 쿼리를 수십개 날리는 것을 발견했다.

서버에서 DB에 보내는 요청 수는 최소화할수록 성능에 좋다.

즉,

DELETE FROM member where id = 1;
DELETE FROM member where id = 2;
DELETE FROM member where id = 3;

보다

DELETE FROM member where id in (1, 2, 3);

아래처럼 하나로 DELETE 요청을 보내는 것이 훨씬 좋다.

JPA에서 다수의 쿼리문이 나가는 N + 1 문제를 해결하고 성능을 개선한 과정을 살펴보자.

1. deleteAll()

먼저 deleteAll()이 구현되어있는 내부 로직은 다음과 같다.

@Override
@Transactional
public void deleteAll() {
	for(T element : findAll()) {
		delete(element);
	}
}

deleteAll()은 내부에서 findAll()로 테이블에 존재하는 전체 데이터를 조회하고, for문을 돌면서 데이터 하나 하나에 대해 각각 delete()을 실행한다.

즉, 전체 데이터를 조회하는 SELECT 쿼리가 실행된 후, 테이블에 존재하는 N개의 데이터에 대해 N개의 DELETE 쿼리가 실행된다. 이런 쿼리 실행 방식은 데이터의 개수가 많아질수록 성능에 영향을 미친다.

프로젝트에서는 특정 데이터들을 한번에 삭제하기 위해 deleteAllByXXX()을 사용했다.

역시 SELECT문으로 삭제할 데이터를 조회한 뒤 데이터 각각에 대해 DELETE문이 나가고 있는 것을 볼 수 있다.

2. deleteAllInBatch()

deleteAll()보다 효율적인 방법을 찾던 중 deleteAllInBatch()로 한꺼번에 여러 Entity 삭제가 가능하다는 것을 알았다.

public static final String DELETE_ALL_QUERY_STRING 
		= "delete from %s x";

private String getDeleteAllQueryString() {
		return getQueryString(DELETE_ALL_QUERY_STRING, entityInfomation.getEntityName());
}

@Override
@Transactional
public void deleteAllInBatch() {
		em.createQuery(getDeleteAllQueryString()).executeUpdate();
}

내부 구현 로직을 살펴보면 하나의 DELETE 쿼리로 데이터를 삭제하고 있는 것을 볼 수 있다.

테이블 전체 데이터가 아닌 특정 데이터들만 삭제하는 메소드 역시 구현되어 있다.

@Override
@Transactional
public void deleteAllInBatch(Iterable<T> entities) {
		```
		applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities, em)
				.executeUpdate();
		```
}

public static <T> Query applyAndBind(String queryString, Iterable<T> entities, EntityManager entityManager) {
		```
    builder.append(" where");

    int i = 0;
    while (iterator.hasNext()) {
        iterator.next();
        builder.append(String.format(" %s = ?%d", alias, ++i));
        if (iterator.hasNext()) {
            builder.append(" or"); 
        }
   }
	```
}


실제 테스트 코드를 돌렸을 때 DELETE문 하나로 삭제하고 있는 것을 확인할 수 있다. 단, 내부에서 직접 SELECT문이 나가지는 않지만 deleteAllInBatch()에서 매개변수로 Entity List를 받기 때문에 삭제할 Entity를 찾기 위해서는 SELECT문을 한번 날려줘야 한다.

N + 1 문제를 해결할 수 있는 deleteAllInBatch()가 성능적으로 훨씬 낫다고 판단하여 프로젝트에 적용했다. 이후 관련 내용을 정리하면서, deleteAllInBatch()가 실행될 때 사용하는 or보다 in을 사용하는 것이 데이터가 많을 때 성능적으로 좋다는 것을 알게 되었다.

3. @Query로 직접 쿼리문 작성

DELETE문에서 or 대신 in을 사용하기 위해서는 @Query로 직접 쿼리문을 작성하는 방법이 있다.


public interface MemberIngredientRepository extends JpaRepository<MemberIngredient, Long> {
	@Modifying
    @Query("delete from MemberIngredient m where m in :ingredients")
    void deleteAllByQuery(Iterable<MemberIngredient> ingredients);
}

실행 SQL문을 보면 in을 사용해서 DELETE문 하나로 데이터들을 삭제한 것을 확인할 수 있다.

0개의 댓글