Search

섹션 2.

생성일
2025/10/04 12:20
태그

Step의 두 가지 유형

Chunk와 Tasklet

섹션 1에서 Job과 Step에 대해 배우면서, 배치 처리의 일반적인 패턴이 읽고-처리하고-쓰는것이라고 학습했다. 하지만 모든 배치가 이런 식으로 데이터를 다루지는 않는데, 청크 지향 처리 방식태스크릿 지향 처리 방식으로 나눌 수 있다.

태스크릿 지향 처리

태스크릿 지향 처리 모델은 스프링 배치의 가장 기본적인 Step 구현 방식으로, 비교적 복잡하지 않은 단순한 작업을 실행할 때 사용된다.

언제 태스크릿 지향 처리를 사용하는가

일반적인 배치의 Step은 읽고-처리하고-쓰는 ETL 작업에 초점을 맞춘다. 하지만 다음과 같은 단순한 시스템 작업이나 유틸성 작업이 필요할 때가 있다.
매일 새벽 불필요한 로그 파일 삭제
특정 디렉토리에서 오래된 파일을 아카이브
사용자에게 단순한 알림 메세지 또는 이메일 발송
외부 API 호출 후 결과를 단순히 저장하거나 로깅
태스크릿 지향 처리는 이러한 단일 비지니스 로직 실행을 목적으로 설계된 처리방식이다.

태스크릿 지향 처리 구현 방식

태스크릿 지향 처리가 사용되는 사례들은 대부분 함수 호출 하나로 끝날 법한 단순한 작업들이다.
@FunctionalInterface public interface Tasklet { @Nullable RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception; }
Java
복사
우리는 스프링 배치가 제공하는 Tasklet 인터페이스의 execute() 메서드에 우리가 원하는 로직을 구현하고, 이 구현체를 넘기면 알아서 스프링 배치가 그 이후의 실행과 흐름 관리를 수행한다.

Tasklet 구현 예시

class HelloTasklet: Tasklet { private val log = org.slf4j.LoggerFactory.getLogger(this::class.java) private final val totalCount = 10 private var count = 0 override fun execute( contribution: StepContribution, chunkContext: ChunkContext ): RepeatStatus { count++ if (count < totalCount) { // 더 처리해야할 프로세스가 남아있음 return RepeatStatus.CONTINUABLE } log.info("HelloTasklet has finished.") return RepeatStatus.FINISHED } }
Java
복사

RepeatStatus

public enum RepeatStatus { CONTINUABLE(true), FINISHED(false); ... }
Java
복사
스프링 배치에서 Step이 Tasklet의 execute 메서드 실행을 계속 해야할지 멈출지를 결정하는 기준이 RepeatStatus이다.
RepeatStatus.FINISHED : 성공/실패 같은 Step의 처리 결과와 관계없이, 스프링 배치에 해당 Step이 완료되었고 더 이상 반복할 필요 없이 다음 Step으로 넘어가도 된다고 알려주는 상태이다. Job에 따라 다음 Step으로 차근차근 진행된다.
RepeatStatus.CONFINUABLE : 스프링 배치에 Tasklet의 execute 메서드가 추가로 더 실행되어야 함을 알려주는 상태이다. Step의 종료를 보류하고 필요한만큼 execute 메서드를 반복 호출하게 된다.
while 문법을 통해 반복 작업을 처리하면 되지 않을까? 싶은 사람도 있을 것이다. 하지만 RepeatStatus가 필요한 이유는, 짧은 트랜잭션으로 안전하게 처리하기 위해 배치 처리에 반복문 이상의 제어 구조가 필요하기 때문이다. 스프링 배치에서는 Tasklet의 execute 호출마다 새로운 트랜잭션을 시작하고, execute 메서드가 종료되어 RepeatStatus가 반환되면 해당 트랜잭션을 커밋한다. 이를 execute 내부의 반복문으로 직접 구현한다면, 모든 반복 작업이 하나의 트랜잭션으로 처리되고 실행 도중 예외가 발생하면 반복문으로 했던 모든 결과가 롤백될 것이다.
이처럼 RepeatStatus가 필요한 이유는 execute를 반복 실행하기 위해서이고, 반복해서 실행하는 이유는 하나의 거대한 트랜잭션 대신 작은 트랜잭션들로 나누어 안전하게 처리하기 위함이다.

Tasklet을 Step에 등록하기

Tasklet에 로직을 잘 처리하도록 만들었다면, 이제 Step에 등록해보자.
@Bean fun helloTasklet() = HelloTasklet() @Bean fun secondStep(): Step { return StepBuilder("secondStep", jobRepository) // HelloTasklet 추가 .tasklet(HelloTasklet(), transactionManager) .build() }
Kotlin
복사
Step을 구성할 때는 어떤 처리 방식을 사용할 지를 결정해야하는데, 위의 예시처럼 StepBuilder를 통해 tasklet() 메서드를 호출하면 태스크릿 지향 처리 방식의 Step을 생성한다.

TransactionManager

tasklet 메서드에는 태스크릿 구현체 외에도 PlatformTransactionManager 인스턴스도 같이 전달되는데, 트랜잭션 매니저를 같이 전달함에 따라 execute 메서드 실행 중 발생하는 모든 데이터베이스 작업을 하나의 트랜잭션으로 관리할 수 있게 된다.
@Configuration class HelloBatchConfig( private val jobRepository: JobRepository, private val transactionManager: PlatformTransactionManager, )
Kotlin
복사
위의 예시에서는 Spring boot가 자동으로 구성한 TransactionManager 빈을 주입받아서 사용했다. 하지만 파일을 정리하거나, 외부 API를 호출하거나, 단순한 알림을 보내는 등 모든 Tasklet에서 데이터베이스 작업을 처리하지는 않는다.
이런 경우처럼 DB 트랜잭션을 고려할 필요가 없다면, ResourcelessTransactionManager 구현체를 사용할 수 있다. 이 구현체는 아무것도 하지 않는(no-op) 방식으로 동작하기 때문에, 불필요한 DB 트랜잭션 처리를 생략할 수 있다.
@Bean fun lastStep(): Step { return StepBuilder("lastStep", jobRepository) .tasklet({ contribution, chunkContext -> println("This is my last step") RepeatStatus.FINISHED }, ResourcelessTransactionManager()) .build() }
Kotlin
복사
PlatformTransactionManager 빈 등록 주의 위의 예시는 ResourcelessTransactionManager 인스턴스를 새로 생성해서 전달하는데, 여러 스텝에서 재사용할 수 있도록 별도의 Bean으로 등록하고 싶을 수 있다. ResourcelessTransactionManager 구현체는 상관이 없지만 PlatformTransactionManager를 빈으로 등록할 때는 주의가 필요하다. 스프링 배치에서는 내부적으로 Job과 Step의 상태 등의 메타데이터를 DB를 통해 관리한다. 이 때에도 트랜잭션이 사용되는데, 별도의 설정 없이 PlatformTransactionManager를 빈으로 정의한다면 메타데이터 관리 트랜잭션과 Step의 비즈니스 로직 처리 트랜잭션이 같은 빈을 사용하여 의도치 않은 문제가 생길 수 있다.

Tasklet 사용 시나리오

우리가 만든 태스크릿을 실행시키면 이처럼 잘 실행되는 것을 확인할 수 있다.
이제 실제 사례들을 통해 Tasklet이 어떤 작업에 적합한지, 언제 활용해야 하는지에 대해 감을 잡아보자.
오래된 파일 삭제
class DeleteOldFilesTasklet( private val filePath: String, private val daysOld: Int, ): Tasklet { private val log = LoggerFactory.getLogger(this::class.java) override fun execute( contribution: StepContribution, chunkContext: ChunkContext ): RepeatStatus { val dir = File(filePath) val cutoff = System.currentTimeMillis() - daysOld * 24 * 60 * 60 * 1000L dir.listFiles()?.forEach { file -> if (file.lastModified() < cutoff) { if (file.delete()) { log.info("Deleted old file: ${file.name}") } else { log.warn("Failed to delete file: ${file.name}") } } } return RepeatStatus.FINISHED } } @Bean fun deleteOldFilesTasklet() = DeleteOldFilesTasklet("/path/to/directory", 30)
Kotlin
복사
이처럼 Tasklet으로 오래된 파일을 삭제하는 배치 Job을 만들 수 있다.
@Bean fun deleteOldRecordStep(): Step { return StepBuilder("deleteOldRecordStep", jobRepository) .tasklet({ contribution, chunkContext -> val deleted = jdbcTemplate.update("DELETE FROM records WHERE created_at < NOW() - INTERVAL '30 days'") RepeatStatus.FINISHED }, transactionManager) .build() }
Kotlin
복사
혹은 간단한 작업이라면 이처럼 람다식으로 처리하기도 한다.
재고 현황 알림
class DailyInventoryReportTasklet( private val alarmService: AlarmService, private val inventoryService: InventoryService, ): Tasklet { private val log = LoggerFactory.getLogger(this::class.java) override fun execute( contribution: StepContribution, chunkContext: ChunkContext ): RepeatStatus { val lowStockItems = inventoryService.findLowStockItems() if (lowStockItems.isNotEmpty()) { log.info("Low stock alert sent for items: ${lowStockItems.joinToString(", ")}") alarmService.sendLowStockAlert(lowStockItems) } else { log.info("All inventory levels are sufficient.") } return RepeatStatus.FINISHED } }
Kotlin
복사

Tasklet 지향 처리 요약

Tasklet 지향 처리는 파일 삭제, 데이터 초기화, 알림 발송 등 단순하고 명확한 작업을 수행하는데 사용되는 Step 유형이다.
단순 작업에 적합
Tasklet 인터페이스를 구현해 로직 작성 후 tasklet() 메서드로 전달하여 Step 구성
RepeatStatus로 반복 실행 여부를 결정
Tasklet.execute() 메서드를 하나의 트랜잭션을 처리하여, 데이터베이스 일관성 및 원자성 보장

Chunk 지향 처리