
태스크릿 지향 처리의 중심은 Tasklet 인터페이스의 execute() 메서드이다. 이 메서드 안에 해당 Step이 수행해야 할 작업을 정의한다. 청크 방식처럼 Reader/Processor/Writer로 책임을 분해하지 않고, “이 Step에서 해야 할 일”을 하나의 메서드에 명확히 담는다.
아래 예시는 지정한 디렉토리에서 특정 기간보다 오래된 파일을 삭제하는 Tasklet이다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.batch.core.step.tasklet.Tasklet;
import java.io.File;
@Slf4j
public class CleanupTasklet implements Tasklet {
private final String directory;
private final int daysOld;
public CleanupTasklet(String directory, int daysOld) {
this.directory = directory;
this.daysOld = daysOld;
}
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
File dir = new File(directory);
long cutoffTime = System.currentTimeMillis() - (daysOld * 24L * 60 * 60 * 1000);
File[] files = dir.listFiles();
if (files == null) {
log.info("cleanup skipped. directory not found or not readable. directory={}", directory);
return RepeatStatus.FINISHED;
}
int deletedCount = 0;
for (File file : files) {
if (file.lastModified() < cutoffTime) {
boolean deleted = file.delete();
if (deleted) {
deletedCount++;
}
log.info("cleanup file. name={}, deleted={}", file.getName(), deleted);
}
}
log.info("cleanup done. directory={}, daysOld={}, deletedCount={}", directory, daysOld, deletedCount);
return RepeatStatus.FINISHED;
}
}
이 코드에서 중요한 것은 구조다. 태스크릿 Step은 “데이터 흐름”이 아니라 “작업 실행”에 초점이 있다. 따라서 실행 흐름은 execute() 메서드 내부에서 끝까지 드러나며, Step의 종료 여부 또한 RepeatStatus로 명시적으로 표현된다. 이 단순함이 태스크릿 지향 처리의 장점이자, 동시에 설계 책임이 구현 코드에 집중된다는 의미이기도 하다.
Tasklet 구현체는 단독으로 실행되지 않는다. Spring Batch가 실행할 수 있도록 Step으로 감싸야 하며, Step은 다시 Job에 포함되어야 한다. 태스크릿 Step은 StepBuilder.tasklet()을 사용해 구성한다.
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class CleanupBatchConfig {
private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
public CleanupBatchConfig(JobRepository jobRepository,
PlatformTransactionManager transactionManager) {
this.jobRepository = jobRepository;
this.transactionManager = transactionManager;
}
@Bean
public Tasklet cleanupTasklet() {
return new CleanupTasklet("/tmp", 30);
}
@Bean
public Step cleanupStep() {
return new StepBuilder("cleanupStep", jobRepository)
.tasklet(cleanupTasklet(), transactionManager)
.build();
}
@Bean
public Job cleanupJob() {
return new JobBuilder("cleanupJob", jobRepository)
.start(cleanupStep())
.build();
}
}
여기서 핵심은 tasklet(cleanupTasklet(), transactionManager)이다. 이 호출을 통해 Step은 태스크릿 지향 처리 모델로 동작한다. transactionManager가 함께 들어가는 이유는 Spring Batch가 Step 실행 과정에서 트랜잭션 경계를 관리하기 때문이다. 실제로 DB 작업이 없더라도 Step은 메타데이터 관리 등을 위해 트랜잭션 매니저를 요구하는 구조를 가진다.
RepeatStatus는 Tasklet의 실행을 한 번으로 끝낼지, 같은 Tasklet을 다시 실행할지 결정하는 신호다. 반환값은 크게 두 가지다.
RepeatStatus.FINISHED는 작업이 완료되었음을 의미한다. Step은 종료되고 다음 Step으로 진행한다.RepeatStatus.CONTINUABLE은 추가 실행이 필요함을 의미한다. Step은 종료되지 않고 execute()가 다시 호출된다.중요한 점은 RepeatStatus가 단순히 “반복을 위한 장치”가 아니라는 점이다. Spring Batch는 Tasklet의 execute() 호출 단위로 트랜잭션을 시작하고 종료한다. 즉, CONTINUABLE로 여러 번 호출되면 그만큼 트랜잭션 경계도 여러 번 생성된다. 이를 통해 큰 작업을 작은 단위로 나누어 커밋할 수 있고, 중간 실패 시에도 이미 커밋된 구간은 보존된다.
반대로 execute() 내부에 while 문으로 대량 작업을 한 번에 몰아넣으면, 실행 도중 예외가 발생할 때 전체가 하나의 트랜잭션으로 묶여 롤백될 가능성이 커진다. RepeatStatus는 이런 위험을 줄이기 위한 구조적 장치이며, Tasklet이 “안전하게 반복될 수 있는 형태”로 설계되도록 유도한다.
Tasklet은 DB를 다루지 않는 작업에 자주 사용된다. 파일 삭제, 파일 이동, 외부 API 호출처럼 DB 트랜잭션이 실질적으로 필요 없는 경우에도, Step 구성 시 PlatformTransactionManager가 요구된다. 이때 사용할 수 있는 선택지가 ResourcelessTransactionManager이다.
ResourcelessTransactionManager는 실제 DB 커넥션을 관리하지 않는 no-op 성격의 트랜잭션 매니저다. 트랜잭션 API의 형태는 유지하되, 불필요한 리소스 트랜잭션 비용을 줄이는 데 목적이 있다. DB 작업이 없는 Tasklet에서 “형식적인 트랜잭션 경계만 필요”할 때 고려할 수 있다.
적용은 단순하다. 기존 transactionManager 대신 ResourcelessTransactionManager를 넘기면 된다.
import org.springframework.batch.core.Step;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
@Configuration
public class CleanupBatchConfigWithResourceless {
private final JobRepository jobRepository;
public CleanupBatchConfigWithResourceless(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}
@Bean
public Tasklet cleanupTasklet() {
return new CleanupTasklet("/tmp", 30);
}
@Bean
public Step cleanupStep() {
return new StepBuilder("cleanupStep", jobRepository)
.tasklet(cleanupTasklet(), new ResourcelessTransactionManager())
.build();
}
}
다만 이 지점에서 한 가지는 분리해서 이해해야 한다. Tasklet 로직의 비즈니스 트랜잭션과, Spring Batch가 메타데이터(잡/스텝 실행 상태)를 관리하기 위해 사용하는 트랜잭션은 성격이 다르다. 트랜잭션 매니저를 애플리케이션 전역에서 무심코 재정의하거나 단일 빈으로 통일하면, 의도하지 않은 동작을 만들 수 있다. 따라서 ResourcelessTransactionManager는 “DB 작업이 없는 Tasklet Step 내부에서만” 국소적으로 사용하는 접근이 안전하다.