웹/Spring

[Spring Batch] 스프링 배치의 기본적인 Job 이해하기

SeongOnion 2024. 1. 10. 20:00
728x90

Job

Job은 Spring Batch의 계층 구조에서 가장 상위에 위치한 개념으로, 하나의 배치 작업 그 자체를 의미한다.

 

 Job은 최소 하나 이상의 Step으로 구성되며, Spring Batch가 정의한 Job 인터페이스를 구현해 빈으로 등록 후 배치 작업을 실행시킬 수 있다.

 

JobParamter

Job 자체는 동일한 논리적인 작업 그 자체이지만, 해당 Job 자체는 모두 독립적인 실행을 보장해야할 것이다.

 

예컨대 특정 사용자가 한 해 사용한 카드값을 정산하는 Job이 있다고 가정했을 때, 카드값을 계산하는 로직 자체는 누구에게나 동일할 것이지만 정산을 하는 시점 및 대상 등은 Job을 실행하는 시점에 따라 달라질 수 있다. 

 

이러한 기능을 제공하기 위해서 Spring Batch에선 실제 Job을 실행할 때 JobParameter와 함께 JobInstance를 생성하고 관리하여 해당 Job이 고유한 것으로 취급될 수 있도록 한다.

 

아래처럼 JobParamter에 변하지 않는 특정 값을 설정해보자.

@Configuration
@RequiredArgsConstructor
public class JobLauncherConfiguration {

    private final Job simpleJob;
    private final JobLauncher jobLauncher;

    @Bean
    public ApplicationRunner jobLauncherWithParameter() {
        return args -> {
            JobParameters JobParamter = new JobParametersBuilder()
                    .addString("paramter", "1")
                    .toJobParameters();

            jobLauncher.run(simpleJob, JobParamter);
        };
    }
}

이후, 해당 Job을 두 번 실행시키면

A job instance already exists and is complete for identifying parameters={'paramter':'{value=1, type=class java.lang.String, identifying=true}'}.  If you want to run this job again, change the parameters.

위와 같이 해당 JobInstance가 이미 실행되었으므로, Job을 다시 한 번 실행시키고자 한다면 paramter를 변경하라는 메시지가 던져진다.

 

이러한 JobParameter는 특정 Job을 실행하는데 있어서 일종의 멱등키 역할을 할 수 있다.

 

만약 구성하고자 하는 Job이 어떠한 기준에 근거하여 중복으로 실행되선 안된다는 요구사항이 있다면 이 JobParamter를 적절히 구성해야할 것이다.

 

SimpleJob과 FlowJob

Spring Batch에선 기능적 측면에서 SimpleJob과 FlowJob을 Job 인터페이스의 기본 구현체로 제공한다.

 

순차 실행은 SimpleJob에게

SimpleJob 객체는 이름에서도 유추할 수 있듯 Job을 이루는 여러 Step들을 순차적으로 실행시켜준다.

 

아래 코드를 보자.

@Bean
public Job simpleJob() {
    Job jobInstance = new JobBuilder("defaultJob", jobRepository)
            .start(step1())
            .next(step2())
            .build();

    return jobInstance;
}


@Bean
public Step step1() {
    return new StepBuilder("step1", jobRepository)
            .tasklet((contribution, chunkContext) -> {
                System.out.println("step 1 executed");
                return RepeatStatus.FINISHED;
            }, transactionManager )
            .build();
}

@Bean
public Step step2() {
    return new StepBuilder("step2", jobRepository)
            .tasklet((contribution, chunkContext) -> {
                System.out.println("step 2 executed");
                return RepeatStatus.FINISHED;
            }, transactionManager )
            .build();
}

 

SimpleJob이 step1을 시작하고, 그 다음엔 step2를 시작하도록 설정되어있다.

 

해당 Job을 실행시켜보면 다음과 같이 step1과 step2가 순차적으로 실행된 것을 확인할 수 있다.

INFO 39162 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step1]
step 1 executed
INFO 39162 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [step1] executed in 56ms
INFO 39162 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step2]
step 2 executed
INFO 39162 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [step2] executed in 42ms

만약 Step을 더 추가하고자 한다면 간단히 JobBuilder의 next()로 체이닝해주면 된다.

 

논리적 실행은 FlowJob에게

FlowJob은 SimpleJob과 다르게, 그것을 구성하는 여러 개의 Step들에 대하여 논리 구조를 추가할 수 있다.

 

예컨대 특정 Step이 성공하면 stepA를, 실패하면 stepB를 실행시키도록 설정할 수 있는 것이다.

 

해당 논리는 앞서 언급했던 on(), to() 등의 flow 및 transition 관련 API와 함께 구성할 수 있다.

 

아래와 같은 flowJob이 구성되어있다고 가정해보자.

@Bean
public Job flowJob() {
    Job flowJob = new JobBuilder("flowJob", jobRepository)
            .start(firstStep())
            .on("COMPLETED").to(stepOnSuccess())
            .from(firstStep())
            .on("FAILED").to(stepOnFail())
            .end()
            .build();

    return flowJob;
}

@Bean
public Step firstStep() {
    FlowBuilder<Object> firstFlow = new FlowBuilder<>("firstFlow");
    return new StepBuilder("firstStep", jobRepository)
            .tasklet((contribution, chunkContext) -> {
                System.out.println("firstStep executed");
                return RepeatStatus.FINISHED;
            }, transactionManager )
            .build();
}

@Bean
public Step stepOnFail() {
    return new StepBuilder("stepOnFail", jobRepository)
            .tasklet((contribution, chunkContext) -> {
                System.out.println("stepOnFail executed");
                return RepeatStatus.FINISHED;
            }, transactionManager )
            .build();
}

@Bean
public Step stepOnSuccess() {
    return new StepBuilder("stepOnSuccess", jobRepository)
            .tasklet((contribution, chunkContext) -> {
                System.out.println("stepOnSuccess executed");
                return RepeatStatus.FINISHED;
            }, transactionManager )
            .build();
}

해당 FlowJob은 firstStep을 실행한 후, 해당 Step의 실행 결과에 따라 stepOnSuccess 혹은 stepOnFail을 실행시킨다.

 

우선 저대로 한 번 실행시켜보자.

firstStep executed
...
stepOnSuccess executed

firstStep이 문제없이 성공했으므로 stepOnSuccess가 실행된다.

 

이번엔 firstStep에서 예외를 발생시켜보자.

@Bean
public Step firstStep() {
    FlowBuilder<Object> firstFlow = new FlowBuilder<>("firstFlow");
    return new StepBuilder("firstStep", jobRepository)
            .tasklet((contribution, chunkContext) -> {
                System.out.println("firstStep executed");
                // return RepeatStatus.FINISHED;
                throw new RuntimeException();
            }, transactionManager )
            .build();
}
firstStep executed
ERROR 47179 --- [           main] o.s.batch.core.step.AbstractStep         : Encountered an error executing step firstStep in job flowJob

java.lang.RuntimeException: null
...
stepOnFail executed

역시 stepOnFail이 잘 실행된 것을 확인할 수 있었다!