[Spring Batch] Spring Batch 전체 동작 과정 알아보기

chrkb1569·2024년 3월 8일
0

Spring

목록 보기
10/11

요즘 개인 프로젝트에 사용하기 위한 목적으로 Spring Batch에 대해서 공부하고 있습니다.

그래서 공식 문서에 들어가 Spring Batch 구조를 살펴보면,

[Reference : https://docs.spring.io/spring-batch/reference/domain.html]

다음과 같은 그림이 우리를 반겨줍니다.

예 뭐

여기까지는 무난합니다.

그래서 희망차게 공부하기 시작하면,

[Reference : https://docs.spring.io/spring-batch/reference/domain.html]

???

JobInstance와 JobExecution이라는 친구들이 등장합니다.

그런데 이게 끝이 아닙니다.

[Reference : https://terasoluna-batch.github.io/guideline/5.0.0.RELEASE/en/Ch02_SpringBatchArchitecture.html]

다음처럼 StepExecution, ExecutionContext 등등.. 뭐가 많습니다..

아니 이거 사기아니냐

아무튼 이왕 사기 당한거 Spring Batch 전체 동작 과정에 대해서 알아보는 시간을 갖도록 하겠습니다.

Spring Batch 간략한 동작 과정

지난 게시글에서 간략하게 정리한 적이 있지만,

Job이라는 것은 Step들의 모음, Step은 Tasklet의 모음이라고 정리할 수 있습니다.

따라서 하나의 Job이 실행되면 Job을 구성하고 있는 Step들이 정해진 순서대로 실행이 되는 것이고,

Step이 실행되면 Step을 구성하고 있는 Tasklet들이 실행되는 구조입니다.

예를 들어서 설명하자면, Job을 '씻기'라고 가정해보겠습니다.

일반적으로 '씻기'에는 '머리 감기', '샤워하기', '세수하기', '이 닦기'가 포함될 것입니다.

여기서 '머리 감기', '샤워하기', '세수하기', '이 닦기'는 Job을 구성하고있는 Step이 될 것이고, 머리를 감는 방법, 샤워를 하는 방법 하나하나는 Tasklet이 될 것입니다.

Spring Batch는 방금 말씀드린 거대한 틀 위에서 동작합니다.

나머지 JobInstance, JobExecution, StepExecution, ExecutionContext 는 이러한 틀을 구성하는 정보입니다.

예를 들어서 우리가 씻다가 어떠한 용무로 인하여 중간에 멈추었다고 가정해보겠습니다.

용무를 처리하고 다시 복귀하였을 때, 우리는 이미 했던 일들을 다시하지 않습니다.

아까 머리를 감았으면, 이번에는 머리 감기를 제외한 나머지 하지 않은 일들을 수행할 것입니다.

이렇게 할 수 있는 이유는 우리가 중단되기 이전에 했던 일들을 기억할 수 있기 때문입니다.

하지만, 애플리케이션은 우리처럼 이전에 있었던 일을 기억할 수 없습니다.

따라서, 이전에 했던 정보들을 데이터베이스에 저장한 뒤, 이를 토대로 작업을 이어하는 것으로 우리의 행동을 모방하는 것입니다.

일단 이렇게까지만 이해하고 나머지는 자세히 알아보면 좋을 것 같습니다.

JobLauncher

이름에서부터 알 수 있듯, Job을 실행해주는 친구입니다.

Spring Batch 라이브러리에 명시되어있는 JobLauncher입니다.

아래에 보면 run() 이라는 추상 메서드 하나만 선언되어있는데, 저게 끝입니다.

우리가 Spring Batch를 동작하면, 저 run() 메서드를 실행하여 우리가 등록한 Job을 실행합니다.

JobParameters는 일단 HTTP 헤더처럼 Job을 실행하는데에 필요한 정보들을 담아놓은 Map이라고 생각하면 좋을 것 같습니다.

JobParameters

JobLauncher에서 Job과 함께 파라미터로 넘겨주는 값입니다.

근데 이 친구, 사실 좀 낯이 익습니다.

Spring Batch가 동작하면서 자동적으로 생성되었던 메타 데이터에 대해 기억하시나요?

BATCH_JOB_EXECUTION_PARAMS라는 테이블이 존재하는데, 하필 JobLauncher의 run() 메서드의 반환 타입이 JobExecution이네요

네 이놈이 저놈 맞습니다.

JobLauncher가 run() 메서드를 실행하면서 넘겨주었던 값이 이 테이블에 저장되는 것입니다.

그럼 한 번 코드로 확인해보겠습니다.

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.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@RequiredArgsConstructor
public class BatchConfiguration {
    @Bean
    public Job helloJob(JobRepository jobRepository, Step helloStep) {
        return new JobBuilder("helloJob", jobRepository)
                .start(helloStep)
                .build();
    }

    @Bean
    public Step helloStep(JobRepository jobRepository, Tasklet helloTasklet, PlatformTransactionManager transactionManager) {
        return new StepBuilder("helloStep", jobRepository)
                .tasklet(helloTasklet, transactionManager)
                .build();
    }

    @Bean
    public Tasklet helloTasklet() {
        return (contribution, chunkContext) -> {
            System.out.println("Hello! This is Spring Batch Practice!!");
            return RepeatStatus.FINISHED;
        };
    }
}

일단 Job, Step, Tasklet은 다음처럼 간단하게 구성하였습니다.

그리고 다음처럼 별도의 클래스를 하나 생성해주겠습니다.

import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class BatchCustomRunner implements ApplicationRunner {
    private final JobLauncher jobLauncher;
    private final Job job;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        JobParameters jobParameters = new JobParametersBuilder()
                .addString("ParameterTest", "Hello!")
                .toJobParameters();

        jobLauncher.run(job, jobParameters);
    }
}

ApplicationRunner를 구현할 경우, 애플리케이션이 동작하면 run() 내부 로직을 실행할 수 있습니다.

여기서 JobLaucher와 Job을 가져오겠습니다.

JobParameters는 JobParametersBuilder를 통해 생성할 수 있으며, 파라미터 값으로 String, Date, Long, Double을 추가할 수 있습니다.

이 상태에서 Spring Batch를 실행하면,

다음처럼 BATCH_JOB_EXECUTION_PARAMS 테이블에 우리가 추가했던 데이터가 저장된 것을 확인할 수 있습니다.

JobInstance

Instance라는 이름으로 확인할 수 있는 것처럼, Job에 의해 생성된 객체입니다.

갑자기 뭔 객체냐? 하실 수 있는데,

Job을 하나의 클래스라고 생각하면 이해하기 편할 것 같습니다.

우리가 클래스를 선언한 뒤, 객체를 생성한다고 가정해보겠습니다.

생성된 객체들의 타입은 모두 동일하고, 동일한 메서드를 가지고 있지만 객체의 데이터에 따라 다른 결과를 보여줄 것입니다.

클래스와 다른 점이 있다면, Spring Batch는 JobInstance를 Job의 이름과 Job을 실행하는 과정에서 사용하였던 JobParameters를 통해 JobInstance를 식별합니다.

JobParameters를 설명하면서 실행했던 기록을 살펴보면,

다음처럼 메타 데이터에 JobInstance가 존재하는 것을 확인할 수 있으며,

다음처럼 아까 입력했던 Job의 이름과 동일한 데이터가 존재하는 것을 확인할 수 있습니다.

JobParameters는 해시 알고리즘에 의해 JOB_KEY라는 이름의 문자열로 변환됩니다.

JobInstance 유의할 점

JobLauncher를 통해 Job을 실행하는 경우, 매개변수로 Job과 JobParameter를 넘겨줍니다.

여기서 Spring Batch는 이 매개변수를 통해 JobInstance를 구분하기 때문에, 동일한 매개변수로 Spring Batch를 실행하려고하면 오류가 발생합니다.

정확히 설명하자면, 동일한 매개변수로 요청했던 처리가 정상적으로 완료되었음에도 불구하고 요청할 경우에 오류가 발생합니다.

동일한 매개변수로 요청하더라도 이전에 실패했던 요청이라면 정상적으로 처리됩니다.

이러한 내용은 나중에 살펴보도록 하겠습니다.

JobExecution

JobLauncher의 run() 메서드 반환타입으로, JobInstance의 실행 기록이라고 보면 될 것 같습니다.

JobLauncher를 통해 Job이 실행되면 JobInstance가 생성되어 정해진 로직을 수행하지만, 이 로직이 무조건 성공하는 것은 아닙니다.

정해진 작업을 수행하다가 예상하지 못한 오류가 발생하여 작업이 중단될 수 있는데, 이러한 JobInstance의 실행 정보들을 저장하고 있는 객체라고 할 수 있습니다.

이전에 실행했던 기록을 살펴보겠습니다.

다음처럼 BATCH_JOB_EXECUTION 이라는 테이블을 확인할 수 있는데, 자세히 살펴보면

Job의 실행 시간, 종료 시간, 결과 등의 정보들을 확인할 수 있습니다.

그럼 실패하면 어떻게 될까요?

Tasklet의 코드를 다음처럼 수정해보겠습니다.

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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@RequiredArgsConstructor
public class BatchConfiguration {
    @Bean
    public Job helloJob(JobRepository jobRepository, Step helloStep) {
        return new JobBuilder("helloJob", jobRepository)
                .start(helloStep)
                .build();
    }

    @Bean
    public Step helloStep(JobRepository jobRepository, Tasklet helloTasklet, PlatformTransactionManager transactionManager) {
        return new StepBuilder("helloStep", jobRepository)
                .tasklet(helloTasklet, transactionManager)
                .build();
    }

	// Tasklet 수정
    @Bean
    public Tasklet helloTasklet() {
        return (contribution, chunkContext) -> {
            throw new RuntimeException("Batch Error!");
        };
    }
}

동일한 Job_NAME, JOB_KEY로 요청하기 때문에 데이터베이스는 한 번 초기화하고 실행하였습니다.

일단 우리가 원했던 것처럼 오류가 발생하였습니다.

JobExecution 테이블을 확인하면 다음처럼 결과에 FAILED가 표기된 것을 확인할 수 있습니다.

그런데 아까 설명했던 내용 기억하시나요?

동일한 Job, Parameters로 요청하는 경우, 성공했던 Job은 오류를 발생시키지만, 실패했던 Job은 오류를 발생시키지 않는다는 점.

Tasklet을 아까처럼 바꾼뒤, 다시 요청해보겠습니다.

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.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@RequiredArgsConstructor
public class BatchConfiguration {
    @Bean
    public Job helloJob(JobRepository jobRepository, Step helloStep) {
        return new JobBuilder("helloJob", jobRepository)
                .start(helloStep)
                .build();
    }

    @Bean
    public Step helloStep(JobRepository jobRepository, Tasklet helloTasklet, PlatformTransactionManager transactionManager) {
        return new StepBuilder("helloStep", jobRepository)
                .tasklet(helloTasklet, transactionManager)
                .build();
    }
	// 원상 복구
    @Bean
    public Tasklet helloTasklet() {
        return (contribution, chunkContext) -> {
            System.out.println("Hello!");
            
            return RepeatStatus.FINISHED;
        };
    }
}

오... 정상적으로 Job이 처리되었습니다.

재미있는 점은 JobExecution 테이블에 데이터가 2개 존재한다는 점인데, 하나는 실패했을 때의 데이터, 하나는 방금 성공한 데이터입니다.

여기서 Spring Batch를 한 번 더 실행해보면,

성공했던 Job이기 때문에 오류가 발생하는 것을 확인할 수 있습니다.

JobRepository

[Reference : https://terasoluna-batch.github.io/guideline/5.0.0.RELEASE/en/Ch02_SpringBatchArchitecture.html]

그림에서 확인할 수 있듯, 데이터베이스와 연결되어 있는 부분으로, Job, Step과 관련된 정보들을 관리합니다.

예를 들어 우리가 Job을 실행했을때, JobInstance가 생성되는데 JobInstance와 관련된 정보들을 데이터베이스에 저장하는 역할을 수행합니다.

StepExecution

앞에서 살펴보았던 JobExecution과 비슷한 속성을 띄고 있는 객체입니다.

아까 실행했던 정보를 바탕으로 확인해보겠습니다.

Job하나에 Step 하나를 생성해두었기 때문에 총 2개의 Step을 확인할 수 있었습니다.

ExecutionContext

앞에서 설명했던 객체들과는 조금 다른 속성을 가지고 있는 친구입니다.

사실 앞에서 설명했던 객체들은 하나의 도메인에 속해있습니다.

Job이면 Job과 관련된 정보만을 저장하고, Step이면 Step과 관련된 정보들을 저장한다는 특징을 가지고 있는데, ExecutionContext는 Job, Step 모든 정보를 저장한다는 특징을 가지고 있습니다.

[Reference : https://terasoluna-batch.github.io/guideline/5.0.0.RELEASE/en/Ch02_SpringBatchArchitecture.html]

아까 앞에서 살펴봤던 그림을 살펴보면, ExecutionContext가 JobExecution 내부에 하나, StepExecution에 하나 이렇게 총 2개가 존재함을 확인할 수 있습니다.

JobParameters처럼 Key-Value 형식으로 데이터를 저장할 수 있다는 점은 동일한데, 데이터를 공유할 수 있는 범위에서 차이가 발생합니다.

Step 내부에 위치한 ExecutionContext는 Step 내부에서만 사용할 수 있습니다.

즉, 다음처럼 Step 내부에서만 사용할 수 있는 데이터 저장 공간이라고 생각하면 좋을 것 같습니다.

반면에 Job 내부에 위치한 ExecutionContext는 Job을 구성하고 있는 Step들까지 공유할 수 있다는 특징을 가지고 있습니다.

그럼 코드를 작성해보면서, 이를 확인해보도록 하겠습니다.

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.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

import java.util.Map;

@Configuration
@RequiredArgsConstructor
public class BatchConfiguration {
    @Bean
    public Job helloJob(JobRepository jobRepository,
                        @Qualifier("helloStep") Step helloStep,
                        @Qualifier("nestStep") Step nextStep,
                        @Qualifier("finalStep") Step finalStep) {
        return new JobBuilder("helloJob", jobRepository)
                .start(helloStep)
                .next(nextStep)
                .next(finalStep)
                .build();
    }

    @Bean
    @Qualifier("helloStep")
    public Step helloStep(JobRepository jobRepository,
                          @Qualifier("helloTasklet") Tasklet helloTasklet,
                          PlatformTransactionManager transactionManager) {
        return new StepBuilder("helloStep", jobRepository)
                .tasklet(helloTasklet, transactionManager)
                .build();
    }

    @Bean
    @Qualifier("nestStep")
    public Step nextStep(JobRepository jobRepository,
                         @Qualifier("nextTasklet") Tasklet nextTasklet,
                         PlatformTransactionManager transactionManager) {
        return new StepBuilder("nextStep", jobRepository)
                .tasklet(nextTasklet, transactionManager)
                .build();
    }

    @Bean
    @Qualifier("finalStep")
    public Step finalStep(JobRepository jobRepository,
                          @Qualifier("finalTasklet") Tasklet finalTasklet,
                          PlatformTransactionManager transactionManager) {
        return new StepBuilder("finalStep", jobRepository)
                .tasklet(finalTasklet, transactionManager)
                .build();
    }

    @Bean
    @Qualifier("helloTasklet")
    public Tasklet helloTasklet() {
        return (contribution, chunkContext) -> {
            System.out.println("This is HelloTasklet!");
            contribution.getStepExecution().getExecutionContext().put("helloKey", "helloTasklet");
            contribution.getStepExecution().getJobExecution().getExecutionContext().put("publicData1", "publicData1");

            return RepeatStatus.FINISHED;
        };
    }

    @Bean
    @Qualifier("nextTasklet")
    public Tasklet nextTasklet() {
        return (contribution, chunkContext) -> {
            System.out.println("This is NextTasklet!");
            contribution.getStepExecution().getExecutionContext().put("nextKey", "nextTasklet");
            contribution.getStepExecution().getJobExecution().getExecutionContext().put("publicData2", "publicData2");

            return RepeatStatus.FINISHED;
        };
    }

    @Bean
    @Qualifier("finalTasklet")
    public Tasklet finalTasklet() {
        return (contribution, chunkContext) -> {
            System.out.println("This is FinalTasklet!");
            System.out.println("ExecutionContext in FinalTasklet");
            for(Map.Entry<String, Object> entry : contribution.getStepExecution().getExecutionContext().entrySet()) {
                System.out.println(entry.getKey() + " " + entry.getValue());
            }

            System.out.println("ExecutionContext in Job");
            for(Map.Entry<String, Object> entry : contribution.getStepExecution().getJobExecution().getExecutionContext().entrySet()) {
                System.out.println(entry.getKey() + " " + entry.getValue());
            }

            return RepeatStatus.FINISHED;
        };
    }
}

첫 번째, 두 번째 Step에서는 ExecutionContext에 데이터를 저장하고, 마지막 Step에서 이를 출력해보도록 Job을 구성해보았습니다.

이전과 동일한 Job과 JobParameters를 사용하기 때문에 데이터베이스를 한 번 초기화하고 동작하였습니다.

첫 번째, 두 번째 Step에서 ExecutionContext에 저장했던 데이터들은 보이지 않지만, Job의 ExecutionContext에 저장했던 데이터들은 확인할 수 있었습니다.

0개의 댓글