Spring Batch 입문 11편. Step 의 개념 + Chunk-oriented Processing 의 정확한 흐름 + TaskletStep 과의 차이 + 어느 상황에 어느 Step 을 쓰나 결정 가이드까지 풀어쓴 학습 노트. Part 3 시작.
이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 11편이에요. Part 2 Job 설정·실행 (5~10) 을 끝냈다면, 이번 11편부터는 Part 3 — Step·Chunk-oriented Processing (8편). 첫 글은 Step 종합 + Chunk vs Tasklet.
Step 의 의미
3편 Domain Language 에서 본 Step:
"A Step is a domain object that encapsulates an independent, sequential phase of a batch job."
Step 은 batch job 의 한 단계예요. 독립적이고 순차적이며, chunk-oriented(한 묶음씩 read·process·write 하는 방식) 거나 tasklet(임의 코드 한 덩어리) 둘 중 하나로 동작합니다.
Job
├── Step 1 (chunk-oriented: read 1000 lines → write to DB)
├── Step 2 (chunk-oriented: aggregate → write report)
└── Step 3 (tasklet: archive file·send email)
각 Step 은 별도 StepExecution (Step 한 번 실행을 추적하는 단위, 3편) 을 갖고, 별도 transaction boundary (트랜잭션이 시작·끝나는 경계) 안에서 돌아갑니다.
Step 의 두 가지 유형
1. Chunk-oriented Step (대부분)
@Bean
public Step chunkStep(JobRepository repo, PlatformTransactionManager tx) {
return new StepBuilder("chunkStep", repo)
.<Input, Output>chunk(100, tx)
.reader(reader())
.processor(processor())
.writer(writer())
.build();
}
N건씩 chunk 단위로 read → process → write 합니다. 11~18편에서 깊이 들어가요.
2. TaskletStep
@Bean
public Step taskletStep(JobRepository repo, PlatformTransactionManager tx) {
return new StepBuilder("taskletStep", repo)
.tasklet(myTasklet, tx)
.build();
}
임의 작업 한 덩어리를 그대로 실행해요. 19편에서 깊이.
Chunk-oriented Processing — 정확한 흐름
공식 문서의 pseudo code:
List items = new ArrayList();
for (int i = 0; i < commitInterval; i++) {
Object item = itemReader.read();
if (item != null) {
items.add(item);
}
}
itemWriter.write(items);
Reader 가 N번 read → Writer 가 N건 통째로 write → transaction commit.
ItemProcessor 포함 시
List items = new ArrayList();
for (int i = 0; i < commitInterval; i++) {
Object item = itemReader.read();
if (item != null) {
items.add(item);
}
}
List processedItems = new ArrayList();
for (Object item : items) {
Object processedItem = itemProcessor.process(item);
if (processedItem != null) {
processedItems.add(processedItem); // null = filter
}
}
itemWriter.write(processedItems);
각 chunk 는 3 단계 로 진행돼요 — Read all → Process all → Write all. Read·Process 는 1건씩, Write 는 chunk 통째로 처리합니다.
그림으로
Step 시작
↓
TX 시작
├─ Read 1
├─ Read 2
├─ ...
├─ Read N (= chunk size)
├─ Process 1, 2, ..., N (옵션)
└─ Write [1..N]
TX commit
↓
TX 시작
├─ Read N+1 ... 2N
├─ Process N+1 ... 2N
└─ Write [N+1..2N]
TX commit
↓
... 반복 ...
↓
Read null = 종료
Step 종료
Read null 의 의미
itemReader.read() 는 더 이상 읽을 게 없을 때 null 을 반환해요. Spring Batch 는 null 을 받으면 chunk 를 끝내고, 마지막 chunk 까지 commit 한 뒤 Step 도 종료합니다.
여기서 시험 함정이 하나 있어요 — null 반환 X = 무한 루프. Reader 구현 시 반드시 종료 조건 명확히.
Chunk-oriented 의 이점
1. I/O 효율
1건마다 transaction commit = N번 DB I/O
100건마다 commit = N/100번 DB I/O
차이가 100배. 84편 Kafka persistence 의 sequential I/O 와 같은 원리예요 (시리즈 2).
2. 메모리 효율
전체 dataset 메모리 로드 = OOM 위험
chunk size 만큼만 메모리 = 안전
수천만 record 도 chunk size 100 이면 안전하게 처리해요. OOM(OutOfMemoryError, JVM 메모리 부족) 걱정 없이.
3. 트랜잭션 격리
한 chunk 안 모든 record 가 *한 transaction* 으로 처리
chunk 실패 = 그 chunk 전체 rollback
이전 chunk 들 = 이미 commit
부분 성공이 가능합니다. 12편 commit-interval 에서 깊이.
4. Skip·Retry 자연스럽게 작동
chunk 단위 transaction 이 곧 재시도 가능 단위 가 돼요. 14·15편에서 깊이.
TaskletStep — 임의 작업
public interface Tasklet {
RepeatStatus execute(StepContribution contribution, ChunkContext context) throws Exception;
}
Tasklet 의 execute() 안에 임의 코드 가 들어가요. 끝나면:
RepeatStatus.FINISHED= Step 종료RepeatStatus.CONTINUABLE= 같은 tasklet 또 호출 (반복)
자주 쓰는 자리
- 파일 압축·이동·정리 — Reader·Writer 패턴 안 맞음
- 외부 명령 실행 — shell·외부 API 호출
- DB DDL·DML 한 번 — DDL(Data Definition Language, 테이블 정의)·DML(Data Manipulation Language, 데이터 조작) 한 줄, table 초기화·index rebuild
- 알림·종료 처리 — Step 시작·종료 시 단발 작업
- 사전 검증·후처리 — 다음 Step 전 setup
예제
@Component
public class ArchiveTasklet implements Tasklet {
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext context) throws Exception {
Path src = Paths.get("/data/raw/input.csv");
Path dst = Paths.get("/data/archive/input-" + LocalDate.now() + ".csv");
Files.move(src, dst);
return RepeatStatus.FINISHED;
}
}
@Bean
public Step archiveStep(JobRepository repo, PlatformTransactionManager tx,
ArchiveTasklet tasklet) {
return new StepBuilder("archiveStep", repo)
.tasklet(tasklet, tx)
.build();
}
Chunk vs Tasklet — 결정 가이드
| 상황 | 권장 |
|---|---|
| 대량 record 의 read → process → write | Chunk-oriented |
| 1건씩 의미 있는 데이터 처리 | Chunk-oriented |
| 파일 압축·이동 | TaskletStep |
| 외부 시스템 호출 (한 번) | TaskletStep |
| DDL·index rebuild | TaskletStep |
| 사전 검증·후처리 | TaskletStep |
| Job 시작·종료 알림 | TaskletStep |
| Spring Batch 의 표준·강점 활용 | Chunk-oriented |
실무에서 한 Job 은 보통 setup·teardown 용 Tasklet 1~2 개 + 실제 처리용 Chunk N 개 조합으로 짜요.
StepBuilder API
new StepBuilder(name, jobRepository)
.<Input, Output>chunk(size, transactionManager) // Chunk-oriented
.reader(reader)
.processor(processor) // 옵션
.writer(writer)
.listener(listener) // 옵션
.faultTolerant() // skip·retry 활성
.skipPolicy(...)
.retryPolicy(...)
.taskExecutor(executor) // 병렬 처리
.build();
또는 Tasklet:
new StepBuilder(name, jobRepository)
.tasklet(tasklet, transactionManager)
.listener(listener)
.build();
TransactionManager 의 자리
.chunk(100, transactionManager) // chunk
.tasklet(tasklet, transactionManager) // tasklet
TransactionManager 는 Step 마다 명시해요. 7편에서 본 Multi-DataSource 환경에서는 Step 별로 다른 TransactionManager 를 줄 수도 있습니다 (예: business DB 의 TM).
기본은 Spring Boot 가 PlatformTransactionManager bean 을 autowire 해줘요.
Step 의 ExitStatus
@Component
public class MyStepListener implements StepExecutionListener {
@Override
public ExitStatus afterStep(StepExecution stepExecution) {
if (stepExecution.getReadCount() == 0) {
return new ExitStatus("NO_DATA");
}
return stepExecution.getExitStatus();
}
}
Custom ExitStatus(Step 종료 시 외부로 알리는 상태값) 는 6편 Job 의 conditional flow 입력으로 들어가요. 값에 따라 다른 Step 으로 분기합니다.
Step 의 BatchStatus
3편의 BatchStatus 동일 — STARTING·STARTED·STOPPING·STOPPED·COMPLETED·FAILED·ABANDONED·UNKNOWN.
BatchStatus(Step·Job 의 현재 실행 상태) 는 Step 단위로 추적되고, JobExecution 의 status 는 보통 마지막 Step 의 status 를 따라가요.
Chunk size 결정 가이드
| 환경 | 권장 chunk size |
|---|---|
| 큰 객체 (1 record = 수 MB) | 10~50 |
| 일반 (DB row, JSON 등 수 KB) | 100~500 |
| 작은 객체 (단순 string) | 1000~5000+ |
| 네트워크 write (REST API call) | 10~100 (호출 횟수 최소화) |
성능 측정으로 최적값 을 찾아요. 12편 commit-interval 에서 깊이.
자주 헷갈리는 자리
Step bean = singleton
@Bean 으로 만든 Step 은 singleton 이에요. 여러 Job 이 같은 step bean 을 재사용 할 수 있고, 다만 runtime 상태 는 StepExecution 별로 분리됩니다.
@StepScope 의 의미
@Bean
@StepScope
public ItemReader<String> reader(@Value("#{jobParameters['filePath']}") String filePath) {
return new FlatFileItemReaderBuilder<String>()
.resource(new FileSystemResource(filePath))
.build();
}
@StepScope 가 붙으면 Step 이 시작될 때 bean 이 생성되고, Step 이 끝나면 소멸해요. Late binding(jobParameters 같은 runtime 값을 bean 생성 시점에 주입, 21편) 의 핵심입니다.
chunk size 와 read 횟수의 차이
chunk(100)
실제 data = 250 record
↓
chunk 1 = 100 read·write
chunk 2 = 100 read·write
chunk 3 = 50 read·write (마지막)
마지막 chunk 는 chunk size 보다 작을 수 있어요. 부분 chunk 도 commit 은 동일.
한계·실무 함정
1. Tasklet 의 transaction
Tasklet 도 transaction 안에서 실행 돼요. 너무 긴 작업이 들어가면 transaction timeout 에 걸려요. 운영 환경에서는 default-timeout 을 늘리거나 chunk 로 분할 합니다.
2. Tasklet 의 RepeatStatus 오해
RepeatStatus.CONTINUABLE 을 반환하면 같은 tasklet 이 다시 호출 돼요. 종료 조건이 없으면 무한 루프에 빠지니, 명확한 종료 조건을 꼭 박아둬요.
3. Chunk-oriented 의 메모리 부담
chunk(100000) // 너무 큼
10만건이면 수십~수백 MB 메모리가 한 번에 잡혀서 OOM 가능해요. 수백~수천 권장.
4. ItemWriter 의 chunk 전체 받기
Writer 는 chunk 통째 로 받아요. 그 안에서 한 record 라도 실패하면 chunk 전체 rollback. 14편 skip 에서 skipPolicy 로 부분 성공이 가능합니다.
5. Step 의 transaction 격리
각 Step 은 별도 transaction 이에요. Step A 가 commit 된 뒤 Step B 가 실패해도 Step A 결과는 그대로 남습니다. 전체 rollback 이 필요한 경우 는 외부 시스템 차원의 별도 패턴이 있어야 해요.
시험 직전 한 번 더 — Step 종합 함정 압축 노트
- Step = batch job 의 한 단계, 독립적·순차적
- 두 가지 = Chunk-oriented (대부분) · TaskletStep (임의 작업)
- Chunk-oriented 흐름 — Read 1~N → Process 1~N (옵션) → Write N건 → transaction commit
- Reader·Processor = 1건씩, Writer = chunk 통째로
- Reader
read()가 null = 종료 신호 - 함정 — null 반환 X = 무한 루프
- 이점 4가지 = I/O 효율 (100배) · 메모리 효율 · 트랜잭션 격리 · Skip/Retry 자연
- TaskletStep = 임의 코드,
Tasklet.execute()→RepeatStatus.FINISHED/CONTINUABLE - TaskletStep 자주 자리 = 파일 압축·외부 명령·DDL·알림·setup/teardown
- 결정 — 대량 record = Chunk, 단발 작업 = Tasklet
- 대부분 Job = setup tasklet + N chunk + teardown tasklet
- StepBuilder =
chunk(size, tx)또는tasklet(tasklet, tx)+ listener·faultTolerant·taskExecutor - TransactionManager 명시 = Step 마다, Multi-DataSource 환경 별 분리
- ExitStatus·BatchStatus = Step 단위 추적, conditional flow 입력
- Chunk size 권장 — 큰 객체 10~50, 일반 100~500, 작은 1000+, 네트워크 10~100
- 마지막 chunk = chunk size 보다 작을 수 있음
@StepScope= Step lifecycle 묶임, late binding (21편)- Step bean = singleton, StepExecution 별 runtime 분리
- 함정 — Tasklet 의 transaction timeout
- 함정 — RepeatStatus.CONTINUABLE 무한 루프
- 함정 — chunk size 너무 큼 = OOM
- 함정 — Writer chunk 통째 rollback = 14편 skip 으로 완화
- 함정 — Step 간 transaction 격리 (전체 rollback X)
공식 문서: Configuring a Step · Chunk-oriented Processing 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 6편 — Configuring a Job (JobBuilder · Validator · Listener)
- 7편 — JobRepository (영속화 · Schema · Isolation)
- 8편 — JobOperator (실행 · 중지 · 재시작 · CommandLine)
- 9편 — Running a Job (JobLauncher · Sync/Async · Scheduler)
- 10편 — Advanced Metadata (JobExplorer · JobRegistry · 운영 대시보드)
다음 글: