Spring Batch 입문 12편 — Chunk Step 설정 + Commit Interval 튜닝

2026-05-17Spring Batch 입문에서 운영까지

Spring Batch 입문 12편. Chunk Step 설정 + Commit Interval 튜닝 — StepBuilder.chunk() 의 모든 옵션, commit-interval 1 vs 10 vs 1000 의 성능 영향, Reader·Processor·Writer 의 정확한 lifecycle, 자주 만나는 튜닝 함정까지 풀어쓴 학습 노트.

📚 Spring Batch 입문에서 운영까지 · 12편 — Chunk Step 설정 + Commit Interval 튜닝

이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 12편이에요. 11편 에서 Chunk vs Tasklet 의 큰 그림을 잡았다면, 이번 12편은 Chunk Step 설정의 모든 것 + commit-interval 튜닝.

Chunk Step 의 최소 설정

@Bean
public Step sampleStep(JobRepository jobRepository,
                       PlatformTransactionManager transactionManager) {
    return new StepBuilder("sampleStep", jobRepository)
        .<String, String>chunk(10, transactionManager)
        .reader(itemReader())
        .writer(itemWriter())
        .build();
}

필수:

  • StepBuilder(name, jobRepository) — 이름 + JobRepository (메타데이터 영속 저장소)
  • .<Input, Output>chunk(size, tx) — Generic type + chunk size + TransactionManager
  • .reader(...) + .writer(...) — 둘 필수

옵션:

  • .processor(...) — 변환·검증 (옵션)
  • .listener(...) — 라이프사이클 hook
  • .faultTolerant() — skip·retry (14·15편)
  • .taskExecutor(...) — 병렬 처리 (37편)

Bean 이름 vs StepBuilder name

@Bean                                    // bean name = "sampleStep" (메서드 이름)
public Step sampleStep(...) {
    return new StepBuilder(repo)         // ← 이름 생략 가능
        ...
        .build();
}

@Bean 으로 등록하면 메서드 이름 이 자동 step name. 명시 안 하면 bean name 이 step name.

@Bean 아닌 경우 = 반드시 이름 명시:

new StepBuilder("myStep", repo)

Generic Type — <Input, Output>

.<Customer, ValidatedCustomer>chunk(100, tx)
  • Input = Reader 가 반환하는 type
  • Output = Processor 가 반환하는 type (Processor 없으면 Input 과 같음)

Type safety + IDE 자동완성.

Processor 의 type 변환

.<Input, Output>chunk(100, tx)
.reader(Reader<Input>)
.processor(Processor<Input, Output>)
.writer(Writer<Output>)

Reader → Input → Processor → Output → Writer 로 type chain 이 한눈에 잡혀요.

Commit Interval — 가장 중요한 튜닝

.<String, String>chunk(10, tx)        // commit-interval = 10

chunk(size) 의 size 가 곧 commit-interval. N건 처리할 때마다 transaction commit.

성능 영향

commit-interval = 1
  → 매 record 마다 commit
  → 100만 record = 100만 transaction
  → DB I/O 폭증, 매우 느림

commit-interval = 10
  → 10건마다 commit
  → 10만 transaction

commit-interval = 100
  → 1만 transaction (X 10 빠름)

commit-interval = 1000
  → 1천 transaction (X 100 빠름)

commit-interval = 10000
  → 100 transaction
  → BUT: 메모리·rollback 비용·lock 시간 ↑

일반적으로 100~1000 이 sweet spot.

Trade-off

항목 작은 chunk 큰 chunk
Transaction 오버헤드
처리 속도
메모리 사용
Rollback 비용
DB Lock 시간
진행 상황 표시 자주 드물게

여기서 시험 함정이 하나 — chunk 가 너무 크면 record 한 건 실패에도 전체 rollbackSkip 비활성 환경에서 매우 비싼 비용. 14편 skip 에서 부분 성공 가능.

Reader · Processor · Writer Lifecycle

Chunk 처리 한 사이클

// Step 시작 시
reader.open(executionContext);
processor.open(executionContext);    // (옵션)
writer.open(executionContext);

// Chunk 반복
while (true) {
    TransactionStatus tx = transactionManager.getTransaction(...);

    List<Input> items = new ArrayList<>();
    for (int i = 0; i < chunkSize; i++) {
        Input item = reader.read();
        if (item == null) break;
        items.add(item);
    }

    if (items.isEmpty()) {
        break;       // Step 종료
    }

    List<Output> processed = new ArrayList<>();
    for (Input item : items) {
        Output out = processor.process(item);
        if (out != null) {
            processed.add(out);     // null = filter
        }
    }

    writer.write(processed);

    reader.update(executionContext);
    processor.update(executionContext);
    writer.update(executionContext);

    transactionManager.commit(tx);
}

// Step 종료 시
reader.close();
processor.close();
writer.close();

핵심:

  • open = Step 시작 시 1회 (ItemStream 인터페이스 — 재시작 안전성을 책임지는 추상화)
  • update = 매 chunk 종료 시 (ExecutionContext 에 진행 상태 저장)
  • close = Step 종료 시 1회

24편 ItemStream 에서 깊이.

TransactionManager 의 자리

.chunk(100, transactionManager)
// 또는 (deprecated 경향)
.chunk(100).transactionManager(transactionManager)

명시 안 하면 Spring Boot 가 autowire 로 PlatformTransactionManager bean 을 찾아 넣어요.

기본 — ResourcelessTransactionManager

JobRepository 가 Resourceless (v6 신규) 환경에서는 ResourcelessTransactionManager (DB 자원 없이 동작하는 TM) 도 가능. DB 없이 메모리 only. 학습용.

운영 — Multi-DataSource

@Bean
public Step myStep(JobRepository repo,
                   @Qualifier("businessTransactionManager") PlatformTransactionManager bizTx) {
    return new StepBuilder("myStep", repo)
        .<X, Y>chunk(100, bizTx)        // business DB 의 TM
        .reader(...).writer(...)
        .build();
}

JobRepository TM 과 business TM 이 따로. 두 DB 가 분리.

ItemProcessor 옵션

.<Input, Output>chunk(100, tx)
.reader(reader)
.processor(processor)        // 옵션 — 없어도 OK
.writer(writer)

Processor 를 빼면 Reader 의 output 이 그대로 Writer 의 input:

.<Customer, Customer>chunk(100, tx)
.reader(customerReader)      // Reader<Customer>
.writer(customerWriter)      // Writer<Customer>

Reader 와 Writer 의 type 이 같으면 변환·검증 없이 곧장 흘려보내요.

Step Listener

.listener(new StepExecutionListener() {
    @Override
    public void beforeStep(StepExecution stepExecution) {
        log.info("Step starting: {}", stepExecution.getStepName());
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        log.info("Step done: read={} write={}",
            stepExecution.getReadCount(), stepExecution.getWriteCount());
        return stepExecution.getExitStatus();
    }
})

StepExecutionListener (Step 시작·종료 시점에 끼어드는 콜백) 로 Step 라이프사이클 hook. 18편 intercepting 에서 listener 종합.

자주 쓰는 패턴

Pattern 1: 단순 ETL

@Bean
public Step etlStep(JobRepository repo, PlatformTransactionManager tx,
                    ItemReader<RawData> reader,
                    ItemProcessor<RawData, CleanData> processor,
                    ItemWriter<CleanData> writer) {
    return new StepBuilder("etlStep", repo)
        .<RawData, CleanData>chunk(500, tx)
        .reader(reader)
        .processor(processor)
        .writer(writer)
        .build();
}

가장 흔한 ETL (Extract·Transform·Load 데이터 파이프라인) 모양.

Pattern 2: 검증 + 분기

.processor(item -> {
    if (item.isValid()) {
        return item;
    } else {
        log.warn("Invalid: {}", item);
        return null;          // filter (skip)
    }
})

검증 실패 시 null 반환 = filter.

Pattern 3: 외부 시스템 호출 포함

@Bean
public Step enrichStep(JobRepository repo, PlatformTransactionManager tx) {
    return new StepBuilder("enrichStep", repo)
        .<User, EnrichedUser>chunk(50, tx)     // 외부 API 호출이 비싸 작은 chunk
        .reader(userReader)
        .processor(user -> externalApi.enrich(user))
        .writer(enrichedUserWriter)
        .build();
}

외부 호출이 들어가면 chunk size 를 작게 (호출 빈도 ↓·timeout 위험 ↓).

Commit Interval 튜닝 절차

  1. 시작 = 100
  2. 처리 속도 측정 — StepExecution 의 duration / writeCount
  3. chunk size 2배·5배 늘리기 → 처리 속도 측정
  4. 처리 속도 향상 시 → 더 늘림
  5. 메모리·OOM·rollback 비용 모니터링
  6. plateau 도달 = 최적값

실측 예시 (예시)

chunk=100   → 1000 record/sec
chunk=500   → 4500 record/sec        (4.5x)
chunk=1000  → 7000 record/sec        (1.55x more)
chunk=5000  → 8500 record/sec        (1.21x more, plateau)
chunk=10000 → OOM ❌

→ 최적 = 5000 또는 그 아래.

자주 헷갈리는 자리

chunk size 와 fetch size 차이

chunk size = Spring Batch 가 commit 단위
fetch size = JDBC driver 가 한 번에 DB 에서 가져오는 row 수

별개 개념. 둘 다 조정 가능. DB-based Reader 라면 fetchSize 설정을 권장. 33편 database 깊이.

Chunk 와 Transaction boundary

chunk(100)
  ↓
1 chunk = 1 transaction = 100 record commit

chunk 끝 = transaction commit. 중간에 실패하면 그 chunk 전체 rollback.

Reader 가 chunk 보다 적게 반환

reader.read() 100 호출 중 80번째에 null
→ chunk size 100 인데 실제 chunk = 80
→ 정상 처리·commit
→ Step 종료

마지막 chunk 가 부분만 채워져도 정상. 함정 X.

Processor null 반환

.processor(item -> isValid(item) ? item : null)

null = filter. Writer 로 안 넘어가요. chunk 의 read 수 = N, write 수 < N 가능.

한계·실무 함정

1. 너무 큰 chunk = OOM

위에서 강조. 100만 record 면 메모리가 폭증해요. (OOM = OutOfMemoryError, 힙 메모리 부족 예외)

2. 너무 작은 chunk = 느림

chunk(1) 은 매 record 마다 commit = 느림 + DB 부담.

3. 외부 호출 timeout

큰 chunk + 외부 API 호출이 겹치면 chunk 전체 처리 시간이 길어져 → transaction timeout. default-timeout 늘리거나 chunk 를 작게.

4. ItemWriter 의 batch 처리

JDBC Writer 는 addBatch() + executeBatch() 가 자동. 그러나 기타 Writer (REST·JMS 등) 는 명시 batch 처리가 필요해요. 23편 ItemWriter 깊이.

5. Reader·Writer 의 state 누락

Reader 의 current position 을 ExecutionContext 에 저장 안 하면 재시작 시 처음부터. 24편 ItemStreamupdate() 구현 필수.

6. Generic type 누락

.chunk(100, tx)              // raw type
.reader(reader)              // type 추론 X
.processor(processor)
.writer(writer)

= IDE warning + runtime 에 type cast 위험. 항상 .<X, Y>chunk(...) 명시.

시험 직전 한 번 더 — Chunk Step Configuring 함정 압축 노트

  • 최소 설정 = StepBuilder + chunk(size, tx) + reader + writer
  • 선택 = processor·listener·faultTolerant·taskExecutor
  • @Bean Step = 메서드 이름이 자동 step name
  • @Bean 아닌 경우 = new StepBuilder("name", repo) 명시
  • Generic = .<Input, Output>chunk(size, tx) — type chain (Reader → Processor → Writer)
  • Commit Interval (= chunk size) = 가장 중요한 튜닝
  • 권장 100~1000 (일반 환경)
  • 작은 객체 = 1000~5000+, 큰 객체 = 10~50, 외부 호출 = 10~100
  • 너무 큼 = OOM·long rollback·lock
  • 너무 작음 = 느림·DB 부담
  • Lifecycleopen (Step 시작) → 반복 (read·process·write·update·commit) → close
  • TransactionManager = chunk 단위 commit/rollback 의 핵심
  • Multi-DataSource = Step 마다 다른 TM 가능
  • Reader·Processor·Writer 의 type 일치 = Reader<I>·Processor<I,O>·Writer<O>
  • Processor 옵션 — Reader 와 Writer type 같으면 생략
  • Processor null 반환 = filter (skip) — read 수와 write 수 다를 수 있음
  • 외부 호출 = chunk size 작게 + timeout 늘림
  • 튜닝 절차 = 100 시작 → 2배·5배·plateau 까지 측정 → 메모리 모니터링 → 최적값
  • chunk size ≠ fetch size (33편)
  • 함정 — 너무 큰 chunk = OOM
  • 함정 — 너무 작은 chunk = 느림
  • 함정 — 외부 호출 timeout
  • 함정 — Reader·Writer state 누락 = 재시작 처음부터 (24편)
  • 함정 — Generic type 누락

공식 문서: Configuring a Step (chunk-oriented) · The Commit Interval 에서 원문을 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!