Spring Batch 입문 12편. Chunk Step 설정 + Commit Interval 튜닝 — StepBuilder.chunk() 의 모든 옵션, commit-interval 1 vs 10 vs 1000 의 성능 영향, Reader·Processor·Writer 의 정확한 lifecycle, 자주 만나는 튜닝 함정까지 풀어쓴 학습 노트.
이 글은 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 가 반환하는 typeOutput= 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 한 건 실패에도 전체 rollback → Skip 비활성 환경에서 매우 비싼 비용. 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 튜닝 절차
- 시작 = 100
- 처리 속도 측정 — StepExecution 의
duration / writeCount - chunk size 2배·5배 늘리기 → 처리 속도 측정
- 처리 속도 향상 시 → 더 늘림
- 메모리·OOM·rollback 비용 모니터링
- 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편 ItemStream 의 update() 구현 필수.
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
@BeanStep = 메서드 이름이 자동 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 부담
- Lifecycle —
open(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 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 7편 — JobRepository (영속화 · Schema · Isolation)
- 8편 — JobOperator (실행 · 중지 · 재시작 · CommandLine)
- 9편 — Running a Job (JobLauncher · Sync/Async · Scheduler)
- 10편 — Advanced Metadata (JobExplorer · JobRegistry · 운영 대시보드)
- 11편 — Step 종합 + Chunk-oriented vs TaskletStep
다음 글: