대용량 데이터 Delete - JPA deleteAll, deleteAllInBatch vs JDBC Batch Delete (IN절)

Hyunjin·2025년 4월 26일
0

📈 성능 최적화

목록 보기
3/3

💡  본 글은 프로젝트 진행 중 수행한 성능 개선 과정을 다룹니다.
따라서 기능의 의미보다는 문제의 본질에 중점을 두어 설명할 예정입니다.
자세한 정보와 원본 코드, PR 내용은 아래 링크에서 확인하실 수 있습니다.


문제 인식

withdrawalFeature→  회원 탈퇴 기능 :  계정 탈퇴 & 입장 내역 삭제

사용자의 회원 탈퇴 시, 계정 삭제를 위해 보유 데이터도 함께 제거되도록 구현하였다.
그러나 입장 내역이 일괄 삭제되면서, 다중 쿼리가 실행되는 현상이 확인되었다.

ERD

위처럼 엔티티는 User(1) : UserRoom(N) 의 연관 관계로 이루어져있다.
이는 사용자(User)가 탈퇴할 때, 소유한 모든 내역(UserRoom) 데이터들도 삭제됨을 의미한다.

즉, 회원 탈퇴의 경우, 사용자가 보유한 내역 수만큼 UserRoom을 삭제해야함.
ex)  3개의 내역을 가진 사용자가 회원 탈퇴 → 3개의 UserRoom 데이터 삭제

Service

// [ AuthServiceImpl.java ]

@Transactional
@Override
public void withdrawal() {

    // - 입장내역 조회 : 로그인 회원이 보유한 방 입장내역들 조회
    User user = userService.findLoginUser();
    List<UserRoom> userRoomList = user.getUserRoomList();
    
    // - 입장내역 삭제 : UserRoom 데이터들 DB에서 일괄 삭제 => Hard Delete
    // [ 방법 1. JPA deleteAll ] - 해결 전
    userRoomRepository.deleteAll(userRoomList);
    // [ 방법 2. JPA deleteAllInBatch ]
    // userRoomRepository.deleteAllInBatch(userRoomList);
    // [ 방법 3. JDBC Batch Delete ]
    // userRoomBatchRepository.batchDelete(userRoomList);
    // [ 방법 4. JDBC Batch Delete (IN절) ] - 해결 후
    // userRoomBatchRepository.batchDeleteIn(userRoomList);
    
    // - 회원 탈퇴 : 이름을 "탈퇴한 사용자"로 변경. 이외는 초기화 => Soft Delete
    user.deleteAccount();  // 채팅 기록은 유지되며, 상대방에게 "탈퇴한 사용자"로 표시됨.
    
    // - 탈퇴 완료 : AWS S3 업로드된 프로필 사진 제거
    awsS3Service.deleteImage(user.getImageUrl());
    user.updateImage(null);  // 기본 프로필 사진을 표시.

withdrawal()은 회원 탈퇴에 앞서, 여러 입장내역을 동시에 삭제해야 한다.
  따라서 JPA deleteAll를 통해, UserRoom 데이터들을 한 번에 Bulk 삭제하려 했다.

Query

→  입장내역 3개 동시 삭제 (JPA deleteAll)
  • 기댓값 : 쿼리 1회 발생
  • 실제값 : 쿼리 3회 발생

실행 결과, 의도와 다르게 Bulk 처리되지 않고 여러 개의 쿼리가 발생했다.
처음에는 delete 대신 deleteAll을 사용하였으므로, 한 번에 Bulk Delete가 이루어질 것이라고 생각했다.
그러나 실제로는 for문으로 각각 delete를 호출하듯이 쿼리가 다중 실행되었고, 결국 속도 저하를 초래했다.

이는 많은 내역을 가진 회원일수록, 성능에 치명적일 수 있음을 의미한다.
따라서 이러한 문제를 해결하고, 동시에 대용량 데이터 처리까지 가능하도록 성능 개선을 진행하였다.

  • JPA deleteAll  →  다중 쿼리 발생 !
  • JPA deleteAllInBatch
  • JDBC Batch Delete
  • JDBC Batch Delete (IN절)

특히, 여러 해결 방안 중 가장 효율적인 방법을 분석해보았다.

원인 분석

JPA deleteAll

Service

// [ 방법 1. JPA deleteAll ] - 해결 전
userRoomRepository.deleteAll(userRoomList);

// [ 방법 2. JPA deleteAllInBatch ]
// userRoomRepository.deleteAllInBatch(userRoomList);

// [ 방법 3. JDBC Batch Delete ]
// userRoomBatchRepository.batchDelete(userRoomList);

// [ 방법 4. JDBC Batch Delete (IN절) ] - 해결 후
// userRoomBatchRepository.batchDeleteIn(userRoomList);

이처럼 withdrawal() 서비스 메소드에서, JPA deleteAll를 호출하여 사용 중이다.
그렇다면 deleteAll() 메소드는 내부에서 어떻게 동작할까?

동작 원리

// [ JPA deleteAll() 내부 코드 ]

@Override
@Transactional
public void deleteAll(Iterable<? extends T> entities) {

	// 매개변수의 entities가 null인지 확인
    Assert.notNull(entities, ENTITIES_MUST_NOT_BE_NULL);

	// !!! 각 엔티티에 대해 delete() 개별 호출 !!!
    for(T entity : entities) {
        delete(entity);
    }
}

@Override
@Transactional
@SuppressWarnings("unchecked")
public void delete(T entity) {

	// 삭제할 엔티티가 null인지 확인
    Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);
    
    // 저장된 적 없는 새 엔티티는 무시
    if(entityInformation.isNew(entity)) {
        return;
    }

	// !!! 영속성 컨텍스트에 있는 엔티티라면, 삭제 O !!!
    if(entityManager.contains(entity)) {
        entityManager.remove(entity);
        return;
    }

    // !!! DB 조회 후 존재하는 엔티티라면, 삭제 O !!!
    Class<?> type = ProxyUtils.getUserClass(entity);
    T existing = (T) entityManager.find(type, entityInformation.getId(entity));
    if(existing != null) {
        entityManager.remove(entityManager.merge(entity));
    }
}

내부 코드를 살펴보면, deleteAll()은 for문을 순회하며 각 엔티티에 대해 delete()를 호출한다.
이때 EntityManager.remove()가 삭제 쿼리를 실행하여, 결국 엔티티 수만큼 다중 쿼리가 발생하게 된다.

이 실행 과정을 요약하면 다음과 같다.

  • 과정 1.  deleteAll() : 엔티티 리스트를 순회하며 delete() 호출
  • 과정 2.  delete() : EntityManager.remove()로 삭제 쿼리 실행
  • 과정 1+2.  for { delete(entity) } : 엔티티 수만큼 삭제 쿼리를 다중 실행

이 때문에 deleteAll(List<UserRoom>) 사용 시, UserRoom 엔티티마다 개별 쿼리가 발생하게 되고, 결과적으로 Bulk 방식으로 처리되지 않는 문제가 발생한 것이다.
→  즉, JPA deleteAll() 내부의 EntityManager.remove() 다중 호출이 문제의 원인이었다.

해결 방안

JPA deleteAllInBatch

JPA의 deleteAll() 외에 다른 삭제 메소드가 있는지 공식 문서를 살펴보던 중, deleteAllInBatch()라는 메소드를 새롭게 발견했다. 해당 설명에는 "단일 쿼리로 일괄 삭제한다" 는 내용이 명시되어 있었다.

JpaRepository 공식 문서 :
" void deleteAllInBatch(Iterable<T> entities)
주어진 엔티티를 일괄 삭제합니다. 즉, 단일 쿼리가 생성됩니다. "

  그래서 이번에는 JPA deleteAllInBatch를 대신 호출해보기로 했다.

Service

// [ 방법 1. JPA deleteAll ] - 해결 전
// userRoomRepository.deleteAll(userRoomList);

// [ 방법 2. JPA deleteAllInBatch ]
userRoomRepository.deleteAllInBatch(userRoomList);

// [ 방법 3. JDBC Batch Delete ]
// userRoomBatchRepository.batchDelete(userRoomList);

// [ 방법 4. JDBC Batch Delete (IN절) ] - 해결 후
// userRoomBatchRepository.batchDeleteIn(userRoomList);

기존 withdrawal() 서비스 메소드에서, JPA deleteAll 대신 JPA deleteAllInBatch을 호출하도록 변경할 수 있다. 그렇다면 deleteAllInBatch() 메소드는 내부에서 어떻게 동작할까?

동작 원리

// [ JPA deleteAllInBatch() 내부 코드 ]

@Override
@Transactional
public void deleteAllInBatch(Iterable<T> entities) {

	// 매개변수의 entities가 null인지 확인
    Assert.notNull(entities, "Entities must not be null");
	
    // 매개변수의 entities가 비어있는지 확인
    if(!entities.iterator().hasNext()) {
        return;
    }

	// !!! entities의 삭제 쿼리를 생성하고 실행 !!!
    applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities,
            entityManager)
            .executeUpdate();
}

public static <T> Query applyAndBind(String queryString, Iterable<T> entities, EntityManager entityManager) {

	// 쿼리 생성 : delete from %s x
	// public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
    Iterator<T> iterator = entities.iterator();

    ...

	// 쿼리 생성 : delete from user_room where
    String alias = detectAlias(queryString);  // user_room
    StringBuilder builder = new StringBuilder(queryString);
    builder.append(" where");  // where

	// 쿼리 생성 : delete from user_room where user_room_id=? or user_room_id=? or ...
    int i = 0;
    while(iterator.hasNext()) {
        iterator.next();
        builder.append(String.format(" %s = ?%d", alias, ++i));  // user_room_id=?
        if(iterator.hasNext()) {
            builder.append(" or");  // or
        }
    }
    Query query = entityManager.createQuery(builder.toString());

	// 쿼리 바인딩 : delete from user_room where user_room_id=17 or user_room_id=23 or ...
    iterator = entities.iterator();
    i = 0;
    while(iterator.hasNext()) {
        query.setParameter(++i, iterator.next());  // user_room_id=17
    }

	// 생성한 쿼리 반환
    return query;
}

내부 코드를 살펴보면, deleteAllInBatch()applyAndBind()를 호출해 쿼리를 생성하고 실행한다.
이때 applyAndBind()는 삭제할 모든 엔티티를 OR절로 이어붙여 하나의 쿼리로 만든다.
  이 쿼리는 한 번만 실행되므로, Bulk 처리로 기존 JPA deleteAll()의 다중 쿼리 문제를 해결할 수 있다.

✅  변경점

Query

위와 같이 OR절이 길게 이어진 하나의 단일 쿼리가 실행된다.
삭제할 입장내역(UserRoom) 수만큼 OR 조건이 늘어나며, 약 10000개 미만까지는 정상 실행된다.

비교 분석

JPA deleteAll  →  JPA deleteAllInBatch

JPA deleteAll JPA deleteAllInBatch
Bulk 처리 X O
DB 접근 횟수 N회 1회
쿼리 실행 횟수 N회 1회
StackOverflow X 위험

이처럼 쿼리는 분명 개선되었지만, 여전히 몇 가지 문제가 남아 만족스럽지 않았다.

🚨  문제점

1.  OR절은 IN절보다 데이터베이스 성능이 비효율적
OR절은 각 조건을 개별 처리하여, DB가 여러 번 조건을 비교해 성능이 저하될 수 있다.
반면, IN절은 여러 값을 한 번에 묶어 처리하므로 더 효율적이다.

2.  너무 많은 데이터를 한 번에 삭제하면, StackOverflow 발생 가능
복잡한 OR 쿼리를 처리하는 과정에서 ORM의 내부 쿼리 호출이 과도하게 증가하고,
이로 인해 호출 스택이 과도하게 깊어져 스택 메모리를 초과할 수 있다.

  데이터 100개, 1000개 :  실행 성공

  데이터 10000개 :  StackOverflow 에러 발생

JDBC Batch Delete

JPA deleteAllInBatchJPA deleteAll의 단점을 보완하며 Bulk 연산을 성공시켰지만,
데이터 수가 10000개를 초과하면 StackOverflow가 발생하는 문제가 있었다.

"Bulk 처리는 가능하되, OR절 없이 구현할 수 있는 다른 방법은 없을까?"

고민하던 중, '다수 인원의 동시입장' 기능에서 Bulk Insert를 개선했던 경험이 떠올랐다.
[ 이전 글 : "대용량 데이터 Insert - JPA saveAll vs JDBC Batch Insert" ]

  이처럼, JDBC batchUpdate()를 활용한 JDBC Batch Delete를 구현해보기로 했다.

Service

// [ 방법 1. JPA deleteAll ] - 해결 전
// userRoomRepository.deleteAll(userRoomList);

// [ 방법 2. JPA deleteAllInBatch ]
// userRoomRepository.deleteAllInBatch(userRoomList);

// [ 방법 3. JDBC Batch Delete ]
userRoomBatchRepository.batchDelete(userRoomList);

// [ 방법 4. JDBC Batch Delete (IN절) ] - 해결 후
// userRoomBatchRepository.batchDeleteIn(userRoomList);

이번에는 withdrawal() 내 JPA deleteAllInBatch 대신 JDBC Batch Delete를 호출해보자.
이러한 JDBC Batch Delete 메소드의 구현 코드는 무엇이며, 어떻게 동작하는걸까?

Repository

// [ UserRoomBatchRepository.java ]

private final JdbcTemplate jdbcTemplate;

public void batchDelete(List<UserRoom> userRoomList) {
    String sql = "DELETE FROM user_room WHERE user_room_id = ?";

	// !!! batchUpdate() : 여러 SQL 쿼리를 한 번에 묶어서 DB에 전송하는 방식 !!!
    jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {

        @Override
        public void setValues(PreparedStatement ps, int i) throws SQLException {
            UserRoom userRoom = userRoomList.get(i);
            ps.setLong(1, userRoom.getId());
        }

        @Override
        public int getBatchSize() {
            return userRoomList.size();
        }
    });
}

이는 batchUpdate()를 활용해 여러 SQL 쿼리를 한 번에 묶어 DB에 전송하는 방식으로 동작한다.
즉, 각 UserRoom 엔티티마다 삭제 쿼리를 생성하고, 이를 하나로 묶어 단일 DB 요청으로 처리하는 것이다. 이로 인해 DB 접근 횟수는 한 번으로 줄어들지만, 쿼리는 여전히 개별적으로 실행된다.
  OR절 없이 쿼리를 개별 실행하므로, JPA deleteAllInBatch()의 StackOverflow를 피할 수 있다.

✅  변경점

Query

보다시피 여러 개의 쿼리가 하나의 DB 요청으로 묶여 전송된다.
사진에 '쿼리 1회'라고 표기되어 있는데, 정확히 말하면 이는 DB 접근 횟수를 의미한다. 즉, DB에는 한 번만 접근하고, 그 안에서 쿼리가 다중 실행된다는 것이다.
또한 OR절을 사용하지 않기 때문에, 데이터 수가 10000개를 초과해도 문제없이 정상 실행된다.

비교 분석

JPA deleteAllInBatch  →  JDBC Batch Delete

JPA deleteAllInBatch JDBC Batch Delete
Bulk 처리 O O
DB 접근 횟수 1회 1회
쿼리 실행 횟수 1회 N회
StackOverflow 위험 X

그 결과 StackOverflow는 방지했지만, 대신 쿼리 실행 횟수가 증가한 것을 확인할 수 있다.

🚨  문제점

-  DB 접근은 한 번이나, 내부적으로 다중 쿼리 발생
JDBC Batch Delete는 여전히 각 엔티티별로 개별 쿼리를 실행한다. DB 접근은 한 번만 이루어지지만, 내부적으로 엔티티 수만큼 쿼리가 실행되므로 대용량 데이터 처리 시 여전히 비효율적이다. 이로 인해 DELETE문이 반복되어, 쿼리 최적화 측면에서도 개선이 필요하다.

JDBC Batch Delete (IN절)

JPA deleteAllInBatch는 내부적으로 OR 조건을 사용하기 때문에 성능 문제가 있었고,
JDBC Batch Delete는 DB 내부에서 쿼리를 다중 실행한다는 점이 비효율적이었다.

이 문제를 해결하기 위해 아래 두 가지에 주목했다 :

  • OR절 대신 IN절을 사용하자.
  • IN절이라도 오버헤드를 줄이도록 BatchSize를 도입하자.

  이를 바탕으로,  JDBC Batch Delete (IN절 + BatchSize) 방식을 새롭게 고안했다.

Service

// [ 방법 1. JPA deleteAll ] - 해결 전
// userRoomRepository.deleteAll(userRoomList);

// [ 방법 2. JPA deleteAllInBatch ]
// userRoomRepository.deleteAllInBatch(userRoomList);

// [ 방법 3. JDBC Batch Delete ]
// userRoomBatchRepository.batchDelete(userRoomList);

// [ 방법 4. JDBC Batch Delete (IN절) ] - 해결 후
userRoomBatchRepository.batchDeleteIn(userRoomList);

이제 withdrawal()에서 기존의 JDBC Batch Delete 대신 JDBC Batch Delete (IN절)을 호출해보자.
그렇다면 JDBC Batch Delete (IN절) 메소드는 어떻게 구현하며, 실제로는 어떻게 동작할까?

Repository

// [ UserRoomBatchRepository.java ]

private final JdbcTemplate jdbcTemplate;
private static final int BATCH_SIZE = 1000;  // 배치 크기 설정 (메모리 오버헤드 방지)

public void batchDeleteIn(List<UserRoom> userRoomList) {

    for (int i=0; i<userRoomList.size(); i+=BATCH_SIZE) {
        List<Long> batchList = userRoomList.subList(i, Math.min(i+BATCH_SIZE, userRoomList.size()))
                .stream()
                .map(UserRoom::getId)
                .collect(Collectors.toList());

        String sql = String.format("DELETE FROM user_room WHERE user_room_id IN (%s)",  // IN절 활용
                batchList.stream()
                        .map(String::valueOf)
                        .collect(Collectors.joining(",")));

		// !!! update() : 단일 SQL 쿼리를 DB에 전송하는 방식 (BatchSize로 나누어 전송) !!!
        jdbcTemplate.update(sql);
    }
}

이는 update()를 활용해 단일 SQL 쿼리를 DB에 전송하는 방식으로 동작한다.
즉, UserRoom 엔티티들의 삭제 쿼리를 IN절로 통합해 하나의 쿼리로 만들고, 이를 단일 DB 요청으로 처리하는 것이다. 또한, BatchSize로 쿼리를 일정 크기씩 나눠 실행함으로써 메모리 오버헤드도 줄일 수 있다.
  단일 쿼리로 처리되기 때문에, 기존 JDBC Batch Delete의 다중 쿼리 문제를 해결할 수 있다.

✅  변경점

Query

위는 총 10000개의 데이터를 사용해 실행한 결과이다. BatchSize를 1000으로 설정했기 때문에, 쿼리는 총 (N/BatchSize = 10000/1000 = 10)회 실행된 것을 확인할 수 있다.
또한 IN절을 사용해 효율적으로 쿼리를 수행하여, 대용량 데이터에도 원활하게 정상 실행된다.

비교 분석

JDBC Batch Delete  →  JDBC Batch Delete (IN절 + BatchSize)

JDBC Batch Delete JDBC Batch Delete (IN절)
Bulk 처리 O O
DB 접근 횟수 1회 1회
쿼리 실행 횟수 N회 1회 or (N/BatchSize)회
StackOverflow X X

표에서 확인할 수 있듯, 기존 문제점들이 모두 해결되며 최적화된 Bulk Delete가 가능해졌다.

성능 비교

Test Code

// [ AuthServiceTest.java ]

@Test
@DisplayName("회원 탈퇴 & 입장내역 동시삭제 - UserRooms Batch Delete")
void withdrawal_Test() {

    // ========== < 입장내역 준비 - UserRooms 생성 > ========== //
    
    // - delete 데이터량 설정
    Integer inputUserRoomsCount = 10000;  // UserRooms 더미데이터 : 10000개
    Long userId = 1L;
    Long roomId = 1L;

    // - Test 이전의 UserRoom 개수 측정 (더미데이터 생성 전)
    Integer startUserRoomsCount = userRoomRepository.findAll().size();  // 초기 UserRooms 개수

    // - 입장내역 생성 : UserRooms 더미데이터 생성
    List<UserRoom> userRoomList = makeFakeUserRooms(userId, roomId, inputUserRoomsCount);
    userRoomBatchRepository.batchInsert(userRoomList);

    // - 탈퇴할 회원 로그인
    String jwt = tokenProvider.generateAccessToken(userId, Role.ROLE_USER);
    Authentication authentication = tokenProvider.getAuthentication(jwt);  // JWT 인증
    SecurityContextHolder.getContext().setAuthentication(authentication);  // 인증정보 설정

    // ========== < 회원 탈퇴 - UserRooms 삭제 > ========== //
    
    // - 입장내역 동시삭제 : UserRooms 더미데이터 삭제
    LocalDateTime startTime = LocalDateTime.now();  // 동시삭제 시작시각 기록
    authService.withdrawal();
    LocalDateTime endTime = LocalDateTime.now();    // 동시삭제 종료시각 기록
    
    // - 동시삭제 실행시간 출력
    System.out.printf("\n< JDBC Batch Delete 사용 (JPA deleteAll X, JPA deleteAllInBatch X) >\n");
    String printTime = getPrintTime(startTime, endTime);
    System.out.printf("- %d개 방의 회원탈퇴 실행시간: %s\n", inputUserRoomsCount, printTime);
    
    // ========== < 종료 - Test 검증 > ========== //

    // - Test 이후의 UserRoom 개수 측정 (더미데이터 삭제 후)
    Integer endUserRoomsCount = userRoomRepository.findAll().size();  // 롤백후 UserRooms 개수

    // - DB 롤백 검증
    assertThat(startUserRoomsCount).isEqualTo(endUserRoomsCount);  // UserRooms 롤백여부 검증
}

public String getPrintTime(LocalDateTime startTime, LocalDateTime endTime) {
    Duration duration = Duration.between(startTime, endTime);  // 실행시간 계산
    long milliseconds = duration.toMillis();  // 단위: 밀리초(ms)
    double seconds = milliseconds / 1000.0;   // 단위: 초(s)
    String printTime = String.format("%dms (%.2fs)", milliseconds, seconds);
    return printTime;
}

이 Test는 특정 사용자(userId=1)가 회원 탈퇴를 통해 대량의 입장내역을 삭제하는 상황을 설정하고,
delete 성능 벤치마킹을 수행한다. 주요 목적은 대량의 입장내역이 동시 삭제될 때,
JPA deleteAll, JPA deleteAllInBatch, JDBC Batch Delete, JDBC Batch Delete (IN절) 방식에서의 성능을 비교하는 것이다.

테스트는 크게 다음 세 가지 단계로 진행된다.

  1.  대용량의 입장내역 생성
  2.  동시 삭제 및 소요시간 측정
  3.  DB 롤백 및 데이터 정합성 검증

결과적으로 대용량 데이터 환경에서 Bulk Delete 시, 발생하는 쿼리 변화와 성능 향상 여부를 분석하고, 이를 실제 지표를 통해 평가하고자 한다.

쿼리 개선

❌  JPA deleteAll
(Data : 10000)
JPA deleteAllInBatch
(Data : 1000, 10000 ≤ StackOverflow)
-  DB 접근 :  N회 → Bulk 동작 X
-  쿼리 실행 :  N회 → 성능 저하
⇒  입장 내역 수만큼 DB 접근 발생
-  DB 접근 :  1회 → Bulk 동작 O
-  쿼리 실행 :  1회 → 성능 향상
⇒  단일 쿼리 발생
JDBC Batch Delete
(Data : 10000)
✅  JDBC Batch Delete (IN절)
(Data : 10000)
-  DB 접근 :  1회 → Bulk 동작 O
-  쿼리 실행 :  N회 → 성능 저하
⇒  DB에는 한 번만 접근하나, 내부에서 다중 쿼리 실행
-  DB 접근 :  1회 → Bulk 동작 O
-  쿼리 실행 :  1회 or (N/BatchSize)회 → 성능 향상
⇒  BatchSize를 고려한 단일 쿼리 발생

사용자(User)의 회원 탈퇴로 10000개의 입장내역(UserRoom)을 동시 삭제하는 경우

  • 기존 JPA deleteAll 방식 :
    사용자가 보유한 내역 수에 비례하여, 매번 쿼리가 실행되었다.   (10000회 or N회)
  • 신규 JDBC Batch Delete (IN절) 방식 :
    내역 수와 무관하게, 쿼리가 최소 한 번만 실행되었다.   (1회 or (N/BatchSize)회)

⇒  JDBC Batch Delete (IN절) 방식으로 리팩토링하여, Bulk 처리를 통해 발생 쿼리를 개선할 수 있었다.

속도 향상

❌  JPA deleteAll
(Data : 10 ~ 10000)
JPA deleteAllInBatch
(Data : 10 ~ 10000)
-  EntityManager.remove() 다중 호출 → Bulk 동작 X
⇒  단점 :  Bulk 미동작으로, 일괄 삭제 불가
-  OR절로 단일 쿼리 실행 → Bulk 동작 O
⇒  단점 :  OR 조건 과다 시, StackOverflow 위험
JDBC Batch Delete
(Data : 10 ~ 10000, 100만)
✅  JDBC Batch Delete (IN절)
(Data : 10000, 100만)
-  다중 쿼리를 묶어 전송 → Bulk 동작 O
⇒  단점 :  결국은 DB 내부에서 다중 쿼리 실행
-  IN절로 단일 쿼리 실행 → Bulk 동작 O
⇒  장점 :  IN절 사용으로, OR절보다 안정적이고 효율적
  • JPA deleteAlldeleteAllInBatchJDBC Batch DeleteBatch Delete (IN절)
  • 10개 :  0.33초 → 0.29초 → 0.26초   (1.3배 ↑)
  • 100개 :  0.53초 → 0.36초 → 0.34초   (1.5배 ↑)
  • 1000개 :  0.74초 → 0.65초 → 0.41초   (1.8배 ↑)
  • 10000개 :  2.08초 → ERROR → 1.39초 → 0.75초   (2.7배 ↑, 64% ↑)
  • 100만개 :  TIMEOUT → ERROR → 181.60초 → 91.95초

위의 속도 향상 정도를 비교해 보면, 데이터의 양이 많을수록 향상의 폭이 더욱 커지는 것을 알 수 있다.
따라서 대용량 데이터를 다룰 때 그 효과가 극대화되며, 해당 용도에 매우 적합하고 우수한 방법임을 나타낸다.
이를 실무에 적용해 더 많은 데이터를 처리한다면, 놀라운 성능 향상을 기대할 수 있을 것이다.
⇒  JDBC Batch Delete (IN절) 방식으로 리팩토링하여, 속도를 10000개 기준 64% 향상시킬 수 있었다.

마치며

느낀 점

이번 경험으로 ORM의 편리함 뒤에 숨겨진 내부 동작 원리를 이해하는 것의 중요성을 깨달았다.
JPA를 단순히 사용하는 데 그치지 않고, deleteAll의 동작 방식과 deleteAllInBatch가 StackOverflow를 발생시키는 원인을 직접 분석하며 실제 개발에 어떻게 적용되는지 파악할 수 있었다.

또한, 성능 최적화는 단순히 '동작하게 만드는 것'이 아니라, '효율적으로 동작하게 만드는 것'까지 고려해야 한다는 점을 다시 한 번 확실히 느꼈다. 대용량 데이터 처리에서는 작은 최적화가 큰 성능 차이를 만들 수 있기에, 앞으로도 항상 최적화 가능성을 염두에 두고 개발에 적극 반영할 것이다.

참고 링크

profile
Success is the sum of small efforts.

0개의 댓글