bedaily.me
Backend2026년 6월 21일7분 읽기

Spring Batch 6, 아키텍처가 이렇게 바뀌었다

Spring Batch 6은 도메인 모델은 그대로 둔 채 인프라와 실행 모델을 다시 짰습니다. 빈 구성 단순화, Resourceless 기본화, 청크 동시성 재설계까지 실코드와 함께 정리합니다.

Spring Batch 6Spring Framework 7배치동시성마이그레이션

개요

Spring Batch 6 릴리즈 노트를 읽으면서 인상적이었던 건, 우리가 배치 코드를 짤 때 쓰던 개념들 — Job, Step, 청크 지향 처리, ItemReader/ItemProcessor/ItemWriter — 은 거의 그대로라는 점이었다.

바뀐 건 그 아래였다. 메타데이터를 다루는 인프라 빈 구성, 청크를 실제로 굴리는 동시성 모델, 재시도가 동작하는 기반. 눈에 잘 안 보이는 곳들이 한 버전에서 크게 정리됐다.

Spring Batch 6은 Spring Framework 7 라인에 맞춘 메이저 릴리스다. 이 포스트는 새 API 목록을 나열하기보다, 어떤 구조가 왜 바뀌었는지에 집중한다.

한눈에 보는 구조

먼저 전체 그림부터. 실행을 기동하는 진입점, 실제 작업 단위, 그리고 그 모든 상태를 기록하는 저장소로 나뉜다. 괄호 안은 6에서 통합된 인터페이스다.

📄실행 구조
실행 요청


JobOperator    →  기동 · 중지 · 재시작 · recover()      (= JobLauncher)


Job            →  여러 Step의 묶음, JobParameter로 식별


Step           →  Tasklet  또는  Chunk(read → process → write)

        ↕   (위 모든 단계가 실행 상태를 읽고 쓴다)

JobRepository  →  실행 이력 · 상태 · 파라미터 영속화      (= JobExplorer)
                  저장소: JDBC · MongoDB · Resourceless(기본)

이 그림에서 괄호로 묶인 통합과 맨 아래 저장소의 기본값이 6에서 바뀐 핵심이다. 하나씩 보자.

인프라 빈 — 6개가 2개로

Spring Batch 5까지 배치를 직접 구성해 본 사람이라면 빈을 꽤 여러 개 다뤄야 했던 기억이 있을 것이다. JobRepository, JobExplorer, JobLauncher, JobOperator, 거기에 JobRegistry와 등록용 초기화 빈까지.

문제는 이들이 역할은 겹치는데 따로 존재했다는 점이다. 같은 직렬화 설정을 JobRepositoryJobExplorer에 두 번 넣는 식의 중복이 흔했다.

6은 인터페이스 계층을 정리해 이 관계를 합쳤다.

📄빈 구성 정리
Spring Batch 5 — 따로 관리하던 빈
   JobRepository · JobExplorer · JobLauncher · JobOperator
   JobRegistry · 등록용 초기화 빈

          │  통합


Spring Batch 6 — 결국 두 갈래로
   JobRepository   ← JobExplorer 흡수 (조회까지 담당)
   JobOperator     ← JobLauncher 흡수 (실행까지 담당)
   JobRegistry       자동 등록 (초기화 빈 불필요)

읽기(조회)와 쓰기(실행)를 같은 빈이 담당하니, 직접 구성하더라도 신경 쓸 대상이 줄었다. 직렬화 설정을 한 곳에만 두면 된다.

Resourceless — DB 없이도 돌아간다

Spring Batch 5에서는 메타데이터를 저장할 곳이 필요했다. 운영에선 보통 JDBC 저장소를 쓰지만, 테스트나 단발성 작업에서도 H2 같은 인메모리 DB를 띄워야 메타데이터 테이블이 만들어졌다. "DB 없이 배치 하나 돌리고 싶을 뿐인데" 하는 순간이 있었다.

6은 기본 저장소가 ResourcelessJobRepository다. 트랜잭션 매니저도 선택 사항이 됐고, 없으면 ResourcelessTransactionManager가 쓰인다.

📄BatchApplication.java
// 트랜잭션 매니저, DataSource 설정 없이도 배치 실행 가능
@SpringBootApplication
public class BatchApplication {
    public static void main(String[] args) {
        SpringApplication.run(BatchApplication.class, args);
    }
}

물론 실행 이력을 영속화하고 재시작을 보장하려면 여전히 JDBC나 MongoDB 저장소가 필요하다. 달라진 건 저장소가 더 이상 시작의 전제 조건이 아니라는 점이다. 가볍게 시작하고, 필요할 때 저장소를 붙이면 된다.

저장소별 설정을 따로 떼어냈다

@EnableBatchProcessing 하나에 공통 설정과 저장소 설정이 섞여 있던 구조도 정리됐다. 6은 공통 인프라와 저장소 종류를 별도 애너테이션으로 분리한다.

📄JobConfiguration.java
@Configuration
@EnableBatchProcessing(taskExecutorRef = "batchTaskExecutor")
@EnableJdbcJobRepository(
        dataSourceRef = "batchDataSource",
        transactionManagerRef = "batchTransactionManager")
class JobConfiguration {

    @Bean
    Job job(JobRepository jobRepository) {
        // ...
    }
}

JDBC를 쓰면 @EnableJdbcJobRepository, MongoDB를 쓰면 @EnableMongoJobRepository로 바꾸면 된다. 저장소를 갈아 끼울 때 공통 설정은 건드리지 않아도 되는 구조다.

청크 동시성 — 병렬 반복에서 생산자–소비자로

이게 가장 안쪽의 변화다. 청크를 여러 스레드로 돌릴 때, 5는 "병렬 반복(parallel iteration)" 방식이었다. 여러 스레드가 각자 읽고-처리-쓰기를 반복하면서 공유 상태를 동기화하고, throttle-limit으로 처리 속도를 제어했다. 동작은 했지만 경합이 끼어들기 쉬웠고 튜닝 포인트를 잡기 까다로웠다.

6은 생산자–소비자(producer–consumer) 모델로 다시 짰다. 읽기를 전담하는 생산자가 내부의 크기 제한된 큐에 아이템을 채우고, 소비자 스레드들이 큐에서 꺼내 처리·쓰기를 한다.

📄동시성 모델
Spring Batch 5 — 병렬 반복(parallel iteration)
   Thread-1 : read → process → write ─┐
   Thread-2 : read → process → write ─┤  공유 상태 동기화 + throttle-limit
   Thread-3 : read → process → write ─┘
   → 경합 지점이 많고 처리 속도 튜닝이 까다로움

Spring Batch 6 — 생산자·소비자(producer–consumer)
   Producer(read) ─▶ [ bounded queue ] ─▶ Consumer(process → write) × N

        큐가 차면 producer 정지 → chunk가 쓰여 자리 나면 재개
   → 큐 용량이 곧 백프레셔. 동기화 경합 없이 처리량 안정

핵심은 백프레셔(backpressure)가 큐 용량으로 자연스럽게 해결된다는 점이다. 소비자가 느리면 큐가 차고, 큐가 차면 생산자가 알아서 멈춘다. 별도의 throttle 수치를 맞추거나 공유 상태를 동기화할 필요가 없다. 흐름이 단순해지니 처리량도 안정적이다.

같은 JVM 안에서 청크를 병렬 처리하는 로컬 청킹도 이 모델 위에 올라간다.

청크 스텝 — 새 전용 빌더

청크 지향 스텝이 독립 구현으로 정식화되면서, 전용 빌더 ChunkOrientedStepBuilder가 생겼다. 청크 크기를 생성 시점에 받고 읽기·쓰기를 체이닝으로 붙인다.

📄StepConfiguration.java
@Bean
Step step(JobRepository jobRepository,
          ItemReader<Person> reader,
          ItemWriter<Person> writer) {
    return new ChunkOrientedStepBuilder<Person, Person>("step", jobRepository, 100)
            .reader(reader)
            .writer(writer)
            .build();
}

기존 StepBuilderchunk(...) 경로도 아직 동작하지만, 6.0에서 deprecated로 표시됐고 7.0에서 제거될 예정이다. 새 코드는 ChunkOrientedStepBuilder로 작성하는 게 좋다. 트랜잭션 매니저를 따로 넘기지 않으면 앞서 본 Resourceless 트랜잭션 매니저가 쓰인다.

재시도와 스킵 — 기반을 갈아 끼웠다

내결함성 처리도 바닥이 바뀌었다. 재시도는 그동안 별도 라이브러리에 의존했지만, 6은 Spring Framework 7에 내장된 재시도 기능을 쓴다. 스킵은 SkipPolicy 인터페이스 중심으로 단순화됐다.

📄FaultTolerantConfig.java
RetryPolicy retryPolicy = RetryPolicy.builder()
        .maxRetries(3)
        .includes(Set.of(TransientDataAccessException.class))
        .build();

SkipPolicy skipPolicy = new LimitCheckingExceptionHierarchySkipPolicy(
        Set.of(FlatFileParseException.class), 50);

// 스텝 빌더 체인에 정책을 붙인다
new ChunkOrientedStepBuilder<Person, Person>("step", jobRepository, 100)
        .reader(reader).writer(writer)
        .faultTolerant()
        .retryPolicy(retryPolicy)
        .skipPolicy(skipPolicy)
        .build();

일시적 예외는 정해진 횟수만큼 재시도해 살리고, 특정 예외가 난 아이템은 건너뛰어 한 건의 불량 데이터가 전체 배치를 막지 않게 한다. 개념은 그대로지만, 의존하는 라이브러리가 프레임워크 안으로 들어왔다.

운영하기 좋아진 것들

배치는 만드는 것만큼 운영이 중요하다. 6은 이쪽에 손이 많이 갔다.

복구 메서드. 실패한 실행을 되살리려고 메타데이터 테이블을 손으로 고치던 경험이 있을 것이다. JobOperator#recover()가 이걸 표준화했다. JDBC든 NoSQL이든 동일한 방식으로 복구한다.

우아한 종료. 종료 신호가 오면 진행 중인 스텝을 멈추고 저장소 상태를 갱신해 "중단됨"으로 표시한다. 덕분에 다음 실행에서 재시작 보장이 깨지지 않는다.

관측성. Java Flight Recorder(JFR) 이벤트가 추가돼 잡·스텝 실행, 아이템 읽기/쓰기, 트랜잭션 경계를 JVM 오버헤드 거의 없이 프로파일링할 수 있다. API에는 JSpecify 널 안정성 애너테이션이 붙어 정적 분석 품질도 올라갔다.

📄GracefulShutdown.java
// 종료 신호 → 활성 스텝 정지 → 저장소에 stopped 기록 → 재시작 가능 상태로 마무리
// 커스텀 스텝도 StoppableStep을 구현하면 외부 stop 신호를 처리한다
public class MyStep implements StoppableStep {
    @Override
    public void stop() {
        // 정리 로직
    }
}

한 장 요약: 5 → 6

영역Spring Batch 5Spring Batch 6
인프라 빈Repository·Explorer·Launcher·Operator 분리Repository(=Explorer), Operator(=Launcher)로 통합
메타데이터 저장소인메모리 DB 등 사실상 필수Resourceless 기본, 저장소는 선택
트랜잭션 매니저필수선택 (없으면 Resourceless)
저장소 설정@EnableBatchProcessing에 혼재@EnableJdbcJobRepository / @EnableMongoJobRepository로 분리
청크 동시성병렬 반복 + 공유 상태 동기화생산자·소비자(bounded queue)
재시도별도 라이브러리프레임워크 내장 RetryPolicy
직렬화Jackson 2Jackson 3
기반Spring Framework 6Spring Framework 7 / Boot 4

마이그레이션 전에 확인할 것들

  • 의존성: Spring Framework 7, Spring Data 4, Spring Integration 7 라인. Spring Boot 4와 함께 올라간다
  • Jackson 3: JsonItemReader, JsonFileItemWriter, 실행 컨텍스트 직렬화기가 Jackson 3 기준으로 바뀌었다. 커스텀 직렬화 코드가 있다면 import 경로 확인
  • 재시도: 별도 재시도 라이브러리 의존성을 빼고 프레임워크 내장 RetryPolicy로 전환
  • 스텝 빌더: StepBuilderchunk(...) 경로는 deprecated. 신규 코드는 ChunkOrientedStepBuilder 권장
  • XML 설정: batch: 네임스페이스 기반 XML 구성은 deprecated. 자바 설정으로 이전 권장
  • 테스트: spring-batch-test의 JUnit 4 지원 deprecated. JUnit 5로 정리
  • 빈 구성: 직접 구성하던 JobExplorer/JobLauncher 빈은 통합된 인터페이스로 단순화 가능
  • 공식 마이그레이션 가이드 확인 — 이 포스트에서 다루지 않은 변경이 더 있다

마무리

Spring Batch 6의 변화는 화려한 신기능이라기보다 "구조 정리"에 가깝다. 빈을 합치고, 저장소 전제를 없애고, 동시성 모델을 다시 짜고, 재시도 기반을 프레임워크 안으로 들였다.

그래서 기존 배치 잡을 옮길 때 비즈니스 로직 — 무엇을 읽어 어떻게 가공해 어디에 쓰는지 — 은 거의 그대로 가져갈 수 있다. 손볼 곳은 그 잡을 떠받치던 설정과 인프라 쪽이다. 한 번 정리해 두면, 더 적은 빈과 더 단순한 동시성 위에서 돌아가는 배치를 갖게 된다.

주간 기술 뉴스레터

Backend · AI · Java 핵심 내용을 매주 이메일로 보내드립니다.