Backend
home

스프링 배치의 두 가지 스텝 유형

생성일
2025/07/12 09:03
태그
섹션2

태스크릿(Tasklet) 지향 처리란?

태스크릿(Tasklet) 지향 처리 모델은 Spring Batch에서 가장 기본적인 Step 구현 방식으로 비교적 복잡하지 않은 단순한 작업을 실행할 때 사용된다.
단순한 작업이라는 말이 바로 와닿지 않을 수 있다. 언제 태스크릿 지향 처리가 사용되는지를 먼저 알면 이해가 훨씬 쉬워진다.

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

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

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

앞서 살펴본 태스크릿 지향 처리가 사용되는 사례들은 대부분 함수 호출 하나로 끝날 법한 단순한 작업들이다. 따라서 복잡하게 생각할 필요 없다. 우리가 해야 할 일은 필요한 로직을 작성하고, 이를 Spring Batch에 전달하는 것뿐이다.
조금 더 기술적으로 말하자면, Spring Batch가 제공하는 Tasklet 인터페이스의 execute() 메서드에 우리가 원하는 로직을 구현하고, 이 구현체를 Spring Batch에 넘기기만 하면 된다. 그 이후의 실행과 흐름 관리는 Spring Batch가 알아서 처리한다.
@FunctionalInterface public interface Tasklet { @Nullable RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception; }
Java
복사

실습

package com.system.batch.kill_batch_system.config; import com.system.batch.kill_batch_system.tasklet.ZombieProcessCleanupTasklet; import lombok.RequiredArgsConstructor; 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.batch.support.transaction.ResourcelessTransactionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.PlatformTransactionManager; @Configuration @RequiredArgsConstructor public class ZombieBatchConfig { private final JobRepository jobRepository; // private final PlatformTransactionManager transactionManager; // no-op(아무것도 하지 않는) 방식으로 동작하는 PlatformTransactionManager 구현체로, // 이를 사용하면 불필요한 DB 트랜잭션 처리를 생략할 수 있다. // new ResourcelessTransactionManager() @Bean public Tasklet zombieProcessCleanupTasklet() { return new ZombieProcessCleanupTasklet(); } // Step - 어떤 처리 방식을 사용할 것인가가 핵심 // 태스크릿 지향 처리 방식의 Step @Bean public Step zombieCleanupStep() { return new StepBuilder("zombieCleanupStep", jobRepository) .tasklet(zombieProcessCleanupTasklet(), new ResourcelessTransactionManager()) .build(); } @Bean public Job zombieCleanupJob() { return new JobBuilder("zombieCleanupJob", jobRepository) .start(zombieCleanupStep()) .build(); } }
Java
복사

RepeatStatus의 두 얼굴: FINISHED vs CONTINUABLE

RepeatStatus.FINISHED :
"다 끝났다. 이제 Step을 종료해도 된다."
Step의 처리가 성공이든 실패든 상관없이 해당 Step이 완료되었음을 의미한다.
더 이상 반복할 필요 없이 다음 단계로 넘어가며, 배치 잡은 차근차근 다음 스텝으로 진행된다.
RepeatStatus.CONTINUABLE : "
작업 진행 중. 추가 실행이 필요하다."
Tasklet의 execute()메서드가 추가로 더 실행되어야 함을 Spring Batch Step에 알리는 신호다. Step의 종료는 보류되고, 필요한 만큼 execute()메서드가 반복 호출된다.

RepeatStatus가 필요한 이유: 짧은 트랜잭션을 활용한 안전한 배치 처리

"반복 작업이라면 while문으로 처리하면 되는 거 아닌가?"
Spring Batch는 Tasklet의 execute() 호출 마다 새로운 트랜잭션을 시작하고 execute()의 실행이 끝나 RepeatStatus가 반환되면 해당 트랜잭션을 커밋한다.
execute()메서드 내부에 반복문을 직접 구현했다고 가정해보자. 이 경우 모든 반복 작업이 하나의 트랜잭션 안에서 실행된다. 만약 실행 도중 예외가 발생하면, 데이터베이스 결과가 execute()호출 전으로 롤백되어 버린다.
예를 들어, 오래된 주문 데이터를 정리하는 배치 작업을 생각해보자. 한 번에 만 건씩 데이터를 삭제하는데, 총 100만 건의 데이터를 처리해야 한다고 하자.
execute() 내부에서 while문을 사용한다면: 80만 건째 처리 중 예외가 발생했을 때, 이미 처리한 79만 건의 데이터도 모두 롤백되어 하나도 정리되지 않은 상태로 돌아간다
RepeatStatus.CONTINUABLE로 반복한다면: 매 만 건 처리마다 트랜잭션이 커밋되므로, 예외가 발생하더라도 79만 건의 데이터는 이미 안전하게 정리된 상태로 남는다

매일 밤 7일이 지난 레코드를 created 컬럼 기준으로 삭제

@Bean public Step deleteOldRecordsStep() { return new StepBuilder("deleteOldRecordsStep", jobRepository) .tasklet((contribution, chunkContext) -> { int deleted = jdbcTemplate.update("DELETE FROM logs WHERE created < NOW() - INTERVAL 7 DAY"); log.info("🗑️ {}개의 오래된 레코드가 삭제되었습니다.", deleted); return RepeatStatus.FINISHED; }, transactionManager) .build(); } @Bean public Job deleteOldRecordsJob() { return new JobBuilder("deleteOldRecordsJob", jobRepository) .start(deleteOldRecordsStep()) // Step을 Job에 등록 .build(); }
Java
복사
위의 예제와 같이 단순한 작업이라면 별도의 Tasklet 구현 클래스를 만들지 않고, 람다 표현식을 사용해 Step 구성 중에 바로 Tasklet을 정의할 수 있다. 복잡한 로직이 필요하지 않다면 이 방식이 훨씬 직관적이고 간편하다.
간단한 작업 = 람다식, 복잡한 작업 = 별도 Tasklet 클래스

태스크릿(Tasklet) 지향 처리 – 한눈에 정리하기

지금까지 우리는 Tasklet이 무엇이며, 어떻게 동작하는지 살펴보았다. 좀비 프로세스를 처형하고, 오래된 파일을 삭제하며, 재고 현황을 알리는 Tasklet까지 만들어봤다.
하지만 학습의 마무리는 언제나 정리다. 지금부터 Tasklet의 주요 특징과 핵심 개념을 정리해보자.
Tasklet 지향 처리는 Spring Batch에서 단순하고 명확한 작업을 수행하는 데 사용되는 Step 유형이다.
파일 삭제, 데이터 초기화, 알림 발송 등 비교적 단순한 작업에 적합하다.
단순 작업에 적합
태스크릿 지향 처리는 알림 발송, 파일 복사, 오래된 데이터 삭제 등 단순 작업을 처리하는 Step 유형이다.
Tasklet 인터페이스 구현
Tasklet 인터페이스를 구현해 필요한 로직을 작성한 뒤, 이를 StepBuilder.tasklet() 메서드에 전달해 Step을 구성한다.
RepeatStatus로 실행 제어Tasklet.execute() 메서드는 RepeatStatus를 반환하며, 이를 통해 실행 반복 여부를 결정할 수 있다.
트랜잭션 지원
Spring Batch는 Tasklet.execute() 메서드 실행 전후로 트랜잭션을 시작하고 커밋하여, 데이터베이스의 일관성과 원자성을 보장한다.
지금까지 살펴본 태스크릿 지향 처리를 이렇게 정의하며 챕터를 마친다.
"Tasklet. 단순하지만 치명적인 배치 암살자.. - KILL-9

Step의 또 다른 얼굴 – 청크 지향 처리

이제Spring Batch Step의 또 다른 얼굴, 청크 지향 처리(aka COP)를 알아볼 차례다.
Spring Batch를 접하며 다룰 대부분의 배치 작업, 특히 데이터를 다루는 작업은 읽기 → 처리 → 쓰기라는 공통된 패턴을 보인다. Spring Batch도 데이터를 다룰 때 이 패턴을 따른다. 그리고 이 방식을 Spring Batch에서는 청크 지향 처리라고 부른다.
그런데, 왜 이름에 '청크(Chunk)'라는 단어가 붙었을까? 이 의문을 시작으로, 청크 지향 처리의 세계로 들어가 보자.

Chunk – 데이터를 작은 덩어리로 나누어 처리하는 방식

청크(Chunk)는 데이터를일정 단위로 쪼갠 덩어리를 말한다. Spring Batch에서 데이터 기반 처리 방식을 청크 지향 처리라고 부르는 이유는, 읽고, 처리하고, 쓰는 작업을 일정 크기로 나눈 데이터 덩어리(청크)를 대상으로 하기 때문이다.
백만 건의 데이터를 처리해야 한다고 가정해보자. Spring Batch는 100만 건 전체를 한 번 읽고 처리하고 쓰지 않는다. 대신, 100개씩 쪼개서 읽고, 처리하고, 저장한다. 이렇게 나뉜 100개의 묶음이 바로 청크다.
예를 들어, 전체 데이터 100만 건이 있을 때, Spring Batch는 이를 100건씩 청크(Chunk) 단위로 나누어 처리한다. 각 청크 단위로 읽기(Read), 처리(Process), 쓰기(Write)의 과정을 거치며, 이 과정이 1만 번 반복된다.
1. 메모리를 지켜라 – 데이터 폭탄 방지
100만 건을 한 번에 메모리에 올리는 건 자살행위다. DB에서 데이터를 불러오는 순간, 메모리 과부하로 시스템이 뻗는다. 하지만 청크 지향 처리는 다르다.
100개씩 나눠서 불러온다.
개념적으로, 메모리엔 단 100개의 데이터만 존재한다.
이 방식 덕분에 메모리 사용량은 안정적이고, 시스템은 무리 없이 데이터 처리를 이어갈 수 있다.
2. 가벼운 트랜잭션 – 작은 실패
트랜잭션은 작업의 성공 또는 실패를 하나의 단위로 묶는 것이라고 볼 수 있다. 하지만, 100만 건을 하나의 트랜잭션으로 처리한다면? 작업 중간에 오류가 발생하면, 100만 건이 전부 롤백된다.
생각만 해도 아찔하다. 청크 지향 처리는 이를 방지한다.
청크 단위로 트랜잭션을 나눈다. 따라서 데이터 100개 단위로 처리가 성공하면 커밋하고, 실패하면 롤백한다.
만약 작업 중간에 에러가 발생하면?
이전 청크는 이미 커밋 완료.
에러가 발생한 청크만 롤백되고 해당 청크부터 재시작하면 된다.
즉, 복구가 쉽고 빠르다. 청크는 작지만 강력한 복구 시스템이다. 작업의 실패를 작은 실패로 제한해준다.
그렇다면 이 패턴이 Spring Batch에서 어떻게 구체화될까?
Spring Batch는 이 읽기-가공깎기-쓰기 패턴을 딱 세 가지 구성 요소로 나눈다.
바로 ItemReader, ItemProcessor, ItemWriter

청크 지향 처리의 3대장 – 읽고, 깎고, 쓴다

자, 이름부터 심플하다.
읽는다(ItemReader), 깎는다(ItemProcessor), 쏜다(ItemWriter).
이 배치 3대장으로 청크 지향 처리가 완료된다.
진정한 시스템 학살자는 '가공한다' 따위로 말하지 않는다.
'깎는다' – 더 거칠고, 더 확실하게.
데이터는 깎아야 제 맛이다.
이제 배치 3대장이 각각 어떤 역할을 하는지 가볍게 살펴보자.

1. ItemReader – 데이터를 끌어오는 수혈관

데이터를 읽어오는 것은 배치의 생명줄이다. ItemReader는 데이터라는 피를 시스템으로 수혈하는 관이다.
public interface ItemReader<T> { T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException; }
Java
복사
한 번에 하나씩, 차례차례:read() 메서드의 반환 타입을 주목하라. read() 메서드는 아이템을 하나씩 반환한다. 여기서 아이템이란 파일의 한 줄 또는 데이터베이스의 한 행(row)과 같이 데이터 하나를 의미한다.
예를 들어, 총 100만 건의 레코드가 있다면, 각각의 레코드를 아이템이라고 부른다.
ItemReader는 데이터 소스(DB, 파일 등)에서 데이터를 하나씩 순차적으로 읽어온다.
읽을 데이터가 더 이상 없으면 null을 반환하며, 스텝은 종료된다.
ItemReader가 null을 반환하는 것이 청크 지향 처리 Step의 종료 시점이라는 점을 반드시 기억하라. 이는 Spring Batch가 Step의 완료를 판단하는 핵심 조건이다.
다양한 구현체 제공: Spring Batch는 파일, 데이터베이스, 메시지 큐 등 다양한 데이터 소스에 대한 표준 구현체를 제공한다. 예를 들어, FlatFileItemReader는 CSV나 텍스트 파일에서 데이터를 읽어오고, JdbcCursorItemReader는 관계형 데이터베이스로부터 데이터를 읽어 온다.

2. ItemProcessor – 아이템 깎기

ItemProcessor는 데이터를 원하는 형태로 깎아내는 가공 담당자다. ItemReader가 넘긴 원재료를 받아다가, 필요한 모양으로 다듬는 작업을 한다.
public interface ItemProcessor<I, O> { O process(I item) throws Exception; }
Java
복사
데이터 가공: 입력 데이터(I)를 원하는 형태(O)로 변환하거나, 필요하면 필터링도 가능하다. 예를 들어, 숫자를 문자열로 변환하거나, 특정 조건에 맞지 않는 데이터를 걸러낼 수 있다.
필터링 - 필요 없으면 버려라: ItemProcessor의process() 메서드에서 null을 반환하면 해당 데이터는 자동으로 뒤 단계에서 걸러진다.
필수 아님: 데이터 가공이 필요하지 않다면, ItemProcessor는 생략 가능하다. 즉, 읽고 바로 쓸 수 있다.

3. ItemWriter – 결과물을 새기는 데이터 집행자

ItemWriter는 ItemProcessor가 만든 결과물을 받아, 원하는 방식으로 최종 저장/출력한다. - 데이터를 DB에 INSERT, 파일에 WRITE, 메시지 큐에 PUSH
public interface ItemWriter<T> { void write(Chunk<? extends T> chunk) throws Exception; }
Java
복사
한 덩어리씩 쓴다: ItemWriter는 데이터를 한 건씩 쓰지 않는다. Chunk 단위로 묶어서 한 번에 데이터를 쓴다.write()메서드의 파라미터 타입이 Chunk 인 것에 주목하자. ItemReader와 ItemProcessor가 아이템을 하나씩 반환하고 입력받는 것과 달리, ItemWriter는 데이터 덩어리(Chunk)를 한 번에 입력받아 한 번에 쓴다.
다양한 구현체 제공: Spring Batch는 파일, 데이터베이스, 외부 시스템 전송 등에 사용할 수 있는 다양한 구현체를 제공한다. 예를 들어, FlatFileItemWriter는 파일에 데이터를 기록하고, JdbcBatchItemWriter는 데이터베이스에 데이터를 저장한다.
 [시스템 인텔리전스]
Reader-Processor-Writer 패턴
ItemReader, ItemProcessor, ItemWriter로 구성된 청크 지향 처리의 장점을 분석해보자.
1. 완벽한 책임 분리
각 컴포넌트는 자신의 역할만 수행한다. ItemReader는 읽기, ItemProcessor는 깎기(가공), ItemWriter는 쓰기에만 집중한다. 덕분에 코드는 명확해지고 유지보수는 간단해진다.
2. 재사용성 극대화
컴포넌트들은 독립적으로 설계되어 있어 어디서든 재사용 가능하다. 새로운 배치를 만들 때도 기존 컴포넌트들을 조합해서 빠르게 구성할 수 있다.
3. 높은 유연성
요구사항이 변경되어도 해당 컴포넌트만 수정하면 된다. 데이터 형식이 바뀌면 ItemProcessor만, 데이터 소스가 바뀌면 ItemReader만 수정하면 된다. 이런 독립성 덕분에 변경에 강하다.
4. 대용량 처리의 표준
데이터를 다루는 배치 작업은 결국 '읽고-처리하고-쓰는' 패턴을 따른다. Spring Batch는 이 패턴을 완벽하게 구조화했다.
결론: ItemReader, ItemProcessor, ItemWriter로 구성된 이 패턴은 대용량 데이터를 처리하기 위한 최적의 아키텍처다.

청크 지향 처리 조립하기

@Bean public Step processStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("processStep", jobRepository) .<CustomerDetail, CustomerSummary>chunk(10, transactionManager) // 청크 지향 처리 활성화 .reader(itemReader()) // 데이터 읽기 담당 .processor(itemProcessor()) // 데이터 처리 담당 .writer(itemWriter()) // 데이터 쓰기 담당 .build(); } @Bean public Job customerProcessingJob(JobRepository jobRepository, Step processStep) { return new JobBuilder("customerProcessingJob", jobRepository) .start(processStep) // processStep으로 Job 시작 .build(); }
Java
복사
1) 청크 사이즈 지정
.chunk(10, transactionManager)
Java
복사
chunk() 메서드의 첫 번째 파라미터로 청크의 크기를 지정한다. 여기서는 10을 지정했는데, 이는 데이터를 10개씩 묶어서 처리하겠다는 의미다. ItemReader가 데이터 10개를 읽어오면, 이를 하나의 청크로 만들어 ItemProcessor에서 처리하고 ItemWriter에서 쓰기를 수행한다.
2) 제네릭 타입으로 데이터 흐름 정의
.<CustomerDetail, CustomerSummary>chunk(..)
Java
복사
chunk() 메서드의 제네릭 타입을 지정해 청크 처리 과정에서의 데이터 타입 변환 흐름을 정의한다.
첫 번째 타입(CustomerDetail): ItemReader가 반환할 타입. 예를 들어 파일에서 읽은 데이터를 CustomerDetail 객체로 변환하여 반환한다.
두 번째 타입(CustomerSummary): CustomerDetail를 입력 받은 ItemProcessor가 아이템을 처리 후 반환할 타입이자 ItemWriter가 전달받을 Chunk의 제네릭 타입이다.
이제 전체적인 데이터 흐름을 자세히 살펴보자.
1.
ItemReader는 read() 호출마다 CustomerDetail 객체를 반환한다
2.
ItemProcessor는 이 CustomerDetail 타입을 입력받아 가공 로직을 수행한 후 CustomerSummary로 변환하여 반환한다
3.
이렇게 반환된 CustomerSummary들이 청크 단위로 모여 Chunk<CustomerSummary>가 된다
4.
최종적으로 이 Chunk<CustomerSummary>가 ItemWriter.write() 메서드의 파라미터로 전달된다
즉, 제네릭으로 지정한 두 타입은 청크 처리 과정에서 데이터가 어떻게 변환되어 흘러가는지를 명시적으로 나타내는 것이다.
chunk() 메서드를 호출해 Step을 청크 지향 처리로 동작하도록 했다면, 이제 실제 데이터를 처리할 3대장을 구성해야 한다. 빌더의 reader()processor()writer() 메서드를 통해 각각의 구현체를 빌더에 전달한다.
.<CustomerDetail, CustomerSummary>chunk(10, transactionManager) .reader(itemReader())// ItemReader 구현체 전달.processor(itemProcessor())// ItemProcessor 구현체 전달.writer(itemWriter())// ItemWriter 구현체 전달
Scss
복사
빌더의 각 메서드는 다음과 같은 역할을 한다.
reader(): ItemReader 구현체를 전달받아 Step의 읽기 작업을 담당할 컴포넌트로 등록한다.
processor(): ItemProcessor 구현체를 전달받아 Step의 처리 작업을 담당할 컴포넌트로 등록한다. ItemProcessor가 필요하지 않다면 생략 가능하며, 이 경우 ItemReader가 읽은 데이터가 그대로 ItemWriter로 전달된다.
writer(): ItemWriter 구현체를 전달받아 Step의 쓰기 작업을 담당할 컴포넌트로 등록한다.

청크 지향 처리의 흐름

Spring Batch의 청크 지향 처리는 읽기-깎기-쓰기를 청크 크기 단위로 묶어서 반복한다는 게 전부다.
1.데이터 읽기 (ItemReader)
ItemReader는 데이터 소스에서 하나씩 데이터를 읽어온다. read() 메서드가 호출될 때마다 데이터를 순차적으로 반환하며, 청크 크기만큼 데이터를 읽어야 끝난다. 다시 말해, 청크 크기가 10이면 read()가 10번 호출되어 하나의 청크가 생성된다.
2. 데이터 깎기 (ItemProcessor)
ItemProcessor는 ItemReader가 읽어온 청크의 각 아이템 하나씩을 처리한다.
여기서 확실히 짚고 가야 할 부분: process() 메서드는 청크 전체를 받지 않는다. 청크 안의 각 아이템을 하나씩 받아서 처리한다는 게 포인트다. Spring Batch는 청크 내부의 아이템마다 process()를 반복 호출해서 데이터를 변환하거나 필터링한다.
다시 말해, 청크 크기가 10이면 process()가 10번 호출된다.
3. 데이터 쓰기 (ItemWriter)
ItemWriter는 청크 단위로 데이터를 저장한다. ItemReader / ItemProcessor가 각각의 아이템을 하나씩 처리하는 것과 달리, ItemWriter는 청크 전체를 한 번에 처리한다.
Spring Batch는 청크 내 아이템들을 하나씩 처리(process)한 결과를 하나로 묶어 ItemWriter에게 전달하는데, 이는 write() 메서드의 파라미터 타입이 Chunk<? extends T>인 것만봐도 알 수 있다.

청크 크기가 10인 경우 흐름

자, 이제 청크 크기가 10일 때의 실제 처리 흐름을 살펴보자.
1.
읽기: read()가 10번 호출돼서 10개의 데이터를 가져와 하나의 청크(inputs)를 생성한다.
2.
가공깎기: process()가 10번 호출되며, 청크(inputs)의 각 데이터를 하나씩 처리한다.
3.
쓰기: 처리된 10개의 아이템이 하나의 청크(outputs)로 묶여 write()에 전달된다. ItemWriter의 write() 메서드는 이 청크(outputs)를 한 번에 저장하거나 출력한다.
4.
이 과정이 모든 데이터를 전부 처리할 때까지 반복된다. 우리 강의에서는 이를 청크 단위 반복이라고 부른다.
[시스템 경고]
과거 스프링 배치 공식 문서의 잘못된 다이어그램으로 인해, 아직도 많은 개발자들이 동작을 잘못 알고 있다.
청크 크기만큼 ItemReader.read()가 모두 호출된 후에 그 다음 청크 크기만큼 ItemProcessor.process()가 호출된다는 사실을 반드시 기억하라.