Spring Batch - 배치의 삭제 로직 성능 최적화

오늘은 Spring Batch를 사용하여 대량의 데이터를 삭제할 때 발생하는 성능 문제와 이를 개선한 과정을 공유하고자 합니다.

 

기존 코드와 문제점

저는 일정 시간이 지난 UserCertLog 엔티티의 데이터를 삭제하는 작업을 진행하고 있었습니다. 초기에는 다음과 같이 코드를 작성했습니다.

/**
 * nice 인증 결과 로그인 UserCertLog 삭제 처리
 * 매일 새벽 12시 30분에 도는 스케줄러 입니다.
 */
@Scheduled(cron = "0 30 0 * * ?")
public void certLogRemove() {
    log.info("---certLogRemove start!!---");
    deleteService.deleteUserCertLog();
    log.info("---certLogRemove finish!!---");
}
@Transactional
public void deleteUserCertLog() {
    // 현재 기준 regDateTime 값이 24시간 이전인 내용의 삭제
    LocalDateTime cutoffDateTime = LocalDateTime.now().minusDays(1);
    userCertLogRepository.deleteByRegDatetimeBefore(cutoffDateTime);
}
public interface UserCertLogRepository extends JpaRepository<UserCertLog, UUID> {
    void deleteByRegDatetimeBefore(LocalDateTime regDatetime);
}

 

이 코드는 regDatetime이 24시간 이전인 모든 데이터를 삭제하는 기능을 수행합니다. 그러나 실행해 보니 데이터가 많은 경우 삭제 속도가 매우 느렸습니다. 로그를 확인해 보니 한 번에 하나의 행(row)씩 삭제가 이루어지고 있었습니다.


문제 원인 분석

Spring Data JPA에서 제공하는 deleteBy... 메소드는 기본적으로 각 엔티티를 개별적으로 조회한 후 삭제합니다.

 

이는 영속성 컨텍스트를 통해 엔티티 상태를 관리하기 때문에 발생하는 현상으로, 대량의 데이터를 삭제할 때는 성능 저하를 일으키는 주요 원인입니다.

 

특히, 데이터량이 많을수록 이러한 방식은 비효율적이며, 트랜잭션 시간이 길어져 데이터베이스에 부하를 줄 수 있습니다.


개선 방법

1. 벌크 삭제 (Bulk Delete) 사용

Spring Data JPA는 JPQL 또는 네이티브 쿼리를 사용하여 벌크 연산을 지원합니다. 이를 활용하면 한 번의 쿼리로 대량의 데이터를 삭제할 수 있습니다

 

JPQL 사용

public interface UserCertLogRepository extends JpaRepository<UserCertLog, UUID> {

    @Modifying
    @Query("DELETE FROM UserCertLog u WHERE u.regDatetime < :cutoffDateTime")
    int deleteByRegDatetimeBefore(@Param("cutoffDateTime") LocalDateTime cutoffDateTime);
}
@Transactional
public void deleteUserCertLog() {
    LocalDateTime cutoffDateTime = LocalDateTime.now().minusDays(1);
    userCertLogRepository.deleteByRegDatetimeBefore(cutoffDateTime);
}

네이티브 쿼리 사용

public interface UserCertLogRepository extends JpaRepository<UserCertLog, UUID> {

    @Modifying
    @Query(value = "DELETE FROM mtn_account.usr_user_cert_log WHERE reg_datetime < :cutoffDateTime", nativeQuery = true)
    int deleteByRegDatetimeBeforeNative(@Param("cutoffDateTime") LocalDateTime cutoffDateTime);
}
@Transactional
public void deleteUserCertLog() {
    LocalDateTime cutoffDateTime = LocalDateTime.now().minusDays(1);
    userCertLogRepository.deleteByRegDatetimeBeforeNative(cutoffDateTime);
}

 

2. 배치(Batch) 삭제 사용

벌크 삭제가 아닌 배치 처리를 통해 데이터를 일정량씩 나누어 삭제할 수도 있습니다. 이 방법은 비즈니스 로직상 한 번에 모든 데이터를 삭제하면 안 되는 경우에 유용합니다.

@Transactional
public void deleteUserCertLog() {
    // 현재 기준 regDateTime 값이 24시간 이전인 내용의 삭제
    LocalDateTime cutoffDateTime = LocalDateTime.now().minusDays(1);
    List<UserCertLog> idsToDelete = userCertLogRepository.findIdsByRegDatetimeBefore(cutoffDateTime);

    log.info("deleteUserCertLog 처리할 총인원 수 : [{}]",idsToDelete.size());

    int batchSize = 100;
    for (int i = 0; i < idsToDelete.size(); i += batchSize) {
        int end = Math.min(i + batchSize, idsToDelete.size());
        List<UserCertLog> batchUserCertLog = idsToDelete.subList(i, end);
        log.info("deleteUserCertLog 처리할 총인원 수 : [{}]",batchUserCertLog.size());
        log.info("batchIds : [{}]",batchUserCertLog);

        userCertLogRepository.deleteAllInBatch(batchUserCertLog);
    }
}
@Query("SELECT u FROM UserCertLog u WHERE (u.regDatetime < :cutoffDateTime or u.regDatetime is null)")
List<UserCertLog> findIdsByRegDatetimeBefore(@Param("cutoffDateTime") LocalDateTime cutoffDateTime);

개선 후 성능

개선된 코드를 적용한 후, 삭제 속도가 현저하게 빨라졌습니다. 벌크 삭제의 경우 데이터베이스에 단 한 번의 삭제 쿼리만 전달되므로, 수만 건의 데이터를 삭제하는 데도 몇 초밖에 걸리지 않았습니다.

 

배치 삭제를 사용한 경우에도 이전의 한 행씩 삭제하던 방식에 비해 성능이 크게 향상되었으며, 필요에 따라 배치 크기를 조절하여 시스템 부하를 관리할 수 있었습니다.

 

저의 경우 필요에 따라 배치 크기를 조절하기 위해 배치 삭제를 최종적으로 적용하였습니다.


결론

Spring Data JPA를 사용할 때 대량의 데이터를 삭제해야 하는 경우, 기본적인 deleteBy... 메소드는 성능 저하를 일으킬 수 있습니다. 이때 JPQL 또는 네이티브 쿼리를 활용한 벌크 삭제를 통해 효율적으로 데이터를 삭제할 수 있습니다. 또한, 비즈니스 로직에 따라 배치 처리를 통해 삭제를 진행할 수도 있습니다.

 

대량의 데이터 처리 시에는 항상 성능을 고려하여 적절한 방법을 선택하는 것이 중요합니다. 이번 경험을 통해 애플리케이션의 성능을 최적화하는 방법에 대해 더욱 깊이 이해하게 되었습니다.