개발놀이터

스프링 배치 @JobScope, @StepScope 본문

Spring/Spring Batch

스프링 배치 @JobScope, @StepScope

마늘냄새폴폴 2022. 9. 29. 01:07

본 포스팅은 인프런의 정수원님의 스프링 배치 강의를 듣고 정리한 포스팅입니다. 더 자세한 내용은 강의를 참고해주세요. 

 

 

@JobScope, @StepScope 기본 개념

  • Scope
    • 스프링 컨테이너에서 빈이 관리되는 범위
    • singleton, prototype, request, session, application 이 있으며 기본은 singleton 으로 생성됨
  • 스프링 배치 스코프
    • @JobScope, @StepScope
      • Job과 Step의 빈 생성과 실행에 관여하는 스코프
      • 프롯기 모드를 기본값으로 하는 스코프 - @Scope(value = "job", proxyMode = ScopedProxyMode.TARGET_CLASS)
      • 해당 스코프가 선언되면 빈의 생성이 어플리케이션 구동시점이 아닌 빈의 실행 시점에 이루어진다.
        • @Values 를 주입해서 빈의 실행 시점에 값을 참조할 수 있으며 일종의 Lazy Binding이 가능해진다.
        • @Value("#{jobParameters[파라미터명]}"), @Value("#{jobExecutionContext[파라미터명]}"), @Value("#{stepExecutionContext[파마리터명]}")
        • @Values를 사용할 경우 빈 선언문에 @JobScope, @StepScope를 정의하지 않으면 오류를 발생하므로 반드시 선언해야 함
      • 프록시 모드로 빈이 선언되기 때문에 어플리케이션 구동시점에는 빈의 프록시 객체가 생성되어 실행 시점에 실제 빈을 호출해준다.
      • 병렬처리 시 각 스레드 마다 생성된 스코프 빈이 할당되기 때문에 스레드에 안전하게 실행이 가능하다.
  • @JobScope
    • Step 선언문에 정의한다.
    • @Value : jobParameter, jobExecutionContext만 사용 가능
  • @StepScope
    • Tasklet이나 ItemReader, ItemProcessor, ItemWriter 선언문에 정의한다.
    • @Value : jobParameter, jobExecutionContext, stepExecutionContext 사용 가능

 

Scope는 스프링 컨테이너에서 빈이 관리되는 범위인데 우리에게 익숙한 내용은 아마 빈 생성주기에서 다루던 내용일겁니다. 

 

보통 빈 생성주기에는 singleton, prototype, request, session, application이 있습니다. 

 

스프링 배치에서는 이러한 빈 생성주기에 두가지가 더 추가됩니다. 바로 @JobScope와 @StepScope이죠 즉 @JobScope와 @StepScope는 빈 생성주기와 관련된 내용이라는 것입니다. 때문에 빈 생성과 실행에 관여하는 스코프입니다. 

 

이 @JobScope와 @StepScope에서 눈여겨 봐야 할 점은 바로 해당 스코프가 선언되면 빈의 생성이 어플리케이션 구동시점ㅁ이 아닌 빈의 실행시점에 이루어진다는 것입니다. 

 

빈 생성주기를 공부해보신 분이라면 감이 빨리 잡힐것입니다. 예를들어서 빈 생성주기에 singleton은 어플리케이션 구동시에 생성됩니다. 하지만 request는 request되고 난 후에 빈이 생성되죠 

 

@JobScope, @StepScope도 마찬가지입니다. @JobScope는 Job이 실행될 때, @StepScope는 Step이 실행될 때 빈이 생성됩니다. 

 

스프링 배치를 조금 사용해보신 분들이라면 아마 더 이해하기 편하실건데요. 우리가 SpEL을 이용해서 jobParameter를 사용할 때 @Value를 이용해 주입받는데 이 값을 해당 메서드를 선언할 때는 null값을 넣는것을 기억하실겁니다. 

 

 

 

@JobScope, @StepScope 아키텍처

  • Proxy 객체 생성
    • @JobScope, @StepScope 어노테이션이 붙은 빈 선언은 내부적으로 빈의 Proxy 객체가 생성된다.
      •  @JobScope
        • @Scope(value = "job", proxyMode = ScopedProxyMode.TARGET_CLASS)
      • @StepScope
        • Scope(value = "step", proxyMode = scopedProxyMode.TARGET_CLASS)
    • Job 실행 시 Proxy 객체가 실제 빈을 호출해서 해당 메서드를 실행시키는 구조
  • JobScope, StepScope
    • Proxy 객체의 실제 대상이 되는 Bean을 등록, 해제하는 역할
    • 실제 빈을 저장하고 있는 JobContext, StepContext를 가지고 있다.
  • JobContext, StepContext
    • 스프링 컨테이너에서 생성된 빈을 저장하는 컨텍스트 역할
    • Job의 실행 시점에서 프록시 객체가 실제 빈을 참조할 때 사용됨

 

@JobScope, @StepScope가 붙은 빈은 내부적으로 실제 객체가 아닌 Proxy 객체가 생성됩니다. 이는 흔히 알려지기로는 AOP 방식이라고 얘기합니다. 

 

왜 이렇게 작동할 수 밖에 없는지는 뒤에서 설명해드리도록 하겠습니다. 

 

JobScope, StepScope가 실제 빈을 저장하고 있는 JobContext, StepContext를 가지고 있다는 의미는 바로 ApplicationContext와 같은 일을 한다고 볼 수 있습니다. 

 

ApplicationContext는 우리가 만든 빈을 저장하고 관리하는 역할을 하고 필요하다면 해당 빈을 꺼내 쓸 수도 있습니다. 이러한 역할과 비슷하게 JobContext, StepContext가 한다고 이해하시면 되겠습니다. 

 

스프링 배치에서 Proxy객체로 만들어진 빈은 ApplicationContext로 참조하는 것이 아니라 JobContext, StepContext에서 참조하면 됩니다. 

 

 

스프링 배치가 실행될 때 @JobScope, @StepScope 는 다음과 같은 방식으로 구동됩니다. 

 

1. 우선 어플리케이션이 구동되면 ApplicationContext가 작동합니다. 그리고 빈을 만듭니다. 그때 @JobScope, @StepScope가 붙어있는지 확인합니다. 

 

2. 안붙어있다면 singleton bean으로 빈을 만듭니다. 이때 @Value가 붙어있다면 오류가 발생합니다. 

 

3. 붙어있다면 Proxy 객체로 빈을 생성합니다. 

 

4. 그 후 스프링 초기화가 완료되고 JobLauncher에 의해 Job이 실행됩니다. 이때 Job은 프록시 객체를 저장하고 있습니다. 

 

5. 프록시 객체는 실제 메소드를 호출 시 실제 빈 객체를 참조하는데 이 시점에 빈이 생성됩니다. 

 

6. 프록시 객체는 실제 빈을 등록하고 관리하는 JobScope를 통해 실제 빈을 관리합니다. 

 

7. 이후 JobContext에서 빈이 존재하면 JobContext에서 빈을 꺼냅니다. 그리고 실행을 시킵니다. 존재하지 않으면 스프링의 BeanFactory를 통해 해당 빈을 생성합니다. 이 시점에 해당 메소드를 실제로 실행합니다. 그리고 이때 @Value를 바인딩합니다. 

 

7-1. 빈이 존재하지 않은 경우 BeanFactory를 이용해 빈을 생성하고 JobContext에 해당 빈을 저장하고 JobScope에서 관리합니다. 

 

이를  그림으로 나타내면 다음과 같습니다. 그림으로 보시는게 좀 더 큰 그림을 이해하는데 도움이 될겁니다. 

 

 

 

그럼 개인적인 궁금증이긴 하지만 조금 궁금해집니다. 왜 AOP를 선택했을까? 

 

개인적인 추측이지만 아마 동적으로 @Value를 바인딩하기 위해 그런것이 아닐까 생각이 듭니다. 음...간단하게 제 생각을 말해보자면 

 

예를 들어서 JobParameter를 사용한다고 가정합니다.

 

JobParameter를 선언하는 부분이 어딜까요? 바로 JobLauncher를 이용해 run 메소드를 호출할 때입니다. 이 말은 JobLauncher를 이용해 잡이 한번 run 되어야 한다는 의미입니다. 

 

하지만 일반적인 ApplicationContext를 사용한다면 어플리케이션이 구동되자마자 빈들이 등록됩니다. 

 

그렇다면 순서가 이렇게 됩니다.

 

1. ApplicationContext 가동 (빈 생성) @Bean이 붙어있는 그리고 @JobScope, @StepScope가 붙어있는 빈도 생성하겠죠

2. 그리고 이 다음 JobLauncher로 Job이 run 됩니다. 그럼 이때 JobParameter가 생성되겠죠

3. 실제 객체가 실행됨에따라 JobParameter를 바인딩 하려고 합니다. 근데 이미 Step이나 다른 ItemReader나 ItemProcessor나 ItemWriter 빈이 생성되어있습니다. JobParameter는 null이겠죠 

4. 따라서 이렇게 작동되면 JobParameter를 SpEL로 주입받아서 사용할 수가 없습니다. 

 

 

때문에 Proxy 객체를 이용하는 것입니다. 

 

1. ApplicationContext 가동 (빈 생성) @Bean이 붙어있는 빈을 생성합니다. 

2. Proxy 객체로 가짜 빈을 생성해두고 다음으로 넘어갑니다. 

3. JobLauncher를 이용해 run이 됩니다. 이때 JobParameter가 생성됩니다. 

4. 이제 실제 객체를 실행합니다. 이때 실제 객체를 실행하면서 JobParameter를 바인딩합니다. 

5. 그럼 JobParameter를 사용할 수 있습니다. 

 

 

대충 이런 느낌때문에 프록시를, AOP를 이용하는게 아닌가 싶습니다. 단순한(?) 순서 문제 때문에 이런 방식을 사용하는 것 같습니다. 

 

 

이렇게 @JobScope, @StepScope에 대해 깊게 알아봤습니다. 이정도로 깊게 공부하니 뭔가 마음이 꽉 찬거같이 뿌듯하네요. 긴 글 읽어 주셔서 감사합니다. 다음 포스팅은 chunk 지향처리에 대해 알아보도록 하겠습니다.