
우리 회사 정산 시스템에서 Spring Batch를 쓰고 있다.
코드를 보다 보니 어떤 Job은 Chunk로, 어떤 Job은 Tasklet으로 구현되어 있었다.
왜 나눠 쓴 걸까?
직접 코드를 뜯어보고 정리해봤다.
🔍 실제로 어떻게 나뉘어 있었나
정산 서버의 배치 Job 60여 개를 확인해보니, Chunk와 Tasklet이 거의 반반이었다.
패턴이 보인다.
그럼 Chunk와 Tasklet이 정확히 뭔지 정리해보자.
📐 먼저 알아야 할 것: Job → Step → ?
📦 Job
▼
Step 1
Chunk 또는 Tasklet
Chunk 또는 Tasklet
→
Step 2
Chunk 또는 Tasklet
Chunk 또는 Tasklet
→
Step N
...
...
Job은 하나 이상의 Step으로 구성된다.
각 Step 안에서 데이터를 처리하는 방식이 Chunk 또는 Tasklet이다.
1) Chunk — 컨베이어 벨트
데이터를 읽고 → 가공하고 → 쓰는 과정을 N건 단위로 반복한다.
📖 ItemReader
1건씩 읽음
1건씩 읽음
→
⚙️ ItemProcessor
1건씩 가공 (선택)
1건씩 가공 (선택)
→
✏️ ItemWriter
chunk 단위로 일괄 쓰기
chunk 단위로 일괄 쓰기
↑ chunk-size만큼 모이면 Writer 실행 → 커밋 → 다시 Reader부터 반복
예를 들어 chunk-size가 10이고 데이터가 100건이면:
1
Reader가 10건을 1건씩 읽음 → Processor가 1건씩 가공 → Writer가 10건 일괄 저장 → 커밋
2
다음 10건 읽음 → 가공 → 일괄 저장 → 커밋
…
총 10번 반복 (100건 / 10 = 10 chunk)
💡 핵심은 트랜잭션 단위다.
chunk-size가 10이면 10건마다 커밋한다.
7번째 chunk에서 실패하면 1~6번째 chunk는 이미 커밋되어 있고, 7번째 chunk만 롤백된다.
chunk-size가 10이면 10건마다 커밋한다.
7번째 chunk에서 실패하면 1~6번째 chunk는 이미 커밋되어 있고, 7번째 chunk만 롤백된다.
코드 예시
@Bean public Step chunkBaseStep() { return stepBuilderFactory.get("chunkBaseStep") .<String, String>chunk(10) // chunk-size = 10 .reader(itemReader()) // 1건씩 읽기 .processor(itemProcessor()) // 1건씩 가공 .writer(itemWriter()) // 10건 모이면 일괄 쓰기 .build(); } private ItemReader<String> itemReader() { return new ListItemReader<>(getItems()); } private ItemProcessor<String, String> itemProcessor() { return item -> item + ", Spring Batch"; } private ItemWriter<String> itemWriter() { return items -> log.info("chunk item size : {}", items.size()); // 출력: "chunk item size : 10" (10번 반복) }
.<String, String>chunk(10)에서 첫 번째 String이 INPUT(Reader 반환 타입),
두 번째 String이 OUTPUT(Processor 반환 → Writer 수신 타입)이다.
2) Tasklet — 수작업
Reader/Processor/Writer 구분 없이 하나의 메서드에서 모든 로직을 직접 처리한다.
🔧 Tasklet.execute()
읽기 + 가공 + 쓰기를 전부 여기서 직접 구현
읽기 + 가공 + 쓰기를 전부 여기서 직접 구현
RepeatStatus.FINISHED → 종료 RepeatStatus.CONTINUABLE → 재호출
코드 예시
@Bean public Step taskBaseStep() { return stepBuilderFactory.get("taskBaseStep") .tasklet(this.tasklet()) .build(); } private Tasklet tasklet() { return (stepContribution, chunkContext) -> { List<String> items = getItems(); // 100건 전부 읽기 log.info("task item size : {}", items.size()); // 출력: "task item size : 100" (1번만 실행) return RepeatStatus.FINISHED; }; }
⚠️ Tasklet의 DB 작업은 Step 트랜잭션으로 묶인다.
하지만 외부 API 호출이나 Kafka 메시지 발행 같은 부작용은 DB 롤백으로 되돌릴 수 없다.
이런 경우 별도의 보상/멱등 처리가 필요하다.
하지만 외부 API 호출이나 Kafka 메시지 발행 같은 부작용은 DB 롤백으로 되돌릴 수 없다.
이런 경우 별도의 보상/멱등 처리가 필요하다.
3) 같은 100건, 다른 결과
Chunk (chunk-size=10)
chunk item size : 10chunk item size : 10... (10번 반복)
chunk item size : 10트랜잭션 10번 커밋
Tasklet
task item size : 100트랜잭션 1번 커밋
📝 결론: 왜 나눠 쓴 걸까
Chunk
행마다 같은 규칙을 반복 적용하는
변환/판단 작업
변환/판단 작업
= 컨베이어 벨트
Tasklet
SQL 집계 한 방, 외부 API, 상태 관리처럼
파이프라인으로 안 쪼개지는 작업
파이프라인으로 안 쪼개지는 작업
= 수작업
처음에는 "Chunk = 대량, Tasklet = 소량"이라고 단순하게 생각했다.
하지만 실제 코드를 보니 데이터 양이 아니라 처리 구조가 기준이었다.
"이 작업이 Reader → Processor → Writer로 자연스럽게 쪼개지는가?"
이 질문에 Yes면 Chunk, No면 Tasklet.
📎 전체 코드
package com.ot.schedule.Taskletandchunk; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.*; import org.springframework.batch.core.configuration.annotation.*; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.item.*; import org.springframework.batch.item.support.ListItemReader; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.context.annotation.*; import java.util.*; @Configuration @Slf4j public class ChunkProcessingConfiguration { private final JobBuilderFactory jobBuilderFactory; private final StepBuilderFactory stepBuilderFactory; public ChunkProcessingConfiguration(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) { this.jobBuilderFactory = jobBuilderFactory; this.stepBuilderFactory = stepBuilderFactory; } @Bean public Job chunkProcessingJob() { return jobBuilderFactory.get("chunkProcessingJob") .incrementer(new RunIdIncrementer()) .start(this.taskBaseStep()) .next(this.chunkBaseStep()) .build(); } // =============================== Tasklet Step =============================== @Bean public Step taskBaseStep() { return stepBuilderFactory.get("taskBaseStep") .tasklet(this.tasklet()) .build(); } private Tasklet tasklet() { return (stepContribution, chunkContext) -> { List<String> items = getItems(); log.info("task item size : {}", items.size()); return RepeatStatus.FINISHED; }; } // =============================== Chunk Step =============================== @Bean public Step chunkBaseStep() { return stepBuilderFactory.get("chunkBaseStep") .<String, String>chunk(10) .reader(itemReader()) .processor(itemProcessor()) .writer(itemWriter()) .build(); } private ItemReader<String> itemReader() { return new ListItemReader<>(getItems()); } private ItemProcessor<String, String> itemProcessor() { return item -> item + ", Spring Batch"; } private ItemWriter<String> itemWriter() { return items -> items.forEach(log::info); } // =============================== 공통 =============================== private List<String> getItems() { List<String> items = new ArrayList<>(); for (int i = 0; i < 100; i++) { items.add(i + "Hello"); } return items; } }
'Programming > Java & Spring 관련 내용 정리' 카테고리의 다른 글
| 우리 정산 시스템은 오류를 어떻게 처리하고 있을까 (0) | 2026.06.23 |
|---|---|
| 대규모 트래픽 티켓팅 시스템 설계 해보기 (0) | 2026.06.05 |
| "주문이 사라졌다!" Oracle → Kafka → DynamoDB 파이프라인 캐시 불일치 (0) | 2026.03.23 |
| Push 서버 재구성: 유연한 이벤트성 푸시 발송 시스템 구현 (1) | 2024.08.27 |
| MDC를 활용해 쓰레드 전환을 이해해보다. (4) | 2024.08.27 |