글 작성자: beaniejoy

Spring Batch를 공부하다 Scope에 대한 내용을 접하게 되었습니다. 그 중에 Batch의 Scope를 설정하면 Thread Safe하게 batch job이나 step을 사용할 수 있다는 내용을 접했고 이에 대한 내용을 테스트해보며 어떤 건지 한 번 알아보았습니다.

Spring Batch의 Scope을 사용했을 때와 안했을 때 어떻게 달라지는지를 확인해보며 Thread Safe한 특징이 무엇인지 알아봅시다.
(개념적인 설명은 추후에 한꺼번에 정리해서 게시글 작성할 계획입니다.)

 

📌 1. Batch Scope

Batch의 Scope은 bean의 생명주기(lifecycle)과 관련이 깊습니다.

기존에 Spring을 사용하면 Spring Context에서 bean을 관리하면서 생명주기에 대해서도 전부 Spring에 위탁을 하게 됩니다. 하지만 Spring Batch에서 제공하는 Scope을 사용하게 되면 Job, Step 별로 bean의 생명주기를 관리할 수 있게 됩니다.

Spring Batch에는 두 가지 scope이 존재합니다.

  • @JobScope
    - Job 실행과 종료 시점에 생성되고 job 종료 시점에 소멸
    - Step에 선언
  •  @StepScope
    - Step 실행과 종료 시점에 생성 및 소멸
    - Tasklet, Chunk(ItemReader, ItemProcessor, ItemWriter)에 선언

Scope의 다른 특징은 제쳐두고 Thread Safe에 초점을 맞춰 알아보겠습니다.

 

📌 2. Scope 없이 같은 bean 객체를 여러 Step으로 구성하기

Batch Scope를 통해 각 batch에 대한 Thread Safe한 작업을 진행할 수 있게 됩니다.

// TestBatchConfig.java
@Bean
public Job testJob() {
    return jobBuilderFactory.get("testJob")
            .incrementer(new RunIdIncrementer())
            .start(testStep())  // 하나의 Tasklet으로 만들어진 Step을
            .next(testStep())   // 여러 개로 지정
            .build();
}

// testJobTasklet Injection 받음
@Bean
public Step testStep() {
    return stepBuilderFactory.get("testStep")
            .tasklet(testJobTasklet)
            .build();
}

Test를 위한 TestBatchConfig를 준비했고 testJob하나에 두 개의 Step을 설정했습니다. 하나의 testStep에는 외부에서 주입받아온 testJobTasklet bean 객체를 tasklet으로 설정했습니다.

결국 testJob은 하나의 testJobTasklet bean 객체를 주입받아서 여러 번의 Step을 진행하게 되는 것입니다.

@Slf4j
@Component
public class TestJobTasklet implements Tasklet {
    private String name;
    private String value;

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        log.info(">>>>>>>>>>>>>>>>>> name: " + name);
        log.info(">>>>>>>>>>>>>>>>>> value: " + value);
        StepExecution stepExecution = contribution.getStepExecution();
        Long id = stepExecution.getId();

        updateInfo(id + " Task", id +" value");

        return RepeatStatus.FINISHED;
    }

    public void updateInfo(String name, String value) {
        this.name = name;
        this.value = value;
    }
}

TestJobTasklet은 임의의 맴버변수 두 개를 설정했고 execute에서 가장 먼저 해당 맴버변수들을 logging하는 것으로 시작합니다.
그 이후에는 stepExecution(BATCH_STEP_EXECUTION)의 id 값을 받아서 TestJobTasklet의 필드 값을 update해줍니다.

이렇게 구성하고 testJob을 실행하면 다음과 같은 로그가 남게됩니다.

첫 Step에서는 TestJobTasklet 객체의 맴버필드들을 로그 찍었을 때 null 값이었지만 (처음은 맴버필드의 default value 설정) 두 번째 Step에서는 이전 Step에서 update했던 내용들이 로그에 남은 것을 확인할 수 있습니다.

하나의 bean 객체를 가지고 여러 Step에서 사용했기 때문에 결국 하나의 객체를 사용하게 되는 것이고 이것은 Thread Unsafe하다고 할 수 있습니다.

 

📌 3. Scope 설정했을 때는 어떻게 될까

다음과 같이 Tasklet 클래스에 Batch Scope를 설정해봅시다.

@Slf4j
@Component
@StepScope	// Tasklet에 지정했으므로 @StepScope
public class TestJobTasklet implements Tasklet {
    //...
}

@StepScope을 설정해주면 기존 Spring Bean Singleton 수행 방식이 아닌 Batch 작업 시작, 종료에 따라 bean 생성, 소멸 시점을 결정하게 됩니다. Step Scope으로 설정했기 때문에 Step의 시작과 종료에 따라 TestJobTasklet bean의 생성과 소멸이 결정되게 됩니다.

즉 Spring Application에 따라 생성, 소멸되는 것이 아닌 Step의 시작과 종료에 따라 해당 bean의 생성, 소멸을 결정하게 된다는 것입니다. 그렇기 때문에 하나의 bean 객체를 가지고 여러 Step에서 사용해도 Thread Safe하게 됩니다.

Batch Scope를 설정하고 testJob을 실행해봅시다.

두 개의 Step 단계에서 TestJobTasklet 맴버필드가 모두 null인 것을 확인할 수 있습니다. 즉 각각의 Step 별로 새로운 TestJobTasklet bean이 생성되고 새로운 bean을 사용한 것입니다.