개요

사용자 기여 캘린더 기능을 구현하면서 30일 이전 데이터는 매일 정각 00시 05분에 모두 삭제하는 스케줄링을 구현하였습니다.

사용자 기여 데이터는 사용자가 게시글, 댓글, 투표, 좋아요 기능을 하루에 한번이라도 이용했다면 생성되게 됩니다.

즉, 사용자가 100명일 경우 모든 사용자가 하나의 기능을 이용했다면 최대 100개의 데이터를 삭제해야합니다.

이처럼 매일 정각 00시 05분에 대량의 데이터가 한번에 삭제되기 때문에 기존의 코드를 개선해보았습니다.



사용자 지정 쿼리

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = "delete from UserHistory where actionDateTime < :localDateTime")
void deleteAllByActionDateTimeBefore(@Param("localDateTime") LocalDateTime localDateTime);



최적화 과정

기존 코드에서 사용하는 deleteAllByIdIterable<?extends ID> ids를 순회하면서 delete 쿼리를 발생시킵니다.

deleteAllById 내부 코드

@Override
@Transactional
public void deleteAllById(Iterable<? extends ID> ids) {

    Assert.notNull(ids, "Ids must not be null!");

    for (ID id : ids) {
        deleteById(id);
    }
}

즉, ids의 크기가 100이면 100개의 쿼리가 실행되는 것 입니다.

deleteAllById 호출 결과

이처럼 여러번의 삭제 쿼리를 발생시키는 문제를 해결하기 위해서 deleteAllByIdInBatch를 사용해서 문제를 해결해보았습니다.

deleteAllByIdInBatch 내부 코드

@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);
        }

        query.executeUpdate();
    }
}

내부 코드를 살펴보면 복합키가 아닌 경우에는 em.createQuery 메서드를 통해 DELETE_ALL_QUERY_BY_ID_STRING 쿼리가 실행되는 것을 확인할 수 있다.


그리고 QueryUtils클래스의 DELETE_ALL_QUERY_BY_ID_STRING 의 상수 값을 확인해보면 delete from %s where %s in :ids 값으로, in절로 여러개의 데이터를 삭제하는 것을 알 수 있다.

deleteAllByIdInBatch 호출 결과

@Query

하지만, deleteAllByIdInBatch 내부 코드를 살펴보았을 때, createQuery 메서드를 통해 in절을 호출하는 방법이라면, @Query 어노테이션을 사용해서 직접 사용자 지정 쿼리를 작성하는 방법과 동일하다. 아래에서 테스트를 통해 성능적인 측면에서 얼마나 차이나는지 확인해보자.

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = "delete from UserHistory where actionDateTime < :localDateTime")
void deleteAllByActionDateTimeBefore(@Param("localDateTime") LocalDateTime localDateTime);

그리고 영속성 컨텍스트와 DB 싱크를 위해서 clearAuthmatically=true, flushAutomatically=true 옵션을 추가해주었다.



성능 테스트

deleteAllById

deleteAllByIdInBatch

deleteAllByActionDateTimeBefore

정리

이처럼 입력 값 없이 데이터를 삭제(스케줄링)할 때 JPA에서 기본으로 제공해주는 메서드를 사용하면 데이터 조회 후 삭제 쿼리를 수행해야한다.

하지만, 커스텀 쿼리를 작성하면 데이터 조회 없이 바로 DB에 삭제 쿼리를 발생시킬 수 있다.
따라서, deleteAllById의 N번 삭제 쿼리를 한번의 쿼리로 해결하여 1000건 기준 431ms -> 18ms 성능 개선을 해보았다.

profile
Back-End Developer

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN