본문 바로가기
Develop/Java

[JPA] delete문이 N개가 발생한다고요?😱

by 연로그 2022. 11. 13.
반응형

💥 원인 발생

현재 진행중인 프로젝트에서 deleteAllByXXX를 호출하자 delete가 N개 나가는 현상이 발생했다. 왜 여러개가 나갈까? 왜 한번에 삭제되지 않는걸까?를 찾아보게 되었다.

 

 

📝 원인 분석

deleteAll이 어떤 식으로 동작하는지 확인하기 위해 아래와 같은 과정을 거쳤다. 일단 저희 Repository에서는 Repository를 extends하고 있기 때문에 해당 클래스로 살펴보았다.

 

 

구현체를 찾아 따라가다보니 SimpleJpaRepository 다다르게 되었다. 내부 구현을 확인해보니 delete를 for문을 돌면서 호출했다.😱 반면에 deleteAllInBatch는 여러번 호출하는 것이 아닌 한번에 호출하고 있었다.

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

    public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
    
    // ...
    
    @Override
    @Transactional
    public void deleteAll() {
        for (T element : findAll()) {
            delete(element);
        }
    }
    
    @Override
    @Transactional
    public void deleteAllInBatch() {
        em.createQuery(getDeleteAllQueryString()).executeUpdate();
    }
    
    private String getDeleteAllQueryString() {
        return getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName());
    }
}

 

 


✨ 해결 방법

 

⭐ 시도 1 - deleteInBatch 사용

현재 서비스에서는 이미 조회된 entity 객체 목록이 존재해서 해당 객체로 deleteInBatch를 시도했다. 

public interface ReminderRepository extends Repository<Reminder, Long> {
    void deleteInBatch(Iterable<Reminder> reminders);
}

 

쿼리가 아래 화면과 같이 돌아갔다.

테스트가 돌아는간다

 

or?or?or?or?or?or?

 

당황스러운 or의 연속... deleteInBatch를 다시 보니 아래와 같은 메서드를 호출하는 것을 확인했다. (로직을 그대로 가져오지 않고 핵심 부분만 가져왔습니다)

public abstract class QueryUtils {
    public static <T> Query applyAndBind(String queryString, Iterable<T> entities, EntityManager entityManager) {
        Iterator<T> iterator = entities.iterator();
        StringBuilder builder = new StringBuilder(queryString);
        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"); // 💥 or로 이어붙인다 💥
            }
        }
        Query query = entityManager.createQuery(builder.toString());
        // 쿼리에 파라미터 세팅
        return query;
    }
}

 

 

⭐ 시도 2 - @Query

public interface ReminderRepository extends Repository<Reminder, Long> {
    @Modifying
    @Query("delete from Reminder r where r in :reminders")
    void deleteAllInBatch(Iterable<Reminder> reminders);
}

 

이제야 의도대로 동작하는 것을 확인할 수 있었다.

 

 

in절을 선택한 이유는 많은 양의 데이터를 조회하는 경우 or을 연속해서 사용하는 것보다 훨씬 빠르기 때문이다. 쿼리 실행계획을 살펴보면 or절은 ALL, in절은 range를 타는 것을 확인할 수 있다.

반응형