프로젝트를 리팩토링하는 과정에서 deleteAllById
와 deleteAllByIdInBatch
가 엄청난 차이가 있다는 것을 알게되었다.
사실 파고들지 않으면 이 차이는 너무나 간단하게 두 줄로 설명이 가능하다.
deleteAllById
는 각id
를select
하고 이후에 각각delete
한다.
반면에deleteAllByIdInBatch
는in
을 사용한 단 한번의delete
만을 시행한다.
deleteAllById
의 코드사실 이 부분은 왜고 뭐고가 없다. 그렇게 코드가 짜여졌으니까!
아래에 deleteAllById
코드를 Spring Dat JPA의 GitHub로 부터 가져와봤다.
@Override
@Transactional
public void deleteAllById(Iterable<? extends ID> ids) {
Assert.notNull(ids, "Ids must not be null");
for (ID id : ids) {
deleteById(id);
}
}
@Transactional
@Override
public void deleteById(ID id) {
Assert.notNull(id, ID_MUST_NOT_BE_NULL);
findById(id).ifPresent(this::delete);
}
@Override
@Transactional
@SuppressWarnings("unchecked")
public void delete(T entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
return;
}
Class<?> type = ProxyUtils.getUserClass(entity);
T existing = (T) em.find(type, entityInformation.getId(entity));
// if the entity to be deleted doesn't exist, delete is a NOOP
if (existing == null) {
return;
}
em.remove(em.contains(entity) ? entity : em.merge(entity));
}
위 코드에서 확인 할 수 있다시피 deleteAllById
는 Iterable<? extends ID>
를 순회하며 findById(id).ifPresent(this::delete);
를 순회 한다.
그리고 그 delete
에서는 친절하게도 각각의 id
로 EntityManager
를 뒤져보고 존재하지 않는 entity라면 if (existing == null) { return; }
를 통해 오류를 발생시키지 않고 알아서 걸러주기까지 한다!
(물론 Iterable<? extends ID>
에 null
이 포함된 경우에는 Assert
를 발생시키기는 한다.)
실제 테스트 코드를 작성하고 Iterable<? extends ID>
에 존재하는 id 1개와 존재하지않는 id 1개를 넣고 확인해본 결과 Query가 다음과 같이 나왔다.
Hibernate:
select
a1_0.id,
a1_0.b_id,
a1_0.label
from
a a1_0
where
a1_0.id=?
Hibernate:
select
a1_0.id,
a1_0.b_id,
a1_0.label
from
a a1_0
where
a1_0.id=?
2023-03-14T21:44:28.976+09:00 DEBUG 14968 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2023-03-14T21:44:28.976+09:00 DEBUG 14968 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(1361655427<open>)]
Hibernate:
delete
from
a
where
id=?
앞서 구현코드에서 확인했던 것처럼 select
가 2번 실행 될지라도 delete
는 존재하는 id에 대해서만 실행된다.
deleteAllByIdInBatch
의 코드마찬가지로 동일한 GitHub에서 가져온 코드이다.
public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
public static final String DELETE_ALL_QUERY_BY_ID_STRING = "delete from %s x where %s in :ids";
@Override
@Transactional
public void deleteAllByIdInBatch(Iterable<ID> ids) {
Assert.notNull(ids, "Ids must not be null");
if (!ids.iterator().hasNext()) {
return;
}
if (entityInformation.hasCompositeId()) {
List<T> entities = new ArrayList<>();
// generate entity (proxies) without accessing the database.
ids.forEach(id -> entities.add(getReferenceById(id)));
deleteAllInBatch(entities);
} else {
String queryString = String.format(DELETE_ALL_QUERY_BY_ID_STRING, entityInformation.getEntityName(),
entityInformation.getIdAttribute().getName());
Query query = em.createQuery(queryString);
/**
* Some JPA providers require {@code ids} to be a {@link Collection} so we must convert if it's not already.
*/
if (Collection.class.isInstance(ids)) {
query.setParameter("ids", ids);
} else {
Collection<ID> idsCollection = StreamSupport.stream(ids.spliterator(), false)
.collect(Collectors.toCollection(ArrayList::new));
query.setParameter("ids", idsCollection);
}
applyQueryHints(query);
query.executeUpdate();
}
}
@Override
@Transactional
public void deleteAllInBatch(Iterable<T> entities) {
Assert.notNull(entities, "Entities must not be null");
if (!entities.iterator().hasNext()) {
return;
}
applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities, em)
.executeUpdate();
}
entity가 복합키를 가졌을 때는 getReferenceById
를 사용해 Proxy 객체를 생성함으로써 select 문
이 발생하지 않는다.
그리고 복합키인 경우, 아닌 경우 모두 넘겨진 id가 null
만 아니라면 내부적으로 in
연산자를 사용해 1번의 Query로 모든 실행을 끝낸다.
위에서 deleteAllById
를 테스트할 때와 똑같이 존재하는 id 1개, 존재하지않는 id 1개를 Iterable<? extends ID>
에 넣어 실행해보면 아래와 같은 Query 1개만 발생한다.
Hibernate:
delete
from
a
where
id in(?,?)
in
연산자를 사용하기 때문에 있으나 없으나 정상적으로 작동한다.
사실 여기까지 알아보고나서 가장 먼저든 의문이 왜 둘을 굳이 나눴을까 이다.
select 문
을 발생시키지 않고 in
연산자로 1번에 처리하면 당연히 좋은거 아닌가?
그럼 그렇지 않은 경우에도 이점이 분명이 존재한다는 건가?
deleteAllById
의 장단점select
와 delete
를 실행해 성능이 저하됨 (N+1 문제)deleteAllByIdInBatch
의 장단점delete
Qeury 1번만 실행되기 때문에 성능이 향상됨이외에도 엔티티의 생명주기에 관련된 콜백, 이벤트 리스너를 등록할 수 있고 없고 동시성 제어가 가능하고 안하고 차이가 있다는데 정확한 레퍼런스를 찾을 수 없어서 적지는 못하겠다.
(생명주기, 버전관리(동시성 제어를 위한)는 각각의 엔티티에 대해서만 가능한데, InBatch를 사용하면 한 번에 처리하기 때문에 안된다고는 한다.)
즉, 요약하자면 deleteAllByIdInBatch
는 삭제한 엔티티에 대한 정보가 영속성 컨텍스트에 적용되지 않는다. 대신에 성능이 좋다! 이렇게 요약할 수 있을 것 같다.
이런 현상이 발생하는 예시를 코드는 아래와 같다.
public void testServiceFunc() {
ArrayList<Long> ids = new ArrayList<>();
for (long i = 3; i <= 4; i++) ids.add(i);
aRepository.findById(3L);
aRepository.deleteAllByIdInBatch(ids);
Optional<A> a = aRepository.findById(3L);
}
id가 3인 엔티티가 DB에 존재할 때 findById(3L)
를 통해 해당 엔티티를 영속성 컨텍스트에 등록해두면 deleteAllByIdInBatch
를 통해 삭제한 후에도 findById(3L)
이 아직 영속성 컨텍스트에 존재하는 엔티티를 가져오는 것을 확인 할 수 있었다.
그럼에도 나는 내 프로젝트에서는 deleteAllByIdInBatch
를 사용하려한다.
위에서 보여준 예시와 같이 현재 내 프로젝트에서는 한 번의 요청이 영속성 컨텍스트에 적용하고 삭제한 후에 다시 불러오는 경우도 없을 뿐더러, Query를 2N + 1개를 호출하냐 1 + 1개를 호출하냐는 의미있는 성능개선이라고 생각하기 때문이다.
추가로 영속성 컨텍스트에 적용이 안되는 것이 영 찝찝하다면 EntityManager
를 이용해 Flush()
메서드를 실행해 동기화하면 된다.