Spring Batch 입문 19편. Chunk-oriented Step 의 대척점 — TaskletStep. RepeatStatus.FINISHED·CONTINUABLE, MethodInvokingTaskletAdapter, SystemCommandTasklet, 단발 SQL·script·정리 작업 패턴, transaction 단위까지 정리한 학습 노트.
이 글은 Spring Batch 입문에서 운영까지 시리즈 48편 중 19편이에요. 18편 까지 Part 3 (Step·Chunk Processing) 을 끝냈다면, 이번 19편부터는 Part 4 — TaskletStep · Flow Control 로 들어갑니다. 첫 주제는 TaskletStep.
왜 TaskletStep 이 필요한가
지금까지 Step = Chunk-oriented Processing (대량 데이터를 묶음 단위로 처리) — read → process → write 패턴.
근데 실무에서 자주 등장하는 case 가 있어요. Stored Procedure (DB 안에 저장된 프로시저) 한 번 호출, FTP 파일 정리·압축, DB 통계 테이블 일괄 갱신, 외부 API 한 번 호출 후 끝, Step 시작 전·끝난 후 환경 준비/정리 같은 것들이죠.
이런 단발성 작업을 Chunk-oriented 로 억지로 짜면 no-op ItemWriter (아무 일도 안 하는 빈 writer) 같은 이상한 코드가 나와요. → TaskletStep.
Tasklet 인터페이스 — 단 1개의 메서드
public interface Tasklet {
RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) throws Exception;
}
이게 끝. execute 메서드 하나가 반복 호출되어 RepeatStatus.FINISHED 를 return 하거나 예외 throw 할 때까지 돕니다.
RepeatStatus — 두 가지 신호
public enum RepeatStatus {
CONTINUABLE,
FINISHED;
}
FINISHED— Step 종료CONTINUABLE— Taskletexecute한 번 더 호출 (각 호출 = 새 transaction)
대부분 케이스 = FINISHED 한 번 return. CONTINUABLE 은 조건 만족할 때까지 반복하는 패턴 (배치 종료 조건이 외부 상태인 경우).
TaskletStep 구성
@Bean
public Step myStep(JobRepository repo, PlatformTransactionManager tx) {
return new StepBuilder("myStep", repo)
.tasklet(myTasklet(), tx)
.build();
}
@Bean
public Tasklet myTasklet() {
return (contribution, chunkContext) -> {
// 작업 코드
return RepeatStatus.FINISHED;
};
}
.tasklet() 호출 시 .chunk() 와 동시 사용 금지. 둘은 상호 배타예요.
execute 의 두 인자
1. StepContribution
public class StepContribution {
int readCount;
int writeCount;
int filterCount;
ExitStatus exitStatus;
StepExecution stepExecution; // 부모 참조
}
현재 chunk 의 누적 통계 (read·write·filter count) 를 담아요. ExitStatus (Step 종료 상태 코드) 와 StepExecution (Step 실행 인스턴스) 까지 들고 있고요. Tasklet 안에서 수동 증가도 가능 — 메트릭 보고용으로 쓰입니다.
contribution.incrementWriteCount(processedCount);
contribution.setExitStatus(ExitStatus.COMPLETED.addExitDescription("처리 완료"));
2. ChunkContext
public class ChunkContext {
Map<String, Object> attributes;
StepContext stepContext; // StepExecution + JobParameters 접근
}
임시 상태 저장 + Step·Job context 접근용. JobParameters (Job 실행 인자) 와 ExecutionContext (재시작용 영속 상태) 까지 도달합니다.
JobParameters params = chunkContext.getStepContext()
.getStepExecution().getJobParameters();
String inputFile = params.getString("inputFile");
TaskletStep 의 transaction 단위
Tasklet 1회 호출 = 1 transaction. execute 진입 시 transaction 시작, execute 종료 (FINISHED 또는 CONTINUABLE return) 시 commit, 예외 throw 시 rollback.
CONTINUABLE 로 반복 = 매 호출마다 새 transaction. 짧은 transaction 여러 번이 긴 transaction 1번보다 안전한 시나리오에 유리해요.
단발 Tasklet 예제 — 파일 정리
public class FileDeletingTasklet implements Tasklet, InitializingBean {
private Resource directory;
@Override
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) throws Exception {
File dir = directory.getFile();
Assert.state(dir.isDirectory(), "The resource must be a directory");
File[] files = dir.listFiles();
for (File file : files) {
boolean deleted = file.delete();
if (!deleted) {
throw new UnexpectedJobExecutionException(
"Could not delete file " + file.getPath());
}
}
return RepeatStatus.FINISHED; // 한 번만 실행
}
public void setDirectoryResource(Resource directory) {
this.directory = directory;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.state(directory != null, "Directory must be set");
}
}
특징을 보면, 디렉토리 정리는 1회면 충분하니까 FINISHED 로 끝나고, 예외 발생 시 throw 하면 Step rollback 으로 이어집니다. Tasklet 과 InitializingBean (스프링 bean 초기화 후 콜백 인터페이스) 을 동시 구현했기 때문에 bean 초기화 시 의존성 검증까지 같이 되고요.
Job 연결
@Bean
public Job taskletJob(JobRepository repo, Step deleteFilesStep) {
return new JobBuilder("taskletJob", repo)
.start(deleteFilesStep)
.build();
}
@Bean
public Step deleteFilesStep(JobRepository repo, PlatformTransactionManager tx) {
return new StepBuilder("deleteFilesStep", repo)
.tasklet(fileDeletingTasklet(), tx)
.build();
}
@Bean
public FileDeletingTasklet fileDeletingTasklet() {
FileDeletingTasklet tasklet = new FileDeletingTasklet();
tasklet.setDirectoryResource(new FileSystemResource("target/test-outputs/test-dir"));
return tasklet;
}
CONTINUABLE 패턴 — 외부 조건 폴링
@Bean
public Tasklet pollingTasklet() {
return (contribution, chunkContext) -> {
boolean ready = checkExternalSystem();
if (ready) {
doFinalWork();
return RepeatStatus.FINISHED;
}
Thread.sleep(5000);
return RepeatStatus.CONTINUABLE; // 5초 후 다시
};
}
외부 시스템이 준비될 때까지 폴링하는 패턴이에요. 각 호출이 독립 transaction 으로 돕니다. 주의할 점은 세 가지인데, 무한 폴링을 막으려면 카운트 또는 timeout 체크를 꼭 추가해야 하고, Thread.sleep 안에서도 transaction 은 살아 있어 DB 커넥션을 계속 점유하니까 sleep 은 짧게 잡는 게 좋고, 더 권장하는 패턴은 38편 Repeat 의 RepeatTemplate (Spring Batch 반복 추상화) 이나 scheduled job (스케줄러로 분리한 잡) 으로 빼는 거예요.
MethodInvokingTaskletAdapter — 기존 DAO 재사용
기존 메서드를 Tasklet 으로 wrapping 할 수 있어요.
@Bean
public MethodInvokingTaskletAdapter myTasklet(FooDao fooDao) {
MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter();
adapter.setTargetObject(fooDao);
adapter.setTargetMethod("updateFoo");
return adapter;
}
fooDao.updateFoo() 가 그대로 Tasklet 으로 변환됩니다. 반환값이 void 또는 non-null 이면 FINISHED, null 이면 CONTINUABLE 로 해석돼요.
사용 시점
- 기존 Service·DAO 메서드를 그대로 batch 의 Step 으로 활용
- Tasklet 인터페이스 의존성 없이 POJO (평범한 자바 객체) 유지
한계
- StepContribution·ChunkContext 접근 불가 (메트릭·context 활용 X)
- 인자 전달은
setArguments(Object[])로 정적 — JobParameters 동적 주입은 21편 Late Binding 와 결합 필요
SystemCommandTasklet — OS 명령 실행
@Bean
public SystemCommandTasklet shellTasklet() {
SystemCommandTasklet tasklet = new SystemCommandTasklet();
tasklet.setCommand("rsync", "-avz", "src/", "dst/");
tasklet.setTimeout(60_000); // 60초
tasklet.setInterruptOnCancel(true);
return tasklet;
}
외부 shell command 를 실행합니다. timeout·termination check·환경 변수까지 세밀하게 제어할 수 있고요.
주의
exit code 0 = COMPLETED, non-zero = FAILED 가 default 입니다. Long-running command 라면 setTimeout() 을 꼭 걸어야 하고요. 보안 측면에서는 사용자 입력을 직접 command 에 넣지 않게 조심해야 합니다 (injection 위험).
CallableTaskletAdapter — Callable 기반
@Bean
public CallableTaskletAdapter callableTasklet() {
CallableTaskletAdapter adapter = new CallableTaskletAdapter();
adapter.setCallable(() -> {
doWork();
return RepeatStatus.FINISHED;
});
return adapter;
}
Callable<RepeatStatus> 를 Tasklet 으로 변환해 줘요. Java 동시성 API 와 함께 쓸 때 유용합니다.
Tasklet · Listener 자동 등록
If it implements the StepListener interface, TaskletStep automatically registers the tasklet as a StepListener.
Tasklet 이 StepExecutionListener (Step 시작/종료 콜백 리스너) 도 같이 구현하면 자동으로 listener 등록이 됩니다. 18편 Step Listener 자동 등록 규칙과 동일해요.
public class MyTasklet implements Tasklet, StepExecutionListener {
@Override
public RepeatStatus execute(...) { ... }
@Override
public void beforeStep(StepExecution exec) {
// Step 시작 hook
}
@Override
public ExitStatus afterStep(StepExecution exec) {
// Step 종료 hook
return exec.getExitStatus();
}
}
TaskletStep · ItemReader 결합 — 함정
Tasklet 안에서 ItemReader (배치 입력 추상화) 를 직접 사용하면 ItemStream (재시작 상태 저장 인터페이스) 자동 등록이 안 됩니다 (17편).
public class CustomTasklet implements Tasklet {
@Autowired
private FlatFileItemReader<Item> reader;
@Override
public RepeatStatus execute(...) {
Item item;
while ((item = reader.read()) != null) {
process(item);
}
return RepeatStatus.FINISHED;
}
}
@Bean
public Step myStep(...) {
return new StepBuilder("myStep", repo)
.tasklet(customTasklet(), tx)
.stream(reader()) // ★ 수동 등록 필수
.build();
}
.stream() 으로 명시 등록해서 재시작 안전성을 확보해야 합니다.
TaskletStep vs Chunk-oriented — 선택 기준
| 시나리오 | 권장 |
|---|---|
| 대량 데이터 read→process→write | Chunk-oriented |
| 단발 SQL·SP 호출 | Tasklet |
| 파일 정리·압축·이동 | Tasklet |
| 외부 API 1회 호출 | Tasklet |
| Job 시작/종료 전후 환경 준비 | Tasklet |
| OS 명령 실행 | SystemCommandTasklet |
| 외부 시스템 폴링 | Tasklet (CONTINUABLE) 또는 Scheduled Job |
| 기존 DAO 메서드 재사용 | MethodInvokingTaskletAdapter |
대체로 read·process·write 의 명시적 분리 + 대량 데이터 = Chunk. 그 외 = Tasklet.
자주 만나는 사고
사고 1: .tasklet() + .chunk() 동시 호출
원인 — 두 메서드가 상호 배타.
해결 — 둘 중 하나만 호출. Step 분리 권장.
사고 2: CONTINUABLE 의 무한 루프
원인 — FINISHED return 조건 누락.
해결 — 카운트·시간 제한 추가. 무한 폴링은 Scheduled job 으로 분리.
사고 3: Tasklet 안 외부 시스템 호출 실패
원인 — 외부 호출이 현재 transaction 안에서 일어남. 외부 실패 시 rollback 발생.
해결 — transaction 분리 또는 비동기 + retry.
사고 4: Tasklet 안 ItemReader 재시작 안 됨
원인 — .stream() 등록 누락.
해결 — .stream(reader) 명시 (17편 참고).
사고 5: MethodInvokingTaskletAdapter 인자 전달
원인 — setArguments() 가 static. JobParameters 동적 주입이 안 됨.
해결 — 21편 Late Binding 패턴을 쓰거나 Tasklet 을 직접 구현.
운영 권장 패턴
Pattern 1: Job 전후 정리 Step
@Bean
public Job dataJob(JobRepository repo, Step prepareStep, Step mainStep, Step cleanupStep) {
return new JobBuilder("dataJob", repo)
.start(prepareStep) // TaskletStep: 환경 준비
.next(mainStep) // Chunk-oriented: 데이터 처리
.next(cleanupStep) // TaskletStep: 파일 정리·통계 갱신
.build();
}
Tasklet → Chunk → Tasklet 흐름. 가장 흔한 batch 구조예요.
Pattern 2: 외부 시스템 사전 검증
@Bean
public Tasklet preCheckTasklet() {
return (contribution, chunkContext) -> {
if (!externalSystem.isAvailable()) {
throw new IllegalStateException("External system down");
}
return RepeatStatus.FINISHED;
};
}
Job 시작 시 외부 시스템 상태를 fail-fast (문제를 일찍 드러내서 즉시 중단) 로 확인합니다 — 큰 batch 시작 전 안전 게이트.
Pattern 3: 결과 통계 통보
public class StatsReportTasklet implements Tasklet {
@Override
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) {
StepContext stepCtx = chunkContext.getStepContext();
JobExecution jobExec = stepCtx.getStepExecution().getJobExecution();
long total = jobExec.getStepExecutions().stream()
.mapToLong(StepExecution::getWriteCount).sum();
slackService.notify("Batch completed: " + total + " items");
return RepeatStatus.FINISHED;
}
}
이전 Step 들의 누적 통계를 Slack·이메일로 통보합니다.
Pattern 4: 멱등성 보장 Tasklet
@Bean
public Tasklet idempotentTasklet() {
return (contribution, chunkContext) -> {
String marker = "lastRun." + LocalDate.now();
ExecutionContext ctx = chunkContext.getStepContext()
.getStepExecution().getExecutionContext();
if (ctx.containsKey(marker)) {
// 이미 실행됨 — 스킵
return RepeatStatus.FINISHED;
}
doWork();
ctx.put(marker, true);
return RepeatStatus.FINISHED;
};
}
ExecutionContext 마커로 재시작 시 중복 실행을 막습니다.
시험 직전 한 번 더 — TaskletStep 함정 압축 노트
- Tasklet 인터페이스 =
execute(StepContribution, ChunkContext)메서드 1개 - return =
RepeatStatus.FINISHED(종료) 또는CONTINUABLE(한 번 더) - Tasklet 1회 호출 = 1 transaction —
execute진입~종료까지 - 예외 throw = Step rollback
.tasklet()과.chunk()= 상호 배타 (동시 호출 금지)- StepContribution = 현재 chunk 통계·ExitStatus
- ChunkContext = 임시 상태 + StepContext·JobExecution 접근
- ChunkContext 에서 JobParameters 접근 —
chunkContext.getStepContext().getStepExecution().getJobParameters() - MethodInvokingTaskletAdapter = 기존 메서드 → Tasklet 변환,
setTargetObject·setTargetMethod - 반환 void·non-null = FINISHED, null = CONTINUABLE
- SystemCommandTasklet = OS 명령 실행,
setTimeout()필수 - exit code 0 = COMPLETED, non-zero = FAILED
- CallableTaskletAdapter =
Callable<RepeatStatus>→ Tasklet - Tasklet 이
StepExecutionListener도 구현 = 자동 listener 등록 - Tasklet 안 ItemReader 사용 =
.stream(reader)수동 등록 필수 (17편) - 함정 — CONTINUABLE 무한 루프 (FINISHED 조건 누락)
- 함정 —
.tasklet()+.chunk()동시 = 예외 - 함정 — 외부 호출 실패 → transaction rollback
- 함정 — MethodInvokingTaskletAdapter 의 setArguments = static (JobParameters 동적 주입은 21편 Late Binding)
- 패턴 — Job 전후 Tasklet 정리 Step
- 패턴 — Pre-check fail-fast
- 패턴 — 결과 통계 통보 (이전 Step writeCount 합산)
- 패턴 — ExecutionContext 마커로 멱등성
공식 문서: TaskletStep 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 14편 — Skip Logic (부분 실패 무시 · SkipPolicy · SkipListener)
- 15편 — Retry Logic (일시적 실패 자동 재시도)
- 16편 — Transaction Attributes (Isolation · Propagation · Timeout)
- 17편 — ItemStream 등록 (재시작 안전성의 핵심)
- 18편 — Step 라이프사이클 Listener 종합
다음 글: