Spring Batch 입문 11편 — Step 종합 + Chunk-oriented vs TaskletStep

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

Spring Batch 입문 11편. Step 의 개념 + Chunk-oriented Processing 의 정확한 흐름 + TaskletStep 과의 차이 + 어느 상황에 어느 Step 을 쓰나 결정 가이드까지 풀어쓴 학습 노트. Part 3 시작.

📚 Spring Batch 입문에서 운영까지 · 11편 — Step 종합 + Chunk-oriented vs TaskletStep

이 글은 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번 readWriter 가 N건 통째로 writetransaction 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 에서 원문을 확인할 수 있어요.

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

이전 글:

다음 글:

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

답글 남기기

error: Content is protected !!