노트북을 열고.

[spring boot batch] 4. 배치실행의 영역. Scope 본문

SpringBoot

[spring boot batch] 4. 배치실행의 영역. Scope

ahndy84 2019. 9. 3. 22:01

1. Scope 그리고 스프링의 기본 Scope, Singleton

앞선 장에서 설명드리지 않았으나 무척이나 중요한 역할을 하던 컴포넌트가 있습니다.

바로 배치가 실행될 때 Spring Bean을 생성하는 시점을 명시하는 @JobScope@StepScope 입니다.

@Bean
@StepScope
public ListItemReader<Member> unPaidMemberReader() {
    log.info("********** This is unPaidMemberReader");
    List<Member> activeMembers = memberRepository.findByStatusEquals(MemberStatus.ACTIVE);
    log.info(" - activeMember SIZE : " + activeMembers.size());
    List<Member> unPaidMembers = new ArrayList<>();
  
    for (Member member : activeMembers) {
      if(member.isUnpaid()) {
        unPaidMembers.add(member);
      }
    }
    log.info(" - unPaidMember SIZE : " + unPaidMembers.size());

    return new ListItemReader<>(unPaidMembers);
}

 

먼저 두 컴포넌트를 이해하기 위해 SpringFramework에서 기본 Scope에 대해 알아봅시다. 

Spring은 기본 ScopeSingleton입니다. 클래스 Instance를 오직 1개만 만들어 참조하는 것이 Singleton입니다. 

ApplicationContext context =
    new GenericXmlApplicationContext("applicationContext.xml");

    BondDao bondDao1 = context.getBean("bondDAO",BondDao.class);
    BondDao bondDao2 = context.getBean("bondDAO",BondDao.class);

    System.out.println("bondDao 1 : " + bondDao1);
    System.out.println("bondDao 2 : " + bondDao2);
    System.out.println("bondDao1 == bondDao2 ? " + bondDao1==bondDao2);

 

위의 코드는 다음의 결과를 보여줍니다.

bondDao1 : spring.dao.BondDAO@2903874
bondDao2 : spring.dao.BondDAO@2903874
bondDao1 == bondDao2 ? true

 

같은 Bean에서 각각 가져와서 만든 bondDao1 그리고 bondDao2 인스턴스는 서로 같은 값을 참조하고 있으니 동일한 인스턴스입니다. 즉 ApplicationContext로부터 Bean을 가져올 때 마다 인스턴스를 새로 생성하지 않는다는 의미입니다.

 

단순하게 응용을 해 보겠습니다. 

위 애플리케이션의 bondDao 클래스에서 데이터 조회를 하는 select메소드를 호출한다고 합니다. 이 애플리케이션은 여러명이 사용할테니 동시다발적으로 bondDao에 있는 select메소드를 호출할 수 있겠죠. 만약 1000개의 호출요청을 들어올 때 그에 맞춰 1000개의 인스턴스를 생성한다면 그야말로 엄청난 자원낭비가 될 수 있습니다. 어차피 동일한 Bean에서 가져올테니까요.

 

그래서 SpringFramework은 각각에 Beans들을 기본적으로 Singletone으로 관리를 하고 여러 Thread에서 이를 공유하여 사용할 수 있게 해줍니다. 다시말해 스프링의 기본 ScopeSingletone 입니다. 

 

2. @JobScope 그리고 @StepScope

@JobScope 그리고 @StepScope는 방금 소개한 스프링의 기본 ScopeSingleton과는 대치되는 역할입니다. @JobScope 그리고 @StepScope가 붙어 있는 메소드에서는 바로 그 메소드의 실행지점에 해당 @BeanSpring Bean으로 생성합니다.

 

이것은 Bean의 생성시점이 스프링 애플리케이션이 실행되는 시점이 아닌 @JobScope/@StepScope가 명시된 메소드의 실행될 때까지 지연시킨 다는 것(LateBinding)을 의미합니다.

 

SpringBatch에서는 이렇게 Bean 생성을 지연시킴으로써 얻게되는 장점은 다음과 같습니다.

1. JobParameter를 특정메서드가 실행하는 시점까지 지연시켜 할당시킬 수 있습니다.
애플리케이션이 구동되는 시점이 아니라 비즈니스로직이 구현되는 어디든 JobParameter를 할당함으로 유연한 설계를 가능하게 합니다.

2. 병렬처리에 안전합니다.
앞서 unPaidMemberReader메소드는 Step 구성요소(Itemreader=읽고, ItemProcessor=처리하고, ItemWriter=쓰고)중 데이터를 읽어오는 역할이 정의된 메소드입니다.
이 메소드가 서로 다른 Step으로 부터 동시에 병렬실행이 된다면 서로의 상태를 간섭을 받게 될 수 있습니다. 하지만 앞서 @StepScope를 명시해 놓을으로써 각각의 Step에서 실행될 때 서로의 상태를 침범하지 않고 처리를 완료할 수 있습니다.
@JobScopeStep선언문에서만 사용이 가능합니다.

@StepScopeStep을 구성하는 ItemReader, ItemWriter, ItemProcessor에서 사용 가능합니다.

 

public class unPaidMemberConfig {
    private MemberRepository memberRepository;
    private JobBuilderFactory jobBuilderFactory;
    private StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job unPaidMemberJob(

    ) {
        log.info("********** This is unPaidMemberJob");
        return jobBuilderFactory.get("unPaidMemberJob")
                .preventRestart()
                .start(this.unPaidMemberJobStep(null)) //1
                .build();
    }
    
    @Bean
    @JobScope  //2
    public Step unPaidMemberJobStep(@Value("#{jobParameters[requestDate]}") String requestDate) {
        log.info("********** This is unPaidMemberJobStep");
        log.info("********** This is requestDate of unPaidMemberJobStep : {}",  requestDate);  //3
        return stepBuilderFactory.get("unPaidMemberJobStep")
                .<Member, Member> chunk(10)
                .reader(unPaidMemberReader())
                .processor(this.unPaidMemberProcessor())
                .writer(this.unPaidMemberWriter())
                .build();
    }
...
}
1 unPiadMemberJobStep의 파라미터로 null을 주었습니다.
2

Bean으로 등록된 메소드에 @JobScope를 선언하였습니다.
이제 이 Bean은 실행되는 시점에 스프링의 Bean으로 생성됩니다

JobParameter는 다음과 같이 SpEL로 선언해서 사용합니다.
@Value("#{jobParameters[파라미터명]}")

3 해당 메소드에 실행되면서 실제 JobParameter로 할당받는 값을 log를 직접 찍어 확인하고자 합니다.

 

그리고 다음과 같이 JobParameter 값을 변경한 이후 애플리케이션을 실행시켜 봅시다.

run/Debug Configurations를 열어 Program arguments의 값을 requestDate=20190903_1 로 입력 후 OK버튼을 누릅니다.

 

실행완료이후 consolelog에서 방금전 작성한 로그의 값을 확인해 보면 requestDate의 값이 201903_1로 할당받은 것을 확인하실 수 있습니다. 

 

********** This is requestDate of unPaidMemberJobStep : 20190903_1

 

이렇게 JobParametersStep 또는 Step의 구성요소라 할 수 있는 읽고=ItemReader, 처리하고=ItemProcessor, 쓰고=ItemWriter와 같은 SpringBatch 컴포넌트 Bean의 생성 시점에 호출할 수 있습니다.

 

3. @JobScope(또는 StepScope) 와 JabParameter

지금까지의 내용을 살펴보면 @JobScope(StepScope)가 선언된 @Bean은 오직 그 Bean이 실행되는 시점에 스프링의 Bean으로 생성합니다.

 

또 그렇게 @JobScope(또는 StepScope)를 명시하면서 까지 스프링 Bean 생성을 지연시킬만 이유는 결국  새로운 JobInstance를 생성할 수 있는  JobParameter의 값을 애플리케이션 실행레벨(구동시점)이 아닌 비즈니스구현부 단계에서 할당되게 함으로써 보다  훨씬 더 유연한 프로세스 설계를 가능하게 하기 때문입니다.

 

이쯤이면 @JobScope(또는 StepScope)는 결국 JobParameter의 바인딩(LateBinding)을 위해, 더 나아가선 JobInstance의 생성과 서로 밀접하고 연쇄적인 관계를 이해하실 수 있을 것입니다.

 

부족한 지식으로 미약하게나마 스프링에서의 Scope 그리고 그 Scope안에서 할당되는 JobParameter의 관계에 대해서는 설명드렸습니다. 특히나 SpringBatch에서의 Scope의 개념은 향후 Batch설계의 복잡도에 따라 아주 섬세하게 고려해야할 만큼  매우 중요한 개념이니 꼭 숙지하셨으면 좋겠습니다.

 

다음편에서는 그동안 언급해왔던 Step의 삼형제, 읽고=ItemReader, 처리하고=ItemProcessor, 쓰고=ItemWriter 를 하나로 퉁친(?) 또 다른 컴포넌트인 TaskletStep의 흐름을 제어할 수 있는 다양한 분기요소에 대해서 설명드리겠습니다.

 

감사합니다.

Comments