스프링 배치 익혀보기 : 개념 정리 + 간단한 실습
# Back-End/Spring

스프링 배치 익혀보기 : 개념 정리 + 간단한 실습

 

주제

  • 배치의 개념과 구성 요소에 대해 알아본다.
  • 하나의 Job을 여러 Step으로 구성하는 방법을 알아본다.
  • Tasklet, Chunk 단위로 처리해본다.

 


 

1. Spring Batch란

1.1 정의

1.1.1 Batch 작업이란?

대용량 데이터를 처리하려면 많은 시간과 자원이 소요될 수 있고, 동일한 데이터를 사용하는 다른 서비스에 영향을 줄 수 있다. 때문에 이를 실시간이 아닌 한번에 많은 데이터를 처리할 때가 있다.

실시간이 아닌 일괄적으로 모아서 처리하는 작업을 배치 작업이라고 한다.

 

 

1.1.2 Spring Batch란?

  • 엔터프라이즈 시스템의 일상적인 작업에 필수적인 강력한 배치 응용 프로그램을 개발할 수 있도록 설계된 가볍고 포괄적인 배치 프레임워크
  •  로깅/추적, 트랜잭션 관리, 작업 처리 통계, 작업 재시작, 건너뛰기, 리소스 관리 등 대용량 레코드 처리에 필수적인 재사용 가능한 기능을 제공
  • 최적화 및 파티셔닝 기술을 통해 매우 높은 볼륨 및 고성능 배치 작업을 가능하게 하는 고급 기술 서비스 및 기능을 제공
  • 단순하고 복잡한 대용량 배치 작업은 확장성이 뛰어난 방식으로 프레임워크를 활용하여 상당한 양의 정보를 처리할 수 있음

(spring.io 제공)

 

 

1.2 배치 구성도

1.2.1 Spring Batch 구성도

  • 배치는 하나 또는 여러 개의 Job으로 구성될 수 있다.
  • 하나의 Job은  하나 또는 여러 개의 Step을 갖고 있다.
  • 각 Step은 ItemReader, ItemProcessor, ItemWriter를 갖는다. (ItemProcessor는 생략될 수 있음)
  • Job은 JobLauncher가 실행하며, 현재 실행 중인 프로세스의 메타정보는 JobRepository에 저장된다.

 

 

1.2.2 Spring Batch 구성 요소

Job

  • 배치 처리 과정을 하나의 단위로 만들어 표현한 객체

 

JobRepository

  • 배치 수행과 관련된 수치 데이터와 잡의 상태를 유지 관리
  • 스프링 배치 내의 대부분의 중요 컴포넌트가 공유함
  • 실행된 Step, 현재 상태, 읽은 아이템 및 처리된 아이템 수 등이 모두 저장됨

 

JobLauncher

  • Job을 실행하는 역할
  • Job의 재실행 가능 여부 검증, 실행 방법, 파라미터 유효성 검증 등을 수행함
  • 직접 다룰일이 거의 없음
  • 각 Step이 실행되면 JobRepository는 현재 상태로 갱신

 

Step

  • Job을 구성하는 독립된 단위
  • Tasklet 혹은 Chunk 기반으로 처리할 수 있음
  • Job의 상태를 나타내는 단위

 

Tasklet

  • Step이 중단될 때까지 execute 메서드가 계속 반복해서 수행하고, 수행할 때마다 독립적인 트랜잭션을 획득
  • 초기화, Stored Procedure 실행, 알림 전송과 같은 용도에 Tasklet 방식이 사용될 수 있다.

 

Chunk

  • ItemReader에서 데이터를 읽어 Chunk라는 덩어리를 만든 뒤 Chunk 단위로 트랜잭션을 다룸
  • 많은 블로그에서 한 번에 한개씩 데이터를 읽어야한다고 가이드를 하고있지만, 
    Chunk를 만들기 위해 한 번에 한개씩 읽는다면 I/O 작업의 오버헤드가 많이 발생할 수 있다.
    Chunk 크기가 10,000이라면 한 번에 10,000개의 아이템을 일겅서 메모리에 적재한 후 일괄 처리한다면 Chunk를 바로 만들어 처리할 수 있기 때문에 성능상 이점을 취할 수 있다.
  • 백엔드 I/O 작업 횟수, 처리량을 고려하여 Chunk 크기를 적절하게 설정해야 한다.

 

Spring Batch에서는 주로 두 가지 방식으로 배치 작업을 구현합니다: Tasklet과 Chunk 방식입니다. 각각의 방식은 특정한 상황과 요구사항에 맞게 사용됩니다.

1. **Tasklet 방식:**
- Tasklet은 간단한 단위의 작업을 처리하는 방식입니다. Spring Batch에서는 `Tasklet` 인터페이스를 구현하거나 `MethodInvokingTaskletAdapter`를 사용하여 Tasklet을 구현할 수 있습니다.
- 주로 한 번의 실행으로 완료될 작업에 사용됩니다. 예를 들어, 파일을 읽어 데이터를 가공하고 처리하는 등의 작업에 적합합니다.
- Tasklet은 스텝의 시작과 끝에서 실행되며, 성공 또는 실패 여부에 따라 다음 스텝으로 이동합니다.
- 사용자 정의한 비즈니스 로직을 직접 구현하고자 할 때 유용합니다.

2. **Chunk 방식:**
- Chunk 방식은 데이터를 묶어서 일괄 처리하는 방식으로, 대용량 데이터 처리에 적합합니다.
- 스텝 내에서 읽기, 가공/처리, 쓰기 작업을 트랜잭션 단위로 묶어서 처리합니다.
- 하나의 트랜잭션 내에서 여러 아이템을 읽어오고, 한 번에 처리하며, 그 후에 한 번의 트랜잭션으로 한 번에 여러 아이템을 쓸 수 있습니다.
- Chunk 방식은 대용량 데이터를 효율적으로 처리할 때 유용하며, 처리량을 높이는 데 도움이 됩니다.
- 병렬 처리도 지원하므로 높은 처리량과 성능을 얻을 수 있습니다.

적절한 용도:
- **Tasklet 방식**: 단순한 작업 또는 비즈니스 로직을 특정한 순서로 처리해야 할 때 사용됩니다. 예를 들어, 특정 파일을 읽어서 데이터베이스에 저장하는 작업이나, API 호출을 통해 데이터를 가져와서 가공하는 작업 등이 포함됩니다.
- **Chunk 방식**: 대용량 데이터를 처리해야 할 때, 데이터를 묶어서 일괄 처리하고 성능을 향상시키고 싶을 때 사용됩니다. 데이터베이스에서 대량의 레코드를 읽어와 가공한 후 다시 데이터베이스에 저장하는 등의 작업에 많이 사용됩니다.

물론, 상황에 따라 Tasklet과 Chunk 방식을 조합하여 복잡한 배치 프로세스를 구성할 수도 있습니다.
어떤 방식을 선택하느냐에 따라 성능과 유지 보수성 등이 크게 영향을 받을 수 있으므로, 프로젝트의 요구사항과 목표에 맞게 선택하는 것이 중요합니다.

출처 : ChatGpt

 

 

1.3 Spring Batch 메타 데이터

  • 스프링 배치에서는 각 작업이 실행될 때마다 Job 또는 Step에 대한 상태와 이력을 기록한다.
  • 스프링 배치 라이브러리에서 DDL과 옵션을 통해 자동 생성 기능을 제공함
  • 총 6개의 테이블로 이루어져 있으며 데이터가 적재되는 순서는 다음과 같다
    테이블명 용도
    BATCH_JOB_INSTANCE Job의 생성 정보
    BATCH_JOB_EXECUTION Job의 실행 정보
    (Instance 하나에 여러 번의 Job Execution 발생 가능)
    BATCH_JOB_EXECUTION_PARAM Job에 사용되는 파라미터 값
    BATCH_JOB_EXECUTION_CONTEXT Job에 사용되는 모든 정보가 기록되는 Context 저장
    Job Execution 하나당 하나의 Job Execution Context를 가짐
    BATCH_STEP_EXECUTION Step의 실행 정보
    BATCH_STEP_EXECUTIOIN_CONTEXT Step에 사용되는 모든 정보가 기록되는 Context 저

 

 

 

2. Spring Batch 실습

2.1 사전 세팅

2.1.1 Dependency

implementation 'org.springframework.boot:spring-boot-starter-batch'

 

 

2.1.1 Yaml

# 자동 생성 옵션 
spring:
  batch:
    jdbc.initialize-schema: always
    job.enabled: false # Application 시작할 때 Job 자동 실행 막음

 

 

2.2 간단한 배치 프로그램 만들어보기(Tasklet)

2.2.1 Tasklet으로 여러 Step을 처리하기

package com.kingpiggy.app.batch.statistic.dailyuserate;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.*;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static com.kingpiggy.app.batch.statistic.dailyuserate.DailyUseRateStatBatchConstant.*;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class DailyUseRateStatBatchConfig {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final DailyUseRateStatBatchService dailyUseRateStatJobService;

    @Bean(name = JOB_DAILY_USE_RATE_STAT)
    public Job dailyUseRateStatJob(){
        return jobBuilderFactory.get(JOB_DAILY_USE_RATE_STAT)
                .start(step1())
                .next(step2())
                .build();
    }

    @JobScope
    public Step step1() {
        return stepBuilderFactory.get(STEP1_DAILY_USE_RATE_STAT)
                .tasklet(((contribution, chunkContext) -> {
                    log.info("[step1#tasklet] param : [{}]", chunkContext.getStepContext().getJobParameters().get(PARAM_DAILY_USE_RATE_STAT));
                    dailyUseRateStatJobService.doSomethingA();
                    return RepeatStatus.FINISHED;
                }))
                .build();
    }

    @JobScope
    public Step step2() {
        return stepBuilderFactory.get(STEP2_DAILY_USE_RATE_STAT)
                .tasklet(((contribution, chunkContext) -> {
                    log.info("[step2#tasklet] param : [{}]", chunkContext.getStepContext().getJobParameters().get(PARAM_DAILY_USE_RATE_STAT));
                    dailyUseRateStatJobService.doSomethingB();
                    return RepeatStatus.FINISHED;
                }))
                .build();
    }

}

 

 

 

2.3 간단한 배치 프로그램 만들어보기(ItemReader, ItemProcessor, ItemWriter)

2.3.1 Chunk 단위를 ItemReader, ItemProceesor, ItemWriter로 처리하기

@Bean(name = JOB_WEEKLY_USE_RATE_STAT)
    public Job weeklyUseRateStatJob(){
        return jobBuilderFactory.get(JOB_WEEKLY_USE_RATE_STAT)
                .start(step1())
                .build();
    }

    @JobScope
    public Step step1() {
        return stepBuilderFactory.get(STEP1_WEEKLY_USE_RATE_STAT)
                .<WeeklyUseRateDto, WeeklyUseRateDto> chunk(weeklyUseRateStatBatchProps.getChunkSize())
                .reader(reader(null))
                .processor(processor())
                .writer(writer())
                .build();
    }

    @Bean
    @StepScope
    public WeeklyUseRateStatBatchItemReader reader(@Value("#{jobParameters[" + PARAM_WEEKLY_USE_RATE_STAT + "]}") String region) {
        return new WeeklyUseRateStatBatchItemReader(getWeeklyUseRateData());
    }

    @Bean
    @StepScope
    public WeeklyUseRateStatBatchItemProcessor processor() {
        return new WeeklyUseRateStatBatchItemProcessor();
    }

    @Bean
    @StepScope
    public WeeklyUseRateStatBatchItemWriter writer() {
        return new WeeklyUseRateStatBatchItemWriter();
    }

    private List<WeeklyUseRateDto> getWeeklyUseRateData() {
        List<WeeklyUseRateDto> sampleDataList = new ArrayList<>();

        for (int i=0; i<100; i++) {
            sampleDataList.add(new WeeklyUseRateDto("Name_" + i, "N"));
        }

        return sampleDataList;
    }

 

 

 


 

 

# 참고자료

728x90