Bulk 연산의 문제점은 없을까?

2025. 8. 7. 01:41·데이터베이스

들어가며

여러 건의 데이터를 변경하거나 추가 또는 삭제를 할 때, 단건 쿼리를 반복 실행하는 방식 대신 Bulk Update 쿼리를 사용한다면 단건 처리보다 훨씬 효율적이고 성능도 향상시킬 수 있다. 하지만 여기서 한 가지 의문이 생겼다. 만약 처리 대상 데이터가 수십만 건 이상으로 많아진다면 어떻게 될까? Bulk 연산은 만능인가? 이번 글에서는 별크 연산의 문제점에 대해 살펴보고자 한다.

Bulk 연산이란?

Bulk 연산이란 데이터베이스에서 다수의 데이터를 한 번에 삽입, 수정 또는 삭제하는 작업을 의미한다. 예를 들어 수십만 건의 데이터를 하나씩 반복하여 처리하는 대신, 한 번의 SQL 실행으로 대량의 데이터를 처리하는 것이다. 이러한 방식은 단건 처리와 비교했을 때 여러 면에서 효율적이다.

단건 연산의 경우, 각 작업마다 트랜잭션이 시작되고 종료되며, 그 과정에서 데이터베이스의 CPU, 메모리, 디스크 I/O 등 다양한 리소스가 반복적으로 사용된다. 이러한 반복은 데이터베이스 인스턴스의 부하를 증가시키고, 처리 시간이 늘어나는 원인이 된다. 특히 클라우드 환경에서와 같이 사용량 기반의 과금 모델에서는 처리 시간이 길어질수록 비용도 함께 증가한다.

반면 Bulk 연산은 한 번의 실행으로 여러 건의 데이터를 처리하기 때문에 트랜잭션 횟수가 줄어든다. 그 결과 데이터베이스 리소스 사용량이 줄어들고, 네트워크 왕복 횟수도 감소한다. 대량 데이터를 다루더라도 효율적으로 작업이 완료되며, 전체 처리 속도가 향상되고 리소스 사용이 최적화되어 비용 절감 효과를 얻을 수 있다.

대량 Bulk 연산의 문제

Bulk 연산은 성능상의 장점이 크지만, 다음과 같은 단점과 주의사항이 존재한다.

 

트랜잭션 크기 증가

한 번에 많은 데이터를 처리하기 때문에 트랜잭션이 매우 커질 수 있다. 트랜잭션 크기가 커지면 롤백이 필요할 때 메모리와 CPU를 포함한 리소스 소모가 급격히 증가한다. 롤백 과정에서 대량의 변경 이력을 되돌려야 하므로 데이터베이스의 처리 속도가 느려지고, 심한 경우 다른 작업에도 영향을 줄 수 있다. 특히, 모든 데이터 변경 사항이 하나의 트랜잭션에 묶이면, DB는 이를 모두 롤백할 수 있도록 로그를 보관해야 한다. 이 로그 저장으로 인해 디스크 쓰기량이 증가하고, 트랜잭션 커밋이나 롤백이 오래 걸릴 수도 있다.

 

롤백 시 부담

처리 과정에서 오류가 발생하면 전체 트랜잭션이 롤백되어야 한다. 이는 단건 연산에서의 롤백보다 훨씬 큰 작업량을 발생시키며, 복구 시간도 그만큼 길어진다. 특히 서비스 운영 중이라면 장애 지속 시간에 직접적인 영향을 미친다. 같은 맥락으로 부분 실패 처리의 어려움이 존재한다.  Bulk 연산은 기본적으로 전체 데이터 집합을 하나의 묶음으로 처리하기 때문에, 특정 행에서만 문제가 발생하더라도 나머지 정상 행까지 롤백되는 경우가 많다. 따라서 대량 데이터 처리 시 일부만 성공시키고 일부만 재처리하는 로직을 추가로 구현해야 하는 경우가 있다.

 

인덱스와 DB 락 점유

대량의 데이터가 삽입·수정될 때 인덱스 재정렬과 제약 조건 검증이 한 번에 이루어진다. 이는 단건 처리보다 순간적으로 훨씬 큰 I/O 부하를 발생시킬 수 있다. 대량의 UPDATE/INSERT 쿼리는 오랜 시간 동안 테이블 락이나 행 락을 점유할 수 있다. 이로 인해 다른 트랜잭션이 블로킹되고, 동시성 처리에 심각한 영향을 미칠 수 있다.

 

메모리 부담

RDBMS는 쿼리 실행 중 임시 데이터나 인덱스 작업 결과를 메모리에 저장한다. 이로 인해 대량 데이터 처리 시, 메모리가 급격히 증가하여, 디스크 스와핑이 발생하거나 쿼리가 느려질 수 있다. JPA나 Hibernate와 같은 ORM을 사용하는 경우, 영속성 컨텍스트에 모든 엔티티가 쌓여서 OOM까지 발생할 수도 있다.

메모리 부담과 MySQL 버퍼 풀(Buffer Pool)의 역할
MySQL의 InnoDB 스토리지 엔진은 데이터 처리 성능을 높이기 위해 버퍼 풀(Buffer Pool)이라는 메모리 영역을 활용한다. 버퍼 풀은 디스크에서 읽어온 데이터 페이지와 인덱스 페이지를 메모리에 캐싱하여, 반복적인 디스크 I/O를 줄이고 쿼리 처리 속도를 향상시키는 역할을 담당한다. 특히 대량의 데이터를 처리하는 Bulk 연산에서는 버퍼 풀의 크기와 효율적인 활용 여부가 전체 성능에 큰 영향을 미친다.

Bulk 연산이 실행되면 많은 데이터 페이지가 동시에 변경되고, 이 변경 내용은 우선 버퍼 풀 내 메모리에 저장된다. 이후 버퍼 풀에 있는 변경된 페이지들은 일정 시점마다 디스크에 플러시되어 영구 저장된다. 이 과정에서 버퍼 풀은 디스크 I/O 부하를 완화하는 중요한 캐시 역할을 수행하지만, 만약 버퍼 풀 크기가 처리해야 할 데이터 양에 비해 부족하면 문제가 발생한다.

버퍼 풀이 충분하지 않을 경우, InnoDB는 디스크에서 필요한 데이터 페이지를 자주 읽어 와야 하므로 디스크 I/O 병목이 발생한다. 또한 버퍼 풀 내 공간 확보를 위해 덜 자주 사용되는 페이지를 디스크로 플러시하는 페이지 교체 작업이 잦아지며, 이로 인해 쿼리 처리 시간이 길어질 수 있다. 결국 대용량 Bulk 연산이 버퍼 풀 크기를 초과하면, 메모리와 디스크 간 잦은 데이터 이동으로 인해 성능 저하가 불가피하다.

벌크 연산의 이점을 살리면서 부작용을 최소화하는 방법

대량의 데이터를 한꺼번에 처리하는 벌크 연산은 효율적이지만, 한 번에 너무 많은 데이터를 다루는 특성 때문에 트랜잭션 크기 증가, 메모리 부담, 롤백 시 리소스 과다 사용, 데이터베이스 락 경합 등의 부작용이 발생한다. 이러한 문제는 데이터가 지나치게 많아지면서 시스템에 부하가 집중되기 때문에 발생한다.

이를 해결하기 위한 대표적인 전략이 바로 청크(Chunk) 분할 처리 기법이다. 데이터를 여러 개의 작고 관리 가능한 단위로 나누어 처리함으로써, 벌크 연산이 갖는 본질적인 장점은 유지하되, 부작용은 크게 완화할 수 있다.

청크 기반 처리

데이터를 일정 개수 단위로 잘라서 처리하고, 각 청크마다 별도의 트랜잭션을 통해 커밋하는 방식이다. 예를 들어, 100,000건의 데이터를 1000건씩 100회에 걸쳐 처리하는 식이다. Spring Batch 등에서 기본적으로 제공하는 데이터 처리 방식이다.

한 번에 메모리에 올리는 데이터 양이 제한되기 때문에 메모리 점유를 효과적으로 줄일 수 있어 OOM 오류 위험이 낮아진다. 오류 발생 시 해당 청크 단위로만 롤백하거나 재처리가 가능하여 장애 복구 역시 상대적으로 쉬워진다. 데이터를 분할해서 처리하기 때문에 트랜잭션 크기 역시 작아져서 롤백 부담과 락 경합도 감소하게 된다. 하지만, 커밋 횟수가 증가하여 네트워크 왕복 횟수가 늘어난다는 단점이 존재한다. 하지만 이 단점은 청크 분할 처리 전반에서 발생하는 특성이며, 단건 반복 쿼리 대비 훨씬 효율적이다.

오프셋(Offset) 기반 처리

Limit과 Offset을 사용해 특정 범위의 데이터를 순차적으로 조회하고 처리하는 방식이다. 예를 들어,`Limit 1000 Offset 0`으로 처음 1000건을 조회한 후, `Limit 1000 offset 1000`으로 다음 1000건을 조회하는 식이다. 구현이 매우 단순하고, 데이터가 명확하게 인덱스 순서대로 분할되기 때문에 처리 로직이 직관적이다. 하지만 해당 방식은 offset 값이 커질수록 성능이 급격히 떨어지게 된다. 데이터베이스가 내부적으로 Offset 만큼의 행을 스킵(skip)해야 하기 때문이다. 예를 들어서 offset이 100000이라면, MySQL은 offset 값만큼 행을 카운팅해야 한다. 이 과정에서 많은 I/O와 CPU 자원을 소모하고, 쿼리 실행 시간이 선형 이상으로 증가하게 된다. 따라서 대량 데이터에서는 오프셋 기반 페이지네이션이 비효율적이며 성능 병목이 되기 쉽다.

커서(Cursor) 기반 처리

커서 기반 처리는 마지막으로 처리한 행의 고유 식별자를 기억하고, 그 이후부터 데이터를 조회하는 방식이다. 예를 들어 `where id > last_processed_id order by id limit 1000` 쿼리를 반복 실행하는 것이다. 커서 기반 조회는 인덱스가 지정된 칼럼을 기준으로 순차 탐색하기 때문에, 데이터베이스가 불필요한 행을 건너뛰지 않고 바로 다음 위치부터 검색한다. 이로 인해 offset 기반과 달리 데이터 양이 커져도 조회 성능이 일정하게 유지되어 쿼리 속도가 급격히 느려지지 않는다. 해당 방식은 대량 데이터를 안정적이고 빠르게 조회하기 위한 표준 방식으로 널리 사용된다.

스트리밍 처리

JDBC 나 JPA에서 ResultSet을 스트리밍 모드로 열어두고 데이터가 로드되는 즉시 처리하는 방식이다. 즉, 데이터를 한꺼번에 메모리에 올리지 않고 순차적으로 읽으면서 처리하는 방식이다. 메모리에 전체 데이터를 올리지 않기 때문에, 메모리 사용량이 적고 안정적이며 OOM을 방지할 수 있다. 하지만 데이터 처리 시간이 길어지면 데이터베이스 커넥션이 장시간 점유되어, 동시 접속 제한에 걸릴 수 있다. 또한 장애 발생 시 커넥션이 끊기거나 중단될 가능성이 높으며, 복구 로직이 복잡해지게 된다. 해당 방식은 일반적으로 커밋 단위가 아닌 전체 결과를 처리한 후에야 트랜잭션을 완료하므로, 트랜잭션 크기가 커지는 경향이 있다. 스트리밍 방식은 메모리 절약이 필요한 상황에서 유용하지만, 트랜잭션 관리와 장애 복구에 신경 써야 한다.

'데이터베이스' 카테고리의 다른 글

Read View, Undo Log를 통한 Repeatable Read 구현  (0) 2025.09.17
결합 알고리즘과 성능  (0) 2025.04.22
InnoDB 스토리지 엔진의 잠금  (0) 2025.04.12
동시성 제어에 대해 알아보자  (0) 2025.04.02
트랜잭션 격리수준  (0) 2025.03.31
'데이터베이스' 카테고리의 다른 글
  • Read View, Undo Log를 통한 Repeatable Read 구현
  • 결합 알고리즘과 성능
  • InnoDB 스토리지 엔진의 잠금
  • 동시성 제어에 대해 알아보자
wing1008
wing1008
휘발을 막기 위한 기록
  • wing1008
    차곡차곡
    wing1008
  • 전체
    오늘
    어제
    • 분류 전체보기 (68) N
      • Spring (26)
      • JPA (11)
      • JAVA (2)
      • 데이터베이스 (9)
      • 운영체제 (2)
      • 네트워크 (3)
      • 자료구조&알고리즘 (5) N
      • AI (0)
      • Etc (5)
      • 끄적끄적 (5)
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
wing1008
Bulk 연산의 문제점은 없을까?
상단으로

티스토리툴바