📚FOS Study
홈카테고리
홈카테고리
📚FOS Study

개발 학습 기록을 정리하는 블로그입니다.

바로가기

  • 홈
  • 카테고리

소셜

  • GitHub
  • Source Repository

© 2025 FOS Study. Built with Next.js & Tailwind CSS

목록으로 돌아가기
☕java/ spring-framework

1.1-type-of-steps

약 11분
GitHub에서 보기

배치 처리의 두 얼굴 : Tasklet과 Chunk

1장은 Spring Batch Step의 두 가지 유형을 소개하면서 시작하려고 한다.


0장에서 배치 처리의 일반적인 패턴이 읽고-처리하고-쓰기라고 말했다.

하지만 모든 배치가 꼭 데이터를 이렇게 다루는 건 아니다.

이 차이가 바로 Step의 유형을 나누는 기준이 된다.


Spring Batch의 Step은 다음과 같이 크게 두 가지 처리 모델로 나뉜다

  • 태스크릿 지향 처리 (Tasklet Oriented Processing)
  • 청크 지향 처리 (Chunk Oriented Processing)

먼저, 태스크릿(Tasklet) 지향 처리부터 살펴본다.

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

태스크릿 지향 처리 모델은 Spring Batch에서 가장 기본적인 Step 구현 방식으로

비교적 복잡하지 않은 단순한 작업을 실행할 떄 사용된다.


단순한 작업이라는 말이 바로 와닿지 않을 수 있다.

언제 태스크릿 지향 처리가 사용되는지를 먼저 알면 이해가 훨씬 쉬워진다.


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


일반적인 Spring Batch의 Step은 대부분 대량 데이터를 처리 (읽고-처리하고-쓰기) 하는 ETL 작업에 초점을 맞춘다.

하지만 때로는 단순한 시스템 작업이나 유틸성 작업이 필요할 떄가 있다. 예를 들어,

  • 매일 새벽 불필요한 로그 파일 삭제
  • 사용자에게 단순한 알림 메시지 또는 이메일 발송
  • 외부 API 호출 후 결과를 단순히 저장하거나 로깅

이처럼 단일 비즈니스 로직 실행에 초점을 맞춘 작업들이 있다.

태스크릿 지향 처리는 바로 이러한 목적을 위해 설계된 처리 방식이다.


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

앞서 살펴본 사례들은 대부분 함수 호출 하나로 끝날 법한 단순한 작업들이다.

조금 더 기술적으로 말하자면, Spring Batch가 제공하는 Tasklet 인터페이스의 execute() 메서드에 우리가 원하는 로직을 구현하고,

이 구현체를 Spring Batch에 넘기기만 하면 된다.

그 이후의 실행과 흐름 관리는 Spring Batch가 알아서 처리한다.


@FunctionalInterface
public interface Tasklet {
  @Nullable
  RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception;
}


그럼 바로 Tasklet 구현 예제를 살펴보자.

Tasklet 구현 예시 : 좀비 프로세스 처형 작전


@Slf4j
public class ZombieProcessCleanupTasklet implements Tasklet {
  private final int processesToKill = 10;
  private int killedProcesses = 0;

  @Override
  public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    killedProcesses++;
    log.info("☠️  프로세스 강제 종료... ({}/{})", killedProcesses, processesToKill);

    if (killedProcesses >= processesToKill) {
        log.info("💀 시스템 안정화 완료. 모든 좀비 프로세스 제거.");
        return RepeatStatus.FINISHED;  // 모든 프로세스 종료 후 작업 완료
    }

    return RepeatStatus.CONTINUABLE;  // 아직 더 종료할 프로세스가 남아있음
  }
}

좀비 프로세스를 반복적으로 처형하는 작업을 Tasklet으로 구현한 예제다.

execute() 메서드 내에서 프로세스를 하나씩 종료하며, 목표 수치에 도달하면 작업이 완료된다.


여기 RepeatStatus라는 미묘한 반환값이 눈에 띌 것이다.

앞선 설명에서 "좀비 프로세스를 반복적으로 처형한다"라고 했던 걸 기억하는가?

하지만 그 어떤 반복문도 보이지 않는다.

대신, 예제에서는 RepeatStatus를 사용해 반복 여부를 결정하고 있다.


그렇다. Spring Batch Step이 Tasklet의 execute() 메서드 실행을 계속 할지 멈출지를 결정하는 기준이 바로 이 RepeatStatus다.

execute() 메서드에서 적절한 RepeatStatus를 반환해, Spring Batch에게 반복 여부를 알리는 방식이다.

  • RepeatStatus.FINISHED
    • Step의 처리가 성공이든 실패든 상관 없이 해당 Step이 완료되었음을 의미
    • 더 이상 반복할 필요 없이 다음 스텝으로 넘어감
  • ReatStatus.CONTINUABLE
    • Tasklet의 execute() 메서드가 추가로 더 실행되어야 함을 Step에 알리는 신호.
    • Step의 종료는 보류되고, 필요한 만큼 execute() 메서드가 반복 호출

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


"반복 작업이라면 while문으로 처리하면 되는 거 아닌가?"

많은 개발자가 처음 Tasklet을 접할 떄 이렇게 생각한다.

그러나 Spring Batch에서는 반복문 이상의 제어 구조가 필요하다.


Spring Batch는 Tasklet의 execute() 호출 마다 새로운 트랜잭션을 시작하고

execute()의 실행이 끝나 RepeatStatus가 반환되면 해당 트랜잭션을 커밋한다.


execute() 메서드 내부에 반복문을 직접 구현했다고 가정해보자.

이 경우 모든 반복 작업이 하나의 트랜잭션 안에서 실행된다.

만약 실행 도중 예외가 발생하면, 데이터베이스 결과가 execute() 호출 전으로 롤백되어 버린다.


예를 들어, 오래된 주문 데이터를 정리하는 배치 작업을 생각해보자.

한 번에 만 건씩 데이터를 삭제하는데, 총 100만 건의 데이터를 처리해야 한다고 하자.

  • execute() 내부에서 while 문을 사용한다면 :
    • 80만 건쨰 처리 중 예외가 발생했을 때,
    • 이미 처리한 79만 건의 데이터도 모두 롤백되어 하나도 정리되지 않은 상태로 돌아간다
  • RepeatStatus.CONTINUABLE로 반복한다면 :
    • 매 만 건 처리마다 트랜잭션이 커밋되므로,
    • 예외가 발생하더라도 79만 건의 데이터는 이미 안전하게 정리된 상태로 남는다

결국, RepeatStatus를 반환해 execute()를 반복 실행하도록 하는 이유는

거대한 하나의 트랜잭션 대신 작은 트랜잭션들로 나누어 안전하게 처리하기 위해서다.


Tasklet을 Step으로 등록하는 방법


다음은 우리가 만든 ZombieProcessCleanupTasklet을 Spring Batch Step에 등록하는 배치 설정 예시다.

@Configuration
public class ZombieBatchConfig {
  // other configs..

  @Bean
    public Step zombieCleanupStep() {
        return new StepBuilder("zombieCleanupStep", jobRepository)
                // Tasklet과 transactionManager 설정
                .tasklet(zombieProcessCleanupTasklet(), transactionManager)
                .build();
    }
}

Step을 생성할 때 가장 중요한 것은 "어떤 처리 방식을 사용할 것인가"를 결정하는 것이다.

예제와 같이 StepBuilder의 tasklet() 메서드를 호출하면, 스텝 빌더는 태스크릿 지향 처리 방식의 Step을 생성한다.

tasklet() 메서드에는 우리가 작성한 Tasklet 구현체인 ZombieProcessCleanupTasklet를 전달한다.

이렇게 전달된 ZombieProcessCleanupTasklet의 execute() 메서드가 실제 Step의 작업 내용이 된다.


Tasklet 구현체 외에도 PlatformTransactionManager 인스턴스가 함께 전달되는 것이 보인다.

이는 앞서 RepeatStatus 설명에서 언급했던 것처럼, Tasklet의 execute() 메서드 실행 중 발생하는 모든 데이터베이스 작업을 하나의 트랜잭션으로 관리하기 위한 것이다.


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


  • 단순 작업에 적합
    • 태스크릿 지향 처리는 알림 발송, 파일 복사, 오래된 데이터 삭제 등 단순 작업을 처리하는 Step 유형이다.
  • Tasklet 인터페이스 구현
    • Tasklet 인터페이스를 구현해 필요한 로직을 작성한 뒤, 이를 StepBuilder.tasklet() 메서드에 전달해 Step을 구성한다.
  • RepeatStatus로 실행 제어
    • Tasklet.execute() 메서드는 RepeatStatus를 반환하며, 이를 통해 실행 반복 여부를 결정할 수 있다.
  • 트랜잭션 지원
    • Spring Batch는 Tasklet.execute() 메서드 실행 전후로 트랜잭션을 시작하고 커밋하여, 데이터베이스의 일관성과 원자성을 보장한다.

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

Spring Batch를 접하며 다룰 대부분의 배치 작업, 특히 데이터를 다루는 작업은 읽기 -> 처리 -> 쓰기라는 공통된 패턴을 보인다.

Spring Batch도 데이터를 다룰 때 이 패턴을 따른다.

그리고 이 방식을 Spring Batch에서는 청크 지향 처리라고 부른다.


그런데, 왜 이름에 청크(Chunk)라는 단어가 붙었을까?

이 의문을 시작으로, 청크 지향 처리의 세계로 들어가 보자.


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


청크(Chunk)는 데이터를 일정 단위로 쪼갠 덩어리를 말한다.

Spring Batch에서 데이터 기반 처리 방식을 청크 지향 처리라고 부르는 이유는,

읽고, 처리하고, 쓰는 작업을 일정 크기로 나눈 데이터 덩어리(청크)를 대상으로 하기 떄문이다.


백만 건의 데이터를 처리해야 한다고 가정해보자.

Spring Batch는 100만 건 전체를 한 번에 읽고 처리하고 쓰지 않는다.

대신, 100개씩 쪼개서 읽고, 처리하고 저장한다.

이렇게 나뉜 100개의 묶음이 바로 청크다.


왜 Spring Batch는 이런 방법을 택했을까?


100만 건의 데이터를 한 번에 메모리로 불러오고, 처리하고 저장한다면?

메모리는 터지고, DB는 과부하로 비명을 지른다.

Spring Batch는 이런 참사를 피하기 위해 청크라는 무기를 꺼내 들었다.

  • 메모리를 지켜라 - 데이터 폭탄 방지
    • 100개씩 나눠서 불러온다.
    • 개념적으로, 메모리엔 단 100개의 데이터만 존재한다.
  • 가벼운 트랜잭션 - 작은 실패
    • 트랜잭션은 작업의 성공 또는 실패를 하나의 단위로 묶는 것이라고 볼 수 있다.
    • 하지만, 100만 건을 하나의 트랜잭션으로 처리한다면?
    • 작업 중간에 오류가 발생하면, 100만 건이 전부 롤백된다.

청크 지향 처리는 이를 방지한다.

트랜잭션이 청크 단위로 분리되기 때문이다.


만약 작업 중간에 에러가 발생하면?

  • 이전 청크는 이미 커밋 완료
  • 에러가 발생한 청크만 롤백되고 해당 청크부터 재시작하면 된다.

그렇다면 이 패턴이 Spring Batch에서 어떻게 구체화될까?

Spring Batch는 이를 세 가지 컴포넌트로 구현한다.

바로 ItemReader, ItemProcessor, ItemWriter


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

읽는다(ItemReader), 깎는다(ItemProcessor), 쓴다(ItemWriter)

이 배치 3대장으로 청크 지향 처리가 완료된다.


이제 배치 3대장이 각각 어떤 역할을 하는지 가볍게 살펴보자.


ItemReader - 데이터를 끌어오는 수혈관


데이터를 읽어오는 것은 청크 지향 처리의 생명줄이다.

ItemReader는 데이터라는 피를 시스템으로 수혈한다.


public interface ItemReader<T> {
  T read() throws Exception
}

  • 한 번에 하나씩, 차례차례:
    • read() 메서드의 반환 타입을 주목하라.
    • read()  메서드는 아이템을 하나씩 반환한다
    • 여기서 아이템이란 파일의 한 줄 또는 데이터베이스의 한 행(row)에 해당하는 데이터 하나를 의미한다.
    • 읽을 데이터가 더 이상 없으면 null을 반환하며, 스텝은 종료된다.
    • ItemReader가 null을 반환하는 것이 청크 지향 처리 Step의 종료 시점이라는 점을 반드시 기억하라
    • 이는 Spring Batch가 Step의 완료를 판단하는 핵심조건이다.
  • 다양한 구현체 제공:
    • Spring Batch는 파일, 데이터베이스, 메시지 큐 등 다양한 데이터 소스에 대한 표준 구현체를 제공한다.
    • 예를 들어, FlatFileItemReader는 CSV나 텍스트 파일에서 데이터를 읽어온다.

ItemProcessor - 아이템 깎기


ItemProcessor는 데이터를 원하는 형태로 깎아내는 가공 담당자다.

ItemReader가 넘긴 재료를 받아다가, 필요한 모양으로 깎아낸다.


public interface ItemProcessor<I, O> {
  @Nullable O process(I item) throws Exception;
}

  • 데이터 가공:
    • 입력 데이터(I)를 원하는 형태(O)로 변환한다.
    • 예를 들어, 읽어온 원본 데이터를 비즈니스 로직에 맞게 가공하거나, 출력 시스템이 요구하는 형식으로 변환한다.
  • 필터링:
    • process() 메서드가 null을 반환하면 해당 입력 데이터는 처리 흐름에서 제외된다.
    • 다시 말해, ItemWriter로 전달되지 않는다.
    • 유효하지 않은 데이터나 처리할 필요가 없는 데이터를 걸러낼 떄 사용한다.
  • 데이터 검증:
    • 입력 데이터의 유효성을 검사한다.
    • 필터링과 달리 조건에 맞지 않는 데이터를 만나면 예외를 발생시킨다.
    • 예를 들어, 필수 필드 누락이나 잘못된 데이터 형식을 발견했을 때 예외를 던져 배치 잡을 중단시킨다.
    • Spring Batch Skip 기능과 함께 사용하면 예외가 발생한 데이터만 건너뛰고 배치 작업을 계속진행 할 수 있다
  • 필수 아님:
    • ItemProcessor는 생략 가능하다.
    • 다시 말해 Step이 데이터를 읽고 바로 쓰도록 구성할 수 있다.

ItemWriter - 데이터를 저장하는 최종 집행자


ItemWriter는 ItemProcessor가 만든 결과물을 받아, 원하는 방식으로 최종 저장/출력한다.


public interface ItemWriter<T> {
  void write(Chunk<? extends T> chunk) throws Exception;
}

  • 한 덩어리씩 쓴다:
    • ItemWriter는 데이터를 한 건씩 쓰지 않는다.
    • Chunk 단위로 묶어서 한 번에 데이터를 쓴다
    • write() 메서드의 파라미터 타입이 Chunk 인 것에 주목하자.
  • 다양한 구현체 제공:
    • Spring Batch는 파일, 데이터베이스, 외부 시스템 전송 등에 사용할 수 있는 다양한 구현체를 제공한다.
    • 예를 들어, FlatFileItemWriter는 파일에 데이터를 기록한다.

위와 같이 읽고-처리하고-쓰기를 분리하면 어떤 장점이 있는가?


  • 완벽한 책임 분리
    • 각 컴포넌트는 자신의 역할만 수행한다.
    • ItemReader는 읽기, ItemProcessor는 가공, ItemWriter는 쓰기에만 집중한다.
    • 덕분에 코드는 명확해지고 유지보수는 간단해진다
  • 재사용성 극대화
    • 컴포넌트들은 독립적으로 설계되어 있어 어디서든 재사용 가능하다.
    • 새로운 배치를 만들 떄도 기존 컴포넌트들을 조합해서 빠르게 구성할 수 있다.
  • 높은 유연성
    • 요구사항이 변경되어도 해당 컴포넌트만 수정하면 된다.
    • 데이터 형식이 바뀌면 ItemProcessor만, 데이터 소스가 바뀌면 ItemReader만 수정하면 된다.
    • 이런 독립성 때문에 변경에 강하다.

청크 지향 처리 조립하기

자, 이제 3대장을 Step에 조립할 시간이다.

여기서는 Step을 구성하는 기본 방법에만 집중하고, ItemReader, ItemProcessor, ItemWriter의 실제 구현체는 생략하자.


@Bean
public Step processStep() {
  return new StepBuilder("processStep", jobRepository)
    .<CustomerDetail, CustomerSummary>chunk(10, transactionManager) // 청크 지향 처리 활성화
    .reader(itemReader())
    .processor(itemProcessor())
    .writer(itemWriter())
    .build();
}

청크 지향 처리 Step 구성의 핵심은 StepBuilder의 chunk() 메서드를 호출하는 것으로 시작된다.

이 메서드 호출을 통해 우리의 Step이 청크 지향 처리 모델로 동작하게 된다.

구성의 핵심 요소를 하나씩 살펴보자.


1) 청크 사이즈 지정


.chunk(10, transactionManager)

chunk() 메서드의 첫 번쨰 파라미터로 청크의 크기를 지정한다.

여기서는 10을 지정했는데, 이는 데이터를 10개씩 묶어서 처리하겠다는 의미다.

ItemReader가 데이터 10개를 읽어오면, 이를 하나의 청크로 만들어 ItemProcessor가 처리하고 ItemWriter에서 쓰기를 수행한다.


2) 제네릭 타입으로 데이터 흐름 정의


.<CustomerDetail, CustomerSummary>chunk(..)

chunk() 메서드의 제네릭 타입을 지정해 청크 처리 과정에서의 데이터 타입 변환 흐름을 정의한다.

  • 첫 번째 타입(CustomerDetail) : ItemReader가 반환할 타입.
    • 예를 들어 파일에서 읽은 데이터를 CustomerDetail 객체로 변환하여 반환한다.
  • 두 번째 타입(CustomerSummary) : CustomerDetail을 입력 받은 ItemProcessor가  아이템을 처리 후 반환할 타입이자, ItemWriter가 전달받은 Chunk의 제네릭 타입이다.

청크 지향 처리의 흐름


Spring Batch의 청크 지향 처리는 읽기-깎기-쓰기를 청크 단위로 묶어서 반복한다는게 전부다.


1. 데이터 읽기 (ItemReader)

ItemReader는 데이터 소스에서 하나씩 데이터를 읽어온다.

read() 메서드가 호출될 때마다 데이터를 순차적으로 반환하며, 청크 크기만큼 데이터를 읽어야 끝난다.

다시 말해, 청크 크기가 10이면 read()가 10번 호출되어 하나의 청크가 생성된다.


2. 데이터 깎기 (ItemProcessor)

ItemProcessor는 ItemReader가 읽어온 청크의 각 아이템 하나씩을 처리한다.

process() 메서드는 청크 전체를 입력받지 않는다.

청크의 각 아이템을 하나씩 받아서 처리한다는게 포인트다.

Spring Batch는 청크의 아이템마다 process()를 반복 호출해서 데이터를 변환하거나 필터링한다.

다시 말해, 청크 크기가 10이면 process()가 10번 호출된다


3. 데이터 쓰기 (ItemWriter)

ItemProcessor의 처리까지 완료되었다면 이제 청크를 실제로 쓸 차례다.

ItemWriter는 청크 전체를 한 번에 입력 받는다.


다시 말해, 청크 단위로 데이터를 저장한다

이는 write() 메서드의 파라미터 타입이 Chunk<? extends T> 인 것만봐도 알 수 있다.



끝은 어디인가? - '청크 단위 반복'의 종료 조건


답은 의외로 간단하다. 더 이상 읽을 데이터가 없을 떄가 모든 데이터를 처리한 떄다.

바로 ItemReader의 read() 메서드가 null을 반환할 때다.

이것이 Spring Batch가 모든 데이터를 다 읽었다고 인식하는 신호다.



청크 처리와 트랜잭션


데이터를 청크 단위로 처리한다는 건 트랜잭션도 청크 단위로 관리된다는 의미다.

즉, 각각의 청크 단위 반복마다 별도의 트랜잭션이 시작되고 커밋된다.


청크 단위 트랜잭션의 의미


대용량 데이터를 처리하는 도중 중간에 스텝이 실패하더라도, 이전 청크 반복에서 처리된 데이터는 이미 안전하게 커밋되어 있어 그 만큼의 작업은 보존된다.

실패한 청크의 데이터만 롤백되므로, 전체 데이터를 처음부터 다시 처리할 필요가 없다.


청크 단위로 트랜잭션이 관리된다는 걸 알았으니, 자연스럽게 다음 의문이 들 수 있다.

그럼 청크 사이즈는 얼마로 하는게 좋을까?


적절한 청크 사이즈란?


결론부터 말하자면, 이건 답이 없다.

다음의 두 가지 트레이드오프와 비즈니스 요구사항, 그리고 처리할 데이터의 양을 고려해서 적절히 선택해야 한다.


  • 청크 사이즈가 클 떄
    • 그만큼 메모리에 많은 데이터를 한 번에 로드하게 된다
    • 트랜잭션의 경계가 커지므로, 문제 발생시 롤백되는 데이터의 양도 많아진다
  • 처읔 사이즈가 작을 떄
    • 트랜잭션의 경계가 작아져서 문제 발생시 롤백되는 데이터가 최소화된다
    • 대신 그만큼 읽기/쓰기/ I/O가 자주 발생하게 된다.

지금까지 우리는 Spring Batch의 두 얼굴,    Tasklet과 Chunk를 해부했다.

그러나 다음 장에서 본격적으로 Spring Batch의 핵심을 파헤지려면, 기본기를 하나 더 다져놓아야 한다.


다음 절에서는 JobParameters를 사용해 Spring Batch를 부려먹는 법을 가르쳐주마.

java 카테고리의 다른 글 보기수정 제안하기
목차
  • 배치 처리의 두 얼굴 : Tasklet과 Chunk
  • 태스크릿 (Tasklet) 지향 처리란?
  • 언제 태스크릿 지향 처리를 사용하는가?
  • 태스크릿 지향 처리의 구현 방식
  • Tasklet 구현 예시 : 좀비 프로세스 처형 작전
  • 좀비 프로세스를 반복적으로 처형하는 작업을 Tasklet으로 구현한 예제다.
  • RepeatStatus가 필요한 이유 : 짧은 트랜잭션을 활용한 안전한 배치 처리
  • Tasklet을 Step으로 등록하는 방법
  • 태스크릿(Tasklet) 지향 처리 - 한눈에 정리하기
  • Step의 또 다른 얼굴 - 청크 지향 처리
  • Chunk - 데이터를 작은 덩어리로 나누어 처리하는 방식
  • 왜 Spring Batch는 이런 방법을 택했을까?
  • 청크 지향 처리의 3대장 - 읽고, 깎고, 쓴다
  • ItemReader - 데이터를 끌어오는 수혈관
  • ItemProcessor - 아이템 깎기
  • ItemWriter - 데이터를 저장하는 최종 집행자
  • 위와 같이 읽고-처리하고-쓰기를 분리하면 어떤 장점이 있는가?
  • 청크 지향 처리 조립하기
  • 1) 청크 사이즈 지정
  • 2) 제네릭 타입으로 데이터 흐름 정의
  • 청크 지향 처리의 흐름
  • 1\. 데이터 읽기 \(ItemReader\)
  • 2\. 데이터 깎기 \(ItemProcessor\)
  • 3\. 데이터 쓰기 \(ItemWriter\)
  • 끝은 어디인가? - '청크 단위 반복'의 종료 조건
  • 청크 처리와 트랜잭션
  • 청크 단위 트랜잭션의 의미
  • 적절한 청크 사이즈란?