Bldev's Blog

[스프링/배치] JDBC 배치와 스프링 배치: 효율적인 대용량 데이터 배치 처리

2026. 10. 6.

대용량 데이터 배치 처리

수십만 건에서 수백만 건에 달하는 대용량 데이터를 데이터베이스에 반영하거나 마이그레이션할 때 단순 루프 기반의 단일 트랜잭션 로직은 시스템 장애를 초래할 가능성이 높아 위험하다. 이 장애의 원인은 크게 두 가지로 분류해볼 수 있다.

첫 번째는 데이터베이스와의 네트워크 왕복 횟수(round-trip) 증가로 인한 네트워크 I/O 병목이다. 쿼리 호출이 매번 발생하여 페이로드 데이터가 데이터베이스로 전송될 경우 데이터베이스 서버 자체의 성능이 좋더라도 소켓 통신 오버헤드와 패킷 왕복 시간으로 인해 처리 시간은 기하급수적으로 늘어나게 된다.

두 번째는 JVM 힙 메모리 증가와 장기 트랜잭션으로 인한 리소스 고갈이다. 대량의 데이터를 한꺼번에 애플리케이션 메모리에 적재하여 가비지 컬렉터(GC)가 감당하지 못할 경우 OutOfMemoryError가 발생하게 된다. 이 경우 트랜잭션이 오랜 기간 유지되면서 데이터베이스 커넥션 풀을 독점하고 테이블 락을 장기간 점유하게 된다.

이 두 가지 문제는 발생 계층과 성격 측면에서 구분해 볼 필요가 있다. 두 문제를 해결하기 위한 기술인 JDBC 배치와 스프링 배치는 기술이 책임과 역할이 근본적으로 구분되어 있어 적절한 요구사항에 맞게 적용이 가능하다.

JDBC 배치

JDBC 배치는 JDBC 드라이버 수준에서 제공하는 네트워크 전송 최적화 기술이다. 개별 쿼리를 매번 전송하는 대신 한 번에 전송하여 네트워크 부하를 줄여주기 때문에 대용량 데이터를 처리할 때 개별 실행 방식보다 훨씬 빠르고 효율적인 장점을 제공한다.

일반적인 쿼리 실행 방식의 경우 PreparedStatement에 파라미터를 바인딩한 후 executeUpdate() 메서드를 호출할 때마다 매번 데이터베이스로 네트워크 패킷이 전송된다. 반면 JDBC 배치는 PreparedStatement의 addBatch() 메서드를 호출하여 SQL 실행 명령과 파라미터들을 드라이버 내부 버퍼에 모아두었다가 executeBatch() 메서드가 호출되는 시점에 단 한 번의 네트워크 요청으로 묶어 일괄 전송한다. 이를 통해 네트워크 왕복 비용을 획기적으로 줄여 통신 지연 시간을 최소화한다.

그러나 JDBC 배치는 드라이버 수준의 네트워크 최적화일 뿐이므로 애플리케이션의 메모리를 관리하거나 트랜잭션의 생명주기를 전혀 제어하지 못한다. 100만 건의 데이터를 리스트에 담아 addBatch()를 100만 번 호출한다면 이미 100만 개의 객체와 바인딩 데이터가 JVM 힙 메모리에 상주하므로 OOM을 피할 수 없다. 또한 이 트랜잭션 작업이 종료될 때까지 애플리케이션은 단 하나의 데이터베이스 커넥션을 계속 점유하므로 커넥션 풀 고갈과 장기 트랜잭션 문제가 남게 된다.

스프링 배치

스프링 배치(Spring Batch)는 JDBC 배치 같은 하위 기술이 안전하고 효율적으로 동작할 수 있도록 메모리를 관리하고 트랜잭션의 생명주기를 제어하는 오케스트레이션 프레임워크이다. JDBC 배치처럼 네트워크 패킷을 묶어주는 역할은 하지 않으며 배치 작업을 특정 단위의 트랜잭션으로 분할하고 커밋을 제어하는 역할을 수행한다.

스프링 배치의 핵심은 청크 지향 처리(chunk-oriented processing) 모델이다. 전체 데이터를 한 번에 메모리에 올리는 대신 지정된 청크(chunk) 크기 단위로 일련의 읽기(ItemReader), 처리(ItemProcessor), 쓰기(ItemWriter) 작업을 반복 수행한다.

대용량 데이터를 특정 단위로 나누어 반복적으로 트랜잭션로 관리하는 것은 스프링 배치를 사용하지 않더라도 가능한 일반적인 방법이다. 스프링 배치는 이 청크 기반 분할 처리를 추상화하고 인터페이스로 표준화하여 반복문, 트랜잭션 관리, 예외 처리 등 청크 기반 분할 처리를 위해 필요한 보일러플레이트 코드 작성을 줄여주는 장점을 제공한다.

스프링 배치의 청크 분할은 JVM 힙 메모리의 상한선을 보장하는 역할을 수행한다. JpaPagingItemReader나 JdbcPagingItemReader 등을 통해 데이터베이스에서 청크 크기만큼만 페이징하여 데이터를 읽기 때문에 전체 데이터가 데이터 크기에 관계 없이 애플리케이션 힙 메모리에는 항상 지정된 청크 단위의 객체만 저장된다.

스프링 배치는 트랜잭션의 범위를 청크 단위로 분할함으로써 커넥션 독점을 방지한다. 청크 단위로 처리와 쓰기 작업이 완료되면 PlatformTransactionManager를 통해 즉시 트랜잭션을 커밋한 후 해당 청크 객체의 참조를 해제하여 메모리를 회수한다. JPA를 사용하는 경우 이 시점에 트랜잭션 종료와 함께 영속성 컨텍스트가 초기화되어 1차 캐시 메모리가 회수된다. 이를 통해 하나의 배치가 실행되는 동안 커넥션을 장시간 독점하지 않으며 락 점유 시간을 최소화한다. 작업 도중 실패가 발생하더라도 전체가 롤백되는 대신 실패한 청크 이전까지의 결과는 안전하게 커밋되어 보존된다.

스프링 배치의 또다른 특징은 배치 처리 작업과 관련된 메타데이터를 별도의 테이블로 관리하여 청크 단위 작업의 실패 지점부터 재시작(restart)하거나 특정 데이터 처리 시 에러가 발생하는 경우 청크 단위의 작업이 전체 실패되지 않고 건너뛰기(skip)될 수 있도록 하여 내결함성을 제공하는 것이다.

비교

비교 영역 JDBC 배치 스프링 배치
동작 계층 데이터베이스 드라이버 계층 (전송 계층) 애플리케이션 오케스트레이션 계층 (애플리케이션 계층)
핵심 해결 과제 네트워크 패킷 왕복 횟수 절감 JVM 메모리 고갈(OOM) 방지 및 장기 트랜잭션 관리
주된 동작 방식 드라이버 버퍼에 SQL을 적재 후 일괄 전송 데이터를 청크 단위로 페이징 분할하여 반복 커밋
메모리 관리 불가능 (호출자가 전달한 대량 데이터를 그대로 메모리에 적재) 완전 보장 (청크 크기만큼만 메모리에 적재)
실패 대응 전체 롤백 또는 예외 처리 (재시작 제어 불가) 메타데이터 기반 실패 지점 관리 및 재시작/스킵/재시도

아키텍처 선택

실무 대용량 배치 파이프라인에서 두 기술은 경쟁이나 대체제로 선택을 고민하기 보다는 상호보완적으로 활용할 수 있다. 스프링 배치가 대용량 배치 처리 작업을 작은 청크 트랜잭션으로 분할하여 애플리케이션의 힙 메모리 한도를 관리하는 역할을 수행하고 JDBC는 청크 단위로 데이터가 데이터베이스로 전달되는 순간에 네트워크 전송량을 최적화한다.

스프링 배치가 제공하는 JdbcBatchItemWriter는 바로 이러한 목적을 위한 구현체이다. ItemReader가 청크 단위로 데이터를 읽어온 후 JdbcBatchItemWriter는 내부적으로 NamedParameterJdbcTemplate의 batchUpdate() 메서드를 호출하여 해당 청크에 속한 모든 데이터를 JDBC 배치의 단일 패킷으로 한 번에 데이터베이스에 전송한다. 전송이 완료되면 스프링 배치는 즉시 트랜잭션을 커밋하고 리소스를 정리한 뒤 다음 청크 처리를 수행한다.

이러한 처리를 통해 애플리케이션은 힙 메모리 고갈 위험 없이 수천만 건의 데이터를 안정적으로 반복 처리할 수 있으며 동시에 데이터베이스와의 네트워크 통신 비용을 최소한으로 하여 배치 처리 성능을 극대화할 수 있게 된다.

참고