[2회차] SpringBatch 코드 설명 및 아키텍처 알아보기

아래 글은 한국 스프링 사용자 모임(KSUG)에서 진행된 스프링 배치 스터디 내용을 정리한 게시글입니다.
DEVOCEAN에 연재 중인 KIDO님의 글을 참고하여 실습한 내용을 기록했습니다.

 

 

[SpringBatch 연재 02] SpringBatch 코드 설명 및 아키텍처 알아보기

 

devocean.sk.com

원본: [SpringBatch 연재 02] SpringBatch 코드 설명 및 아키텍처 알아보기

 

1. Spring Batch Application 실행

  • 스프링 배치 중 Tasklet이라는 것을 만들고 스프링 배치를 실행해 보자.

이전에 사용하던 스프링 배치 프로젝트를 그대로 이용합니다.

❗️ 들어가기 전 용어 알기 — Spring Batch에서 Tasklet이란?

Tasklet 은 배치 작업에서 단일 단계(Step) 내에서 수행할 수 있는 간단한 작업 단위를 말합니다. 각 Tasklet은 execute() 메서드를 구현하여 배치 처리를 수행하고, 필요에 따라 상태나 결과를 반환할 수 있습니다.

자세한 내용은 아래(3. Spring Batch 아키텍처)에서 다룹니다.

 

실습 순서는 다음과 같습니다.

  1. Tasklet 구현체를 생성한다.
  2. @Configuration을 통해서 생성할 배치 빈을 스프링에 등록한다.
  3. Job, Step을 생성하고 빈에 등록한다.
  4. 실행 결과를 확인한다.

 

실습 들어가기 전, 해당 실습에서는 Lombok 의존성을 사용합니다.

build.gradle 파일에 아래 해당 의존성을 추가해 주세요.

// lombok
compileOnly 'org.projectlombok:lombok:1.18.28'
annotationProcessor 'org.projectlombok:lombok:1.18.28'

 

2. 스프링 배치 실행 해보기

2.1. Tasklet 구현체 생성하기

GreetingTasklet.java 파일을 생성하고 다음과 같이 작성합니다.

@Slf4j
public class GreetingTasklet implements Tasklet, InitializingBean {
    @Override
    public RepeatStatus execute(StepContribution contribution,
                                ChunkContext chunkContext) throws Exception {
        log.info("------------------ Task Execute -----------------");
        log.info("GreetingTask: {}, {}", contribution, chunkContext);

        // StepContribution: 현재 단계의 기여 정보를 나타내며, 배치 처리 결과에 대한 정보를 포함할 수 있습니다.
        // ChunkContext: 실행 중인 청크(Chunk)에 대한 컨텍스트 정보를 제공하며, 현재 배치 작업과 관련된 메타데이터를 담고 있습니다.

        return RepeatStatus.FINISHED;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("----------------- After Properites Sets() --------------");
    }
}

 

Tasklet과 InitializingBean 인터페이스를 구현합니다.

 

Tasklet: Spring Batch의 작업 단위로, execute() 메서드를 구현하여 특정 작업을 실행합니다.

  • execute() 메서드는 StepContributionChunkContext를 파라미터로 받아 배치 작업을 수행하며 최종적으로 RepeatStatus 값을 반환합니다. 이때, 반환 값에 따라 배치 작업의 상태가 결정됩니다.
    • FINISHED: Tasklet이 정상적으로 완료되었음을 나타냅니다.
    • CONTINUABLE: Tasklet이 계속해서 수행되어야 함을 나타냅니다.
    • continueIf(condition): 주어진 조건에 따라 작업을 계속할지, 종료할지를 결정하는 메서드입니다.

 

InitializingBean: Spring의 빈 초기화 인터페이스로, 빈이 모든 의존성이 주입되고 나면 호출되는 afterPropertiesSet() 메서드를 구현합니다.

  • afterPropertiesSet() 메서드는 배치 작업을 수행하기 전에 의존성 설정이 완료된 후 호출되는 메서드입니다. 필수적인 부분은 아니므로 실제 작업에서 생략해도 무방합니다.

 

2.2. @Configuration을 통해서 생성할 배치 빈을 스프링에 등록하기

스프링부트는 @Configuration을 통해서 빈을 등록할 설정을 할 수 있도록 어노테이션을 제공합니다.

bean으로 등록하기 위해 BasicTaskJobConfiguration 클래스를 작성합니다.

 

2.3. Job, Step을 생성하고 빈에 등록하기

생성한 BasicTaskJobConfiguration 클래스에 Job과 Step을 생성합니다.

@Slf4j
@AllArgsConstructor
@Configuration
public class BasicTaskJobConfiguration {

    // 트랜잭션 처리를 관리하는 역할을 한다. 배치 처리 중 실패가 발생하면 트랜잭션 롤백이 가능하다.
    private final PlatformTransactionManager transactionManager;

    @Bean
    public Tasklet greetingTasklet() {
        // 사용자 정의 tasklet 빈으로 등록
        return new GreetingTasklet();
    }

    /**
     * Step 객체를 생성하고 빈으로 등록하는 메서드
     * param: JobRepository, PlatformTransactionManager
     * 스프링 배치는 주로 Datasource 와 함께 작업되므로 transactionManager 클래스를 받는다.
     */
    @Bean
    public Step step(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        log.info("------------------ Init myStep -----------------");

        // StepBuilder 클래스를 이용하여 myStep 이라는 이름으로 Step을 생성한다. jobRepository는 Step의 실행 상태를 기록하고 관리한다.(필수)
        return new StepBuilder("myStep", jobRepository)
                // tasklet을 Step에 추가하고 greetingTasklet(실제로 실행할 작업 정의)을 통해 Step내 tasklet을 주입한다.
                .tasklet(greetingTasklet(), transactionManager)
                // Step을 생성하고 return 한다.
                .build();
    }

    /**
     * Job 객체를 생성하고 빈으로 등록하는 메서드
     * param: Step, JobRepository
     * Job은 Step의 집합이며 JobRepository 역시 필요하다.
     */
    @Bean
    public Job myJob(Step step, JobRepository jobRepository) {
        log.info("------------------ Init myJob -----------------");

        // JobBuilder 클래스를 이용하여 myJob 이라는 이름으로 Job을 생성한다. jobRepository는 job의 실행 상태를 기록하고 관리한다.(필수)
        return new JobBuilder("myJob", jobRepository)
                // incrementer() 메서드는 Job이 지속적으로 실행될때, Job의 유니크성을 구분할 수 있는 방법을 설정한다.
                // RunIdIncrementer는 배치 작업 실행 시마다 고유한 Job 실행 ID를 생성해주는 객체이다.
                .incrementer(new RunIdIncrementer())
                // Job이 실행될 때 첫 번째로 실행될 Step을 정의한다. 처음 시작하는 Step은 파라미터로 받은 step으로 등록했다.
                // 만약 여러 개의 Step이 필요하다면 추가적인 Step들을 next() 메서드를 통해 연결할 수 있다.
                .start(step)
                // Job을 생성하고 return 한다.
                .build();
    }
}

 

2.4. 실행 결과 확인하기

아래 로그를 확인 해보면 afterPropertySet() → Job → Step → Tasklet 순으로 실행됨을 알 수 있습니다.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.4)

 2024-10-11T23:02:10.795+09:00  INFO 9769 --- [           main] c.e.batch_sample.BatchSampleApplication  : Starting BatchSampleApplication using Java 17.0.10 with PID 9769 (/Users/yeseul/Desktop/study/spring/batch-sample/build/classes/java/main started by yeseul in /Users/yeseul/Desktop/study/spring/batch-sample)

... 중략

2024-10-11T23:02:11.414+09:00  INFO 9769 --- [           main] c.e.b.jobs.task01.GreetingTasklet        : ----------------- After Properites Sets() --------------
2024-10-11T23:02:11.452+09:00  INFO 9769 --- [           main] c.e.b.j.t.BasicTaskJobConfiguration      : ------------------ Init myStep -----------------
2024-10-11T23:02:11.463+09:00  INFO 9769 --- [           main] c.e.b.j.t.BasicTaskJobConfiguration      : ------------------ Init myJob -----------------
2024-10-11T23:02:11.654+09:00  INFO 9769 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path '/'
2024-10-11T23:02:11.660+09:00  INFO 9769 --- [           main] c.e.batch_sample.BatchSampleApplication  : Started BatchSampleApplication in 1.038 seconds (process running for 1.283)
2024-10-11T23:02:11.662+09:00  INFO 9769 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: []
2024-10-11T23:02:11.687+09:00  INFO 9769 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob]] launched with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}]
2024-10-11T23:02:11.698+09:00  INFO 9769 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [myStep]
2024-10-11T23:02:11.701+09:00  INFO 9769 --- [           main] c.e.b.jobs.task01.GreetingTasklet        : ------------------ Task Execute -----------------
2024-10-11T23:02:11.701+09:00  INFO 9769 --- [           main] c.e.b.jobs.task01.GreetingTasklet        : GreetingTask: [StepContribution: read=0, written=0, filtered=0, readSkips=0, writeSkips=0, processSkips=0, exitStatus=EXECUTING], ChunkContext: attributes=[], complete=false, stepContext=SynchronizedAttributeAccessor: [], stepExecutionContext={batch.version=5.1.2, batch.taskletType=com.example.batch_sample.jobs.task01.GreetingTasklet, batch.stepType=org.springframework.batch.core.step.tasklet.TaskletStep}, jobExecutionContext={batch.version=5.1.2}, jobParameters={run.id=1}
2024-10-11T23:02:11.705+09:00  INFO 9769 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [myStep] executed in 6ms
2024-10-11T23:02:11.707+09:00  INFO 9769 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob]] completed with the following parameters: [{'run.id':'{value=1, type=class java.lang.Long, identifying=true}'}] and the following status: [COMPLETED] in 14ms
  1. greetingTasklet() 빈 생성
  2. step() 빈 생성 (이 시점에서 greetingTasklet() 사용)
    • Step이 생성되며 로그 출력: "------------------ Init myStep -----------------"
  3. myJob() 빈 생성 (이 시점에서 step() 사용)
    • Job이 생성되며 로그 출력: "------------------ Init myJob -----------------"
  4. 배치 작업이 실행되면 Step이 실행되며 Tasklet을 수행하고, GreetingTask에서 설정된 작업을 실행
    • 로그 출력: "------------------ Task Execute -----------------"

 


 

3. Spring Batch 아키텍처 알아보기

스프링 배치는 스프링 DI와 AOP를 지원하는 배치 프레임워크이며, 배치 작업을 구성하는 방식에는 크게 Tasklet ModelChunk Model 두 가지가 있습니다.

  • Tasklet model
    • 단순한 처리 모델을 가지고 있습니다.
    • 주로 로직이 단순한 경우 사용합니다.
    • 다양한 데이터 소스나 파일을 한 번에 처리해야 할 때, Tasklet은 필요한 로직을 자유롭게 구현할 수 있어 높은 유연성을 제공합니다.
  • Chuck model
    • 대량의 데이터를 효율적으로 처리 가능합니다.
    • 데이터를 한꺼번에 처리하는 것이 아닌 Chunk 단위로 나누어 처리하는 모델로 나누어 읽고 처리한 후에 일정량의 데이터를 기록하는 방식입니다.
    • Reader → Processor → Writer 순서로 처리됩니다.

 

특징 Tasklet Model
사용 목적 단순한 작업
(파일 삭제, 테이블 초기화)
대량 데이터 처리
(파일 읽기, DB 저장 등)
구성 단일 Tasklet ItemReader, ItemProcessor, ItemWriter
재사용성 비교적 낮음 재사용 가능
데이터 처리 방식 한 번에 전체 처리 청크 단위로 나누어 처리
트랜잭션 관리 단일 트랜잭션 청크 단위 트랜잭션 관리

 

3.1. Spring Batch 기본 아키텍처

스프링 배치는 다음과 같은 기본 아키텍처를 가집니다.

 

요소 설명
Job Spring Batch에서 일괄 처리 작업을 정의하는 단일 실행 단위입니다.
여러 Step으로 구성되며, 하나의 Job에 여러 Step을 재사용, 병렬화, 조건 분기 등을 설정할 수 있습니다.
Step Step은 Job을 구성하는 처리 단위로, Tasklet Model 또는 Chunk Model을 사용하여 구현됩니다.
Chunk Model을 사용할 경우, Step은 ItemReader, ItemProcessor, ItemWriter로 구성됩니다.
ItemReader, ItemProcessor, ItemWriter는 각각 독립적으로 사용될 수 있으며,
Step에 따라 조합하여 사용할 수 있습니다.
JobLauncher Job을 수행하기 위한 인터페이스입니다.
사용자가 직접 JobLauncher를 호출하여 배치 작업을 시작할 수 있습니다.
자바 커맨드를 통해 CommandLineJobRunner를 실행하여 배치 프로세스를 수행할 수 있습니다.
ItemReader 청크 단위 모델에서 데이터를 읽어오는 역할을 수행합니다.
파일, 데이터베이스 등 다양한 소스에서 읽어올 수 있습니다.
ItemProcessor 읽어들인 데이터를 처리하는 역할을 담당하며, 데이터 변환, 필터링, 정제 작업을 수행할 수 있습니다.
선택적 요소이므로, 필요하지 않으면 생략할 수 있습니다.
ItemWriter 읽거나 처리된 데이터를 출력하는 역할을 담당합니다.
데이터베이스에 데이터를 저장하거나 파일로 결과를 출력할 수 있습니다.
JobRepository Job과 Step의 상태를 관리하는 시스템입니다.
Spring Batch에서 제공하는 테이블 스키마를 기반으로 Job의 실행 상태 및 메타데이터를 저장하고 관리합니다.

 

3.2. Spring Batch 흐름

스프링 배치의 기본 흐름을 이해 하는 것은 매우 중요합니다.

 

3.2.1. 주요 처리 흐름(검은색 선)

JobLauncher → Job → Step → ItemReader → ItemProcessor → ItemWriter 순으로 진행되며, 각 단계는 ExecutionContext를 통해 실행 상태를 관리합니다.

  1. Job Scheduler에서 배치를 시작하며 JobLauncher를 실행합니다.
  2. JobLauncher에서 Job이 실행됩니다.
    • 이때 Execution Context 정보를 이용하여 JobExecution을 수행합니다.
  3. Job에서 Step이 실행됩니다.
    • 이때 Execution Context 정보를 이용하여 StepExecution을 수행합니다.
    • Step은 Tasklet과 Chuck 모델이 존재하지만, 해당 그림에서는 Chunk 모델을 나타내고 있습니다z.
  4. ItemReader가 입력 데이터를 읽어옵니다.
  5. ItemProcessor가 데이터를 가공하거나 변환합니다. 사용하여 입력 데이터를 Chunk 단위로 처리하게 됩니다.
  6. ItemWriter는 가공된 데이터를 출력합니다. 다양한 Writer을 통해 DB에 저장하거나 파일에 쓸 수도 있습니다.

 

3.2.2. Job 정보 흐름(빨간색 선)

JobLauncher는 JobRepository와 상호작용하여 JobInstance와 JobExecution 정보를 데이터베이스에 기록하고, 처리 상태를 추적합니다.

  1. JobLauncher는 JobRepository를 통해 JobInstance를 생성하고, 실행 정보를 데이터베이스에 기록합니다.
    • JobInstanace란 특정 Job의 실행 단위를 의미하며 Job과 JobParameters의 조합에 의해 결정됩니다.
  2. JobLauncher는 JobRepository를 통해 JobExecution을 데이터베이스에 저장하여 Job이 시작되었음을 기록합니다.
    • JobExecution은 해당 Job의 실행 상태를 추적하는 데 사용됩니다.
  3. Step이 실행되면서 StepExecution 정보를 JobRepository를 이용하여 데이터베이스에 업데이트합니다.
    • StepExecution에는 Step의 처리 상태, 입출력 레코드 수, 오류 발생 여부 등의 다양한 정보가 포함됩니다.
  4. Job이 완료되면 JobLauncher는 JobRepository를 통해 JobExecution을 완료 상태로 업데이트하고 DB에 기록합니다.
    • Job의 성공/실패 등 관련된 메타데이터도 함께 기록됩니다.

 

3.2.3. Spring Batch 저장 정보

흐름 설명에서 사용했던 용어들을 아래에서 다시 정리해보겠습니다.

 

컴포넌트 역할
JobInstance Spring Batch에서 Job의 "논리적" 실행을 나타냅니다.

JobInstance는 Job 이름과 전달 파라미터(JobParameter)로 식별됩니다.

Job이 재실행을 지원하고, 실행 중에 오류로 인해 중단된 경우 중단된 부분부터 다시 실행됩니다.
그러나 재실행을 지원하지 않거나 이미 성공적으로 처리된 JobInstance를 재실행 할 경우 예외가 발생하며,
Java 프로세스가 비정상적으로 종료됩니다.

예를 들어, 이미 완료된 작업을 재실행하려 할 때 JobInstanceAlreadyCompleteException이 발생합니다.
JobExecution JobExecution은 Job의 "물리적" 실행을 나타냅니다.

JobInstance와 달리, 동일한 Job을 재실행할 때도 다른 JobExecution으로 간주됩니다.
따라서 JobInstance와 JobExecution은 일대다 관계를 가집니다.

ExecutionContext는 동일한 JobExecution 내에서 처리 단계와 같은 메타데이터를 공유하는 영역입니다.
ExecutionContext는 주로 Spring Batch가 상태를 기록하는 데 사용되지만, 애플리케이션에서 ExecutionContext에 접근하는 방법도 제공됩니다.
JobExecutionContext에 저장되는 객체는 java.io.Serializable을 구현하는 클래스여야 합니다.
StepExecution StepExecution은 Step의 "물리적" 실행을 나타냅니다.
Job은 Step의 집합이기 때문에 일대다 관계를 가집니다.

ExecutionContext는 JobExecution과 마찬가지로 Step 내에서 데이터를 공유하는 영역입니다.
여러 Step 간에 공유할 필요가 없는 데이터는 JobExecutionContext가 아닌 대상 Step의 ExecutionContext를 사용하는 것이 좋습니다.
StepExecutionContext에 저장되는 객체는 반드시 java.io.Serializable을 구현하는 클래스여야 합니다.
JobRepository JobExecution 또는 StepExecution 등과 같은 배치 실행 결과나 상태를 관리하고 영구적으로 저장하는 기능을 제공합니다.
데이터를 저장할 장소가 필요하며, 휘발성 메모리뿐만 아니라 데이터베이스와 같은 영구 저장소를 사용할 수 있습니다.
이렇게 저장된 정보를 활용하여 배치 Job이 종료되거나 Java 프로세스가 재시작되더라도 이전의 실행 정보를 참조할 수 있게 해줍니다.

 

정리

  • 스프링 배치의 구조에 대해 알아보았습니다.
  • 스프링 배치에는 Tasklet 모델과 Chunk 모델이 존재합니다.
  • 스프링 배치의 처리 흐름과 데이터 흐름에 대해서도 알아보았습니다.

참고