5. Configuring a Step

도메인 장에서 설명한 것처럼, 스텝(Step)은 배치 잡의 독립적이고 순차적인 단계를 캡슐화하며 실제 배치 처리를 정의하고 제어하는 ​​데 필요한 모든 정보를 가지고 있는 도메인 객체이다. 주어진 스텝의 내용은 을 작성하는 개발자의 재량에 따르기 때문에 스텝에 대한 설명는 애매하다. 스텝은 개발자의 노력에 따라 간단하거나 복잡할 수 있다. 간단한 스텝은 파일에서 데이터베이스로 데이터를 로드할 수 있으며, 코드가 거의 또는 전혀(사용된 구현에 따라 다름) 필요하지 않다. 복잡한 스텝은 다음 이미지와 같이, 처리의 일부로 복잡한 비즈니스 규칙이 있을 수 있다:

스텝

이미지 13. 스텝(Step)

5.1. Chunk-oriented Processing

일반적으로 스프링 배치는 구현체에서 “청크 지향” 스타일을 사용한다. 데이터를 한 번에 읽어 트랜잭션 경계 내에서 ‘청크’ 단위를 생성하는 것을 청크 지향 처리라 말한다. 읽은 아이템 수가 커밋 인터벌(commit interval)과 같으면, 청크가 아아템라이터(ItemWriter)에 의해 기록되며, 트랜잭션이 커밋된다. 다음 이미지는 해당 프로세스를 보여준다:

청크 지향 처리

이미지 14. 청크 지향 처리

다음 의사 코드는 동일한 개념을 단순한 형태로 보여준다:

  List items = new Arraylist();
  for(int i = 0; i < commitInterval; i++){
      Object item = itemReader.read();
      if (item != null) {
          items.add(item);
      }
  }
  itemWriter.write(items);

아이템을 아이템라이터(ItemWriter)에 전달하기 전에 아이템을 처리하기 위해 옵셔널로 아이템프로세서(ItemProcessor)를 사용하여 청크 지향 스텝를 구성할 수도 있다. 다음 이미지는 스텝에 아이템프로세서(ItemProcessor)를 등록한 경우의 프로세스를 보여준다:

이미지 15. 아이템프로세서(ItemProcessor)와 청크 지향 처리

다음 의사 코드는 단순한 형식의 구현 방법을 보여준다:

  List items = new Arraylist();
  for(int i = 0; i < commitInterval; i++){
      Object item = itemReader.read();
      if (item != null) {
          items.add(item);
      }
  }
  List processedItems = new Arraylist();
  for(Object item: items){
      Object processedItem = itemProcessor.process(item);
      if (processedItem != null) {
          processedItems.add(processedItem);
      }
  }
  itemWriter.write(processedItems);

아이템 프로세서(item processor) 및 해당 사례에 대한 자세한 내용은, 아이템 처리(processing) 장을 참고하자.

5.1.1. Configuring a Step

상대적으로 스텝의 필수 의존성은 적지만, 잠재적으로 많은 협력 라이브러리를 포함할 수도 있는 매우 복잡한 클래스이다.

쉬운 구성을 위해, 다음 예제와 같이, 스프링 배치 XML 네임스페이스를 사용할 수 있다:

XML 구성

  <job id="sampleJob" job-repository="jobRepository">
    <step id="step1">
      <tasklet transaction-manager="transactionManager">
        <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
      </tasklet>
    </step>
  </job>

자바를 사용하는 경우, 다음 예제와 같이, 스프링 배치 빌더를 사용할 수 있다:

자바 구성

  /**
   * 잡리포지터리(JobRepository)는 일반적으로 오토와이어드(autowired)되며 명시적으로 구성할 필요는 없다.
   */
  @Bean
  public Job sampleJob(JobRepository jobRepository, Step sampleStep) {
    return new JobBuilder("sampleJob", jobRepository)
            .start(sampleStep)
            .build();
  }

  /**
   * 트랜젝션매니저(TransactionManager)는 일반적으로 자동 연결되며 명시적으로 구성할 필요는 없다.
   */
  @Bean
  public Step sampleStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder("sampleStep", jobRepository)
                .<String, String>chunk(10, transactionManager)
                .reader(itemReader())
                .writer(itemWriter())
                .build();
  }

항목 지향 스텝(item-oriented step)를 만드는 데 필요한 유일한 의존성은 다음과 같다:

  • 리더(reader): 처리할 아이템을 제공하는 아이템리더(ItemReader).
  • 라이터(writer): 아이템리더(ItemReader)가 제공한 아이템을 처리하는 아이템라이터(ItemWriter).
  • 트랜젝션 매니저(transaction-manager) (XML)/트랜젝션매니저(transactionManager) (자바): 처리 중 트랜잭션을 시작하고 커밋하는 스프링의 플랫폼트랜젝션매니저(PlatformTransactionManager).
  • 잡 리포지터리(job-repository) (XML)/리포지터리(repository) (Java): 처리 중(커밋 직전) 스텝익스큐션(StepExecution)익스큐션컨텍스트(ExecutionContext)를 주기적으로 저장하는 잡리포지터리(JobRepository). XML에서, 인라인 ( 내에 정의된 것)의 경우, 엘리먼트의 애트리뷰트이다. 독립형(standalone) 스텝의 경우 의 애트리뷰트로 정의된다.
  • 커밋-인터벌(commit-interval) (XML)/청크(chunk) (Java): 트랜잭션 커밋 전에 처리할 아이템의 수이다.

잡-리포지터리(job-repository)(XML)/리포지터리(repository)(Java)의 기본값은 잡리포지터리(jobRepository)이고 트랜젝션 매니저(transaction-manager)(XML)/트랜젝션매니저(transactionManager)(Java)의 기본값은 트랜젝션매니저(transactionManager)이다. 또한, 아이템프로세서(ItemProcessor)는 아이템이 리더(reader)에서 라이터(writer)로 직접 전달될 수 있으므로, 선택 사항이다.

5.1.2. Inheriting from a Parent Step

스텝 그룹이 유사한 구성을 공유하는 경우, 스텝이 프로퍼티를 상속할 수 있도록 “상위” 스텝을 정의하는 것이 도움이 된다. 자바의 클래스 상속과 유사하게, “하위” 스텝은 해당 엘리먼트와 애트리뷰트를 상위 스텝과 결합시킨다. 하위 스텝은 또한 상위 스텝의 모든 것을 오버라이드(override)한다.

다음 예에서, 스텝, 콘크리트스텝1(concreteStep1)페어런트스텝(parentStep)을 상속한다. 아이템리더(itemReader), 아이템프로세서(itemProcessor), 아이템라이터(itemWriter), startLimit=5allowStartIfComplete=true로 인스턴스화된다. 또한, 커밋인터벌(commitInterval)은 다음 예제와 같이 콘크리트스텝1(concreteStep1) 스텝에 의해 오버라이드(override)되므로 5이다:

  <step id="parentStep">
    <tasklet allow-start-if-complete="true">
      <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
    </tasklet>
  </step>

  <step id="concreteStep1" parent="parentStep">
    <tasklet start-limit="5">
      <chunk processor="itemProcessor" commit-interval="5"/>
    </tasklet>
  </step>

id 애트리뷰트는은 잡 엘리먼트 내의 스텝에서 계속 필요하다. 이것은 두 가지 이유 때문이다:

  • id는 스텝익스큐션(StepExecution)을 유지할 때 스텝 이름으로 사용된다. 동일한 독립 실행형(standalone) 스텝이 잡에서 두 번 이상 참조되면, 오류가 발생한다.
  • 잡 플로우를 만들 때, 이 장의 뒷부분에서 설명하는 대로, 다음 애트리뷰트는 독립 실행형 스텝이 아니라, 플로우에서 스텝을 참조해야 한다.

Abstract Step

경우에 따라, 완전벽하지 않은 상위 스텝을 정의해야 할 수도 있다. 예를 들어 리더(reader), 라이터(writer)tasklet 애트리뷰트가 스텝 구성에서 제외되면 초기화에 실패한다. 이러한 애트리뷰트가 하나 이상 없는 상위 스텝을 정의해야 하는 경우, 앱스트랙트(Abstract) 애트리뷰트를 사용해야 한다. 앱스트랙트 스텝은 상속(extend)만 가능하며 인스턴스화(instantiated)되지 않는다.

다음 예제에서, 스텝(abstractParentStep)는 앱스트랙트(abstract)로 선언되지 않은 경우 인스턴스화되지 않는다. 스텝, 콘트리트스텝2(concreteStep2)는 아이템리더(itemReader), 아이템라이터(itemWriter) 그리고 commit-interval=10를 가진다.

  <step id="abstractParentStep" abstract="true">
    <tasklet>
      <chunk commit-interval="10"/>
    </tasklet>
  </step>

  <step id="concreteStep2" parent="abstractParentStep">
    <tasklet>
      <chunk reader="itemReader" writer="itemWriter"/>
    </tasklet>
  </step>

Merging Lists

스텝에서 일부 엘리먼트는 <listeners/> 엘리먼트 같이 리스트이다. 상위 스텝과 하위 스텝 모두 <listeners/> 엘리먼트를 선언하면 하위 스텝의 리스트가 상위 스텝의 리스트를 오버라이드한다. 하위 스텝에 상위 스텝의 리스트에 리스너를 추가할 수 있도록 모든 리스트 엘리먼트에는 머지(merge) 애트리뷰트가 있다. 엘리먼트에 merge="true"를 지정하면, 하위 리스트가 오버라이드하는 대신 상위 리스트와 결합된다.

다음 예에서 “concreteStep3” 스텝listenerOnelistenerTwo라는 두 개의 리스너로 생성된다:

  <step id="listenersParentStep" abstract="true">
    <listeners>
      <listener ref="listenerOne"/>
    <listeners>
  </step>

  <step id="concreteStep3" parent="listenersParentStep">
    <tasklet>
      <chunk reader="itemReader" writer="itemWriter" commit-interval="5"/>
    </tasklet>
    <listeners merge="true">
      <listener ref="listenerTwo"/>
    <listeners>
  </step>

5.1.3. The Commit Interval

앞에서 언급했듯이, 스텝은 아이템을 읽고 쓰고, 제공된 플랫폼스랜젝션매니저(PlatformTransactionManager)를 사용하여 주기적으로 커밋한다. 커밋 인터벌(commit interval)이 1이면, 각각의 아이템을 작성한 후 커밋한다. 트랜잭션을 시작하고 커밋하는 데는 비용이 많이 들기 때문에, 이는 대부분 상황에서 이상적이지 않다. 이상적으로, 각 트랜잭션에서 가능한 한 많은 아이템을 처리하는 것이 바람직하며, 이는 처리 중인 데이터 타입과 스텝이 상호 작용하는 리소스에 따라 완전히 달라진다. 이러한 이유로, 커밋 내에서 처리되는 아이템 수를 적절히 구성해야 한다.

다음 예는 XML에 정의된 것처럼 tasklet커밋 인터벌 값이 10인 스텝을 보여준다:

XML 구성

  <job id="sampleJob">
    <step id="step1">
      <tasklet>
        <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
      </tasklet>
    </step>
  </job>

다음 예는 tasklet이 자바에서 정의된 대로 커밋 인터벌 값이 10인 스텝를 보여준다:

자바 구성

  @Bean
  public Job sampleJob(JobRepository jobRepository) {
      return new JobBuilder("sampleJob", jobRepository)
                .start(step1())
                .build();
  }

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder("step1", jobRepository)
                  .<String, String>chunk(10, transactionManager)
                  .reader(itemReader())
                  .writer(itemWriter())
                  .build();
  }

앞의 예에서는, 각 트랜잭션 내에서 10개의 아이템이 처리된다. 처리 초기에, 트랜잭션이 시작된다. 또한 아이템리더(ItemReader)에서 read가 호출될 때마다 카운터가 증가한다. 10에 도달하면, 집계된 아이템 리스트가 아이템라이터(ItemWriter)로 전달되고 트랜잭션이 커밋된다.

5.1.4. Configuring a Step for Restart

“잡 구성 및 실행” 장에서, 잡 재시작에 대해 설명했다. 재시작은 스텝에 많은 영향을 미치므로, 일부 특정 구성이 필요할 수 있다.

Setting a Start Limit

스텝이 시작될 수 있는 횟수를 제어하려는 많은 상황이 있다. 예를 들어, 재실행하기 전에 수동으로 일부 리소스를 수정해야 하는 일부 스텝 때문에 한 번만 실행되도록 구성해야 할 수 있다. 스텝마다 요구 사항이 다를 수 있으므로, 스텝 레벨에서 구성할 수 있다. 한 번만 실행할 수 있는 스텝은 무한히 실행할 수 있는 스텝과 동일한 의 일부로 존재할 수 있다.

다음 코드 조각은 XML에서 시작 제한 구성의 예를 보여준다:

XML 구성

  <step id="step1">
    <tasklet start-limit="1">
      <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
    </tasklet>
  </step>

다음 코드 조각은 자바의 시작 제한 구성 예를 보여준다:

자바 구성

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder("step1", jobRepository)
                  .<String, String>chunk(10, transactionManager)
                  .reader(itemReader())
                  .writer(itemWriter())
                  .startLimit(1)
                  .build();
  }

위 예제의 스텝은 한 번만 실행할 수 있다. 재실행하려고 하면 스타트리미트익시드익셉션(StartLimitExceededException)이 발생한다. 시작 제한의 기본값은 Integer.MAX_VALUE이다.

Restarting a Completed Step

재시작 가능한 잡의 경우, 처음 성공 여부에 관계없이, 항상 실행해야 하는 하나 이상의 스텝이 있을 수 있다. 유효성 검사 스텝 또는 처리 전에 리소스를 정리하는 스텝를 예로 들 수 있다. 재시작된 잡이 정상적인 처리 중에 상태가 COMPLETED(이미 성공적으로 완료되었음을 의미)인 모든 스텝은 모두 건너뛴다.allow-start-if-completetrue로 설정하면 스텝이 항상 실행되도록 이를 오버라이드한다.

다음 코드 조각은 XML에서 재시작 가능한 잡을 정의하는 방법을 보여준다:

XML 구성

  <step id="step1">
    <tasklet allow-start-if-complete="true">
      <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
    </tasklet>
  </step>

다음 코드 조각은 자바에서 재시작 가능한 잡을 정의하는 방법을 보여준다:

자바 구성

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder("step1", jobRepository)
                  .<String, String>chunk(10, transactionManager)
                  .reader(itemReader())
                  .writer(itemWriter())
                  .allowStartIfComplete(true)
                  .build();
  }

Step Restart Configuration Example

다음 XML 예제는 재시작할 수 있는 스텝를 갖도록 잡을 구성하는 방법을 보여준다:

XML 구성

  <job id="footballJob" restartable="true">
    <step id="playerload" next="gameLoad">
      <tasklet>
        <chunk reader="playerFileItemReader" writer="playerWriter" commit-interval="10" />
      </tasklet>
    </step>
    <step id="gameLoad" next="playerSummarization">
      <tasklet allow-start-if-complete="true">
        <chunk reader="gameFileItemReader" writer="gameWriter" commit-interval="10"/>
      </tasklet>
    </step>
    <step id="playerSummarization">
      <tasklet start-limit="2">
        <chunk reader="playerSummarizationSource" writer="summaryWriter" commit-interval="10"/>
      </tasklet>
    </step>
  </job>

다음 자바 예제는 재시작할 수 있는 스텝를 갖도록 잡을 구성하는 방법을 보여준다:

자바 구성

  @Bean
  public Job footballJob(JobRepository jobRepository) {
    return new JobBuilder("footballJob", jobRepository)
              .start(playerLoad())
              .next(gameLoad())
              .next(playerSummarization())
              .build();
  }

  @Bean
  public Step playerLoad(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
    return new StepBuilder("playerLoad", jobRepository)
            .<String, String>chunk(10, transactionManager)
            .reader(playerFileItemReader())
            .writer(playerWriter())
            .build();
  }
  
  @Bean
  public Step gameLoad(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
    return new StepBuilder("gameLoad", jobRepository)
            .allowStartIfComplete(true)
            .<String, String>chunk(10, transactionManager)
            .reader(gameFileItemReader())
            .writer(gameWriter())
            .build();
  }

  @Bean
  public Step playerSummarization(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder("playerSummarization", jobRepository)
              .startLimit(2)
              .<String, String>chunk(10, transactionManager)
              .reader(playerSummarizationSource())
              .writer(summaryWriter())
              .build();
  }

앞의 예제는 축구 경기에 대한 정보를 로드하고 요약하는 잡에 대한 것이다. 여기에는 playerLoad, gameLoadplayerSummarization의 세가지 스텝이 포함된다. playerLoad 스텝은 플랫 파일에서 플레이어 정보를 로드하는 반면, gameLoad 스텝은 게임에 대해 동일한 작업을 수행한다. 마지막 단계인 playerSummarization은 제공된 게임을 기반으로 각 플레이어에 대한 통계를 요약한다. playerLoad에 의해 하나의 파일이 로드되어야 하지만 gameLoad는 특정 디렉토리 내에서 발견된 모든 게임파일을 로드할 수 있으며, 데이터베이스에 성공적으로 로드된 후에 파일을 삭제할 수 있다고 가정한다. 결과적으로 playerLoad 스텝에는 추가 구성이 포함되지 않는다. 완료 후 여러 번 스킵해도 된다. 그러나, gameLoad 스텝은 마지막 실행 이후 파일이 추가되면 매번 실행해야 한다. allow-start-if-complete가 항상 시작되도록 true로 설정되어 있다. (요약 스텝에서 새 게임을 제대로 찾을 수 있도록 게임이 로드되는 데이터베이스 테이블에 프로세스 표시기(indicator)가 있다고 가정한다.) 잡에서 가장 중요한 요약 스텝은, 시작 제한을 2로 설정한다. 이는 스텝이 계속해서 실패하면, 잡 익스큐션(job execution)을 제어하는 ​​운영자에게 새로운 종료 코드가 반환되고, 수동 개입이 발생할 때까지 재시작할 수 없기 때문에 유용하다.

이 잡은 이 문서에 대한 예제이며 샘플 프로젝트에 있는 footballJob과 동일하지 않다.

실행 1:

  1. playerLoad가 성공적으로 실행되고 완료되어, PLAYERS 테이블에 400명의 플레이어를 추가한다.
  2. gameLoad는 게임 데이터에 해당하는 11개 파일을 실행 및 처리하고, 해당 콘텐츠를 GAMES 테이블에 로드한다.
  3. playerSummarization이 처리를 시작하고 5분 후 실패한다.

실행 2:

  1. playerLoad는 이미 성공적으로 완료되었고, allow-start-if-completefalse(기본값)이므로 실행되지 않는다.
  2. gameLoad가 재실행되고 또 다른 2개의 파일을 처리하여, 콘텐츠도 GAMES 테이블에 로드(아직 처리되지 않았음을 나타내는 프로세스 표시기 포함)한다.
  3. playerSummarization은 나머지 모든 게임 데이터의 처리를 시작하고(프로세스 표시기를 사용하여 필터링) 30분 후에 다시 실패한다.

실행 3:

  1. playerLoad는 이미 성공적으로 완료되었고, allow-start-if-completefalse(기본값)이므로 실행되지 않는다.
  2. gameLoad가 재실행되고 또 다른 2개의 파일을 처리하여, 콘텐츠도 GAMES 테이블에 로드(아직 처리되지 않았음을 나타내는 프로세스 표시기 포함)한다.
  3. playerSummarization이 시작되지 않고 잡이 즉시 종료된다. 이는 playerSummarization의 세 번째 실행이고 제한이 2이기 때문이다. 제한을 높이거나 잡을 새 잡인스턴스(JobInstance)로 실행해야 한다.

5.1.5. Configuring Skip Logic

처리 중 발생한 오류로 스텝 실패가 발생하지 않고, 스킵해야 하는 상황이 많이 있다. 이것은 일반적으로 데이터 자체와 데이터의 의미를 이해하는 사람이 내릴 수 있는 결정이다. 예를 들어 금융 데이터는 완전히 정확해야 하는 송금으로 이어지기 때문에 스킵할 수 없다.

반면, 공급업체 목록을 로드하는 것은 스킵할 수 있다. 포맷이 잘못되었거나, 필요한 정보가 누락되어 공급업체가 로드되지 않은 경우, 문제가 없을 수 있다. 일반적으로, 이러한 잘못된 레코드도 기록되며 나중에 리스너(listener)에 대해 논의할 때 다룬다.

다음 XML 예제는 스킵 제한을 사용하는 예제를 보여준다:

XML 구성

  <step id="step1">
    <tasklet>
      <chunk reader="flatFileItemReader" writer="itemWriter" commit-interval="10" skip-limit="10">
        <skippable-exception-classes>
          <include class="org.springframework.batch.item.file.FlatFileParseException"/>
        </skippable-exception-classes>
      </chunk>
    </tasklet>
  </step>

다음 자바 예제는 스킵 제한을 사용하는 예제를 보여준다:

자바 구성

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder("step1", jobRepository)
                  .<String, String>chunk(10, transactionManager)
                  .reader(flatFileItemReader())
                  .writer(itemWriter())
                  .faultTolerant()
                  .skipLimit(10)
                  .skip(FlatFileParseException.class)
                  .build();
  }

위 예시에서, 플랫파일아이템리더(FlatFileItemReader)가 사용됐다. 어느 시점에서든, 플랫파일파스익셉션(FlatFileParseException)가 발생하면, 아이템을 스킵하고 전체 스킵 제한 10에서 계산이 된다. 선언된 예외(및 해당 하위 클래스)는 청크 처리(읽기(reader), 처리(process) 또는 쓰기(writer))의 모든 스텝에서 발생할 수 있다. 별도의 카운트는 스텝 익스큐션(step execution) 내부의 읽기, 처리 및 쓰기에 대한 스킵으로 구성되지만, 한도는 모든 스킵에 적용된다. 건너뛰기 제한에 도달하면 다음 예외로 스텝이 실패한다. 즉, 11번째 스킵이 예외를 트리거하지만 10번째 스킵은 트리거하지 않는다.

이전 예제의 한 가지 문제점은 플랫파일파스익셉션(FlatFileParseException) 이외의 다른 예외로 인해 잡이 실패한다는 것이다. 특정 상황에서는 이것이 올바른 동작일 수 있다. 그러나 다른 어떤 상황에서는 실패를 유발하는 예외를 식별하고 다른 모든 것을 스킵하는 것이 더 쉬울 수 있다.

다음 XML 예제는 특정 예외를 제외한 예제를 보여준다:

XML 구성

  <step id="step1">
    <tasklet>
      <chunk reader="flatFileItemReader" writer="itemWriter" commit-interval="10" skip-limit="10">
          <skippable-exception-classes>
              <include class="java.lang.Exception"/>
              <exclude class="java.io.FileNotFoundException"/>
          </skippable-exception-classes>
      </chunk>
    </tasklet>
  </step>

다음 자바 예제는 특정 예외를 제외한 예제를 보여준다:

자바 구성

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder("step1", jobRepository)
                  .<String, String>chunk(10, transactionManager)
                  .reader(flatFileItemReader())
                  .writer(itemWriter())
                  .faultTolerant()
                  .skipLimit(10)
                  .skip(Exception.class)
                  .noSkip(FileNotFoundException.class)
                  .build();
  }

java.lang.Exception을 스킵할 수 있는 예외 클래스로 식별함으로써 해당 구성은 모든 예외를 스킵할 수 있음을 나타낸다. 그러나 java.io.FileNotFoundException을 “제외(excluding)”함으로써 해당 구성은 스킵할 수 있는 예외 클래스 목록을 파일낫파운드익셉션(FileNotFoundException)을 제외한 모든 예외로 처리한다. 제외된 예외 클래스가 발생하는 것은 치명적(즉, 스킵하지 않음)이다. 발생한 모든 예외에 대해, 스킵 기능은 클래스 계층 구조에서 가장 가까운 슈퍼클래스에 의해 결정된다. 분류되지 않은 모든 예외는 ‘치명적(fatal)’으로 처리된다. include와 exclude를 지정하는 순서(XML 태그 또는 skip 및 noSkip 메소드 호출 사용)는 중요하지 않다.

5.1.6. Configuring Retry Logic

대부분의 경우, 스킵 또는 스텝 실패를 유발하는 예외면 충분하다. 그러나 모든 예외가 그렇게 결정된 것은 아니다. 읽는 동안 플랫파일파스익셉션(FlatFileParseException)이 발생하면 해당 레코드는 항상 예외를 발생시킨다. 아이템리더(ItemReader)를 재설정해도 도움이 되지 않는다. 그러나, 다른 예외(예: 현재 프로세스가 다른 프로세스가 보유하고 있는 락(lock)이 걸린 레코드를 업데이트하려고 시도했음을 나타내는 데드락루저데이터억세스익셉션(DeadlockLoserDataAccessException))의 경우, 기다렸다가 재시도하면 성공할 수 있다.

XML에서, 재시도는 다음과 같이 구성해야 한다:

  <step id="step1">
    <tasklet>
      <chunk reader="itemReader" writer="itemWriter" commit-interval="2" retry-limit="3">
        <retryable-exception-classes>
          <include class="org.springframework.dao.DeadlockLoserDataAccessException"/>
        </retryable-exception-classes>  
      </chunk> 
    </tasklet>
  </step>

자바에서, 재시도는 다음과 같이 구성해야 한다:

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder("step1", jobRepository)
                  .<String, String>chunk(2, transactionManager)
                  .reader(itemReader())
                  .writer(itemWriter())
                  .faultTolerant()
                  .retryLimit(3)
                  .retry(DeadlockLoserDataAccessException.class)
                  .build();
  }

스텝에서는 개별 아이템을 재시도할 수 있는 횟수 제한과 “재시도 가능한(retryable)” 예외 목록이 있다. 재시도 작동 방식에 대한 자세한 내용은 재시도(Retry)에서 확인할 수 있다.

5.1.7. Controlling Rollback

기본적으로, 재시도 또는 스킵에 관계없이 아이템라이터(ItemWriter)에서 예외가 발생하면 스텝에서 제어하는 ​​트랜잭션이 롤백된다. 앞서 설명한 대로 스킵를 구성하면, 아이템리더(ItemReader)에서 발생한 예외로 인해 롤백이 발생하지 않는다. 그러나, 트랜잭션을 무효화하기 위한 작업이 수행되지 않았기 때문에, 아이템라이터(ItemWriter)에서 발생한 예외로 인해 롤백이 발생하지 않는 많은 상황가 있다. 이러한 이유로, 롤백을 발생시키지 않아야 하는 예외 목록으로 스텝를 구성할 수 있다.

XML에서는, 다음과 같이 롤백을 제어할 수 있다:

XML 구성

  <step id="step1">
    <tasklet>
      <chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
      <no-rollback-exception-classes>
        <include class="org.springframework.batch.item.validator.ValidationException"/>
      </no-rollback-exception-classes>
    </tasklet>
  </step>

자바에서는, 다음과 같이 롤백을 제어할 수 있다:

자바 구성

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder("step1", jobRepository)
                  .<String, String>chunk(2, transactionManager)
                  .reader(itemReader())
                  .writer(itemWriter())
                  .faultTolerant()
                  .noRollback(ValidationException.class)
                  .build();
  }

Transactional Readers

아이템리더(ItemReader)의 기본적인 기능은 전달이다. 스텝에서 롤백이 발생할 경우, 리더(reader)에서 아이템을 다시 읽을 필요가 없도록 리더의 입력을 버퍼링한다. 그러나, 리더(reader)가 JMS 큐(queue)와 같이 트랜잭션 리소스 위에 구축되는 특정 상황이 있다. 이 경우, 롤백된 트랜잭션에 큐가 연결되어 있으므로, 리더는 큐에서 가져온 메시지를 다시 저장한다. 이러한 이유로, 아이템을 버퍼링하지 않도록 스텝를 구성해야 한다.

다음 예제는 XML에서 아이템을 버퍼링하지 않는 리더(reader)를 만드는 방법을 보여준다:

XML 구성

  <step id="step1">
    <tasklet>
        <chunk reader="itemReader" writer="itemWriter" commit-interval="2" is-reader-transactional-queue="true"/>
    </tasklet>
  </step>

다음 예제는 자바에서 아이템을 버퍼링하지 않는 리더(reader)를 만드는 방법을 보여준다:

자바 구성

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder("step1", jobRepository)
                  .<String, String>chunk(2, transactionManager)
                  .reader(itemReader())
                  .writer(itemWriter())
                  .readerIsTransactionalQueue()
                  .build();
  }

5.1.8. Transaction Attributes

트랜잭션 애트리뷰트를 사용하여 격리(isolation), 전파(propagation)제한 시간(timeout) 설정을 제어할 수 있다. 스프링 코어 문서에서 트랜잭션 속성 설정에 대한 자세한 정보를 찾을 수 있다.

다음 예는 XML에서 격리, 전파제한 시간을 트랜잭션 애트리뷰트로 설정한다:

XML 구성

  <step id="step1">
    <tasklet>
      <chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
      <transaction-attributes isolation="DEFAULT" propagation="REQUIRED" timeout="30"/>
    </tasklet>
  </step>

다음 예는 자바에서 격리, 전파제한 시간을 트랜잭션 애트리뷰트로 설정한다:

자바 구성

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
      attribute.setPropagationBehavior(Propagation.REQUIRED.value());
      attribute.setIsolationLevel(Isolation.DEFAULT.value());
      attribute.setTimeout(30);
      return new StepBuilder("step1", jobRepository)
                  .<String, String>chunk(2, transactionManager)
                  .reader(itemReader())
                  .writer(itemWriter())
                  .transactionAttribute(attribute)
                  .build();
  }

5.1.9. Registering ItemStream with a Step

스텝은 생명주기이 필요한 시점에서 아이템스트림(ItemStream)(아이템스트림(ItemStream) 인터페이스에 대한 자세한 내용은 아이템스트림(ItemStream)을 참고하자.)을 콜백을 처리해야 한다. 아이템스트림(ItemStream)인터페이스는 스텝 실행 사이의 지속 상태에 대해 필요한 정보를 가져오는 곳이기 때문에, 스텝이 실패하고 재시작해야 하는 경우 중요하다.

아이템리더(ItemReader), 아이템프로세서(ItemProcessor) 또는 아이템라이터(ItemWriter) 자체가 아이템스트림(ItemStream) 인터페이스를 구현하는 경우 자동으로 등록된다. 다른 모든 스트림은 별도로 등록해야 한다. 델리게이트와 같은, 간접 의존성이 리더와 라이터에, 주입되는 경우가 많다. 스트림(stream) 엘리먼트를 통해 스텝에 스트림을 등록할 수 있다.

다음 예는 XML의 스텝에서 스트림을 등록하는 방법을 보여준다:

XML 구성

  <step id="step1">
    <tasklet>
      <chunk reader="itemReader" writer="compositeWriter" commit-interval="2">
        <streams>
          <stream ref="fileItemWriter1"/>
          <stream ref="fileItemWriter2"/>
        </streams>
      </chunk>
    </tasklet>
  </step>

  <beans:bean id="compositeWriter" class="org.springframework.batch.item.support.CompositeItemWriter">
    <beans:property name="delegates">
      <beans:list>
        <beans:ref bean="fileItemWriter1" />
        <beans:ref bean="fileItemWriter2" />
      </beans:list>
    </beans:property>
  </beans:bean>

다음 예는 자바의 스텝에서 스트림을 등록하는 방법을 보여준다:

자바 구성

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
      return new StepBuilder("step1", jobRepository)
                  .<String, String>chunk(2, transactionManager)
                  .reader(itemReader())
                  .writer(compositeItemWriter())
                  .stream(fileItemWriter1())
                  .stream(fileItemWriter2())
                  .build();
  }

  /**
   * 스프링 배치 4에서, 컴포짓아이템라이터(CompositeItemWriter)는 아이템스트림(ItemStream)을 구현하므로 필요하지 않지만, 예제로 사용된다.
   */
  @Bean
  public CompositeItemWriter compositeItemWriter() {
      List<ItemWriter> writers = new ArrayList<>(2);
      writers.add(fileItemWriter1());
      writers.add(fileItemWriter2());
      CompositeItemWriter itemWriter = new CompositeItemWriter();
      itemWriter.setDelegates(writers);
      return itemWriter;
  }

앞의 예에서, 컴포짓아이템라이터(CompositeItemWriter)아이템스트림(ItemStream)이 아니지만, 두 객체 모두 델리게이트(delegate)는 아이템스트림(ItemStream)이다. 따라서, 두 델리게이트 라이터(delegate writers)는 프레임워크가 올바르게 처리할 수 있도록 스트림으로 명시적인 등록을 해야한다. 아이템리더(ItemReader)스텝의 직접적인 프로퍼티이므로 스트림으로 명시적인 등록을 할 필요가 없다. 이제 스텝를 재시작할 수 있으며, 오류 발생 시 리더(reader) 및 라이터(writer)의 상태가 올바르게 유지된다.

5.1.10. Intercepting Step Execution

과 마찬가지로, 스텝을 실행면서 일부 기능을 수행해야 하는 많은 이벤트가 있다. 예를 들어, 푸터(footer)를 플랫 파일에 작성하려면, 푸터(footer)를 쓸 수 있도록 스텝이 완료되었을 때 아이템라이터(ItemWriter)에 알려야 한다. 이는 많은 스텝 스코프 리스너(Step scoped listeners) 중 하나를 사용하여 수행할 수 있다.

스텝리스너(StepListener)의 확장(extension) 중 하나를 구현하는 클래스(비어 있는 인터페이스 자체는 아님)를 리스너(listeners) 엘리먼트를 통해 스텝에 적용할 수 있다. 리스너(listeners) 엘리먼트는 스텝, 태스크릿(tasklet) 또는 청크 선언 내에서 유효하다. 기능이 적용되는 레벨에 리스너를 선언하거나, 다기능인 경우(예: 스텝익스큐션리스너(StepExecutionListener)아이템리드리스너(ItemReadListener)) 적용되는 가장 세분화된 레벨에서 리스너를 선언하는 것이 좋다.

다음 예는 XML의 청크 레벨에 적용된 리스너를 보여준다:

XML 구성

  <step id="step1">
    <tasklet>
      <chunk reader="reader" writer="writer" commit-interval="10"/>
      <listeners>
        <listener ref="chunkListener"/>
      </listeners>
    </tasklet>
  </step>

다음 예는 자바의 청크 레벨에 적용된 리스너를 보여준다:

자바 구성

  @Bean
  public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
    return new StepBuilder("step1", jobRepository)
                .<String, String>chunk(10, transactionManager)
                .reader(reader())
                .writer(writer())
                .listener(chunkListener())
                .build();
  }

스텝리스너(StepListener) 인터페이스 중 하나를 구현하는 아이템리더(ItemReader), 아이템라이터(ItemWriter) 또는 아이템프로세서(ItemProcessor)는 네임스페이스 <step> 엘리먼트 또는 *StepFactoryBean 팩토리 중 하나를 사용하는 경우 스텝에 자동으로 등록된다. 이는 스텝에 직접 주입된 컴포넌트에만 적용된다. 리스너가 다른 컴포넌트 내에 중첩된 경우 (이전에 아이템스트림(ItemStream) 등록에서 설명한 대로)명시적으로 등록해야 한다.

스텝리스너(StepListener) 인터페이스 외에도, 동일한 문제를 해결하기 위해 어노테이션이 제공된다. 일반 자바 객체는 이러한 어노테이션이 포함된 메서드를 가질 수 있으며 해당 스텝리스너(StepListener) 타입으로 변환된다. 아이템리더(ItemReader), 아이템라이터(ItemWriter) 또는 태스크릿(Tasklet)과 같은 청크 컴포넌트의 커스텀 구현체에 어노테이션을 추가하는 것도 가능하다. 어노테이션은 <listener/> 엘리먼트에 대한 XML 파서에 의해 분석되고 빌더의 리스너 메소드에 등록되므로, XML 네임스페이스 또는 빌더를 사용하여 리스너를 스텝에 등록하기만 하면 된다.

StepExecutionListener

스텝익스큐션리스너(StepExecutionListener)는 스텝 익스큐션(Step execution)을 위한 가장 일반적인 리스너이다. 다음 예제와 같이 스텝이 시작되기 전과 끝난 후에, 정상적으로 종료되었는지 또는 실패했는지에 대한, 알림기능이다:

  public interface StepExecutionListener extends StepListener {
    void beforeStep(StepExecution stepExecution);
    ExitStatus afterStep(StepExecution stepExecution);
  }

엑시트스테이터스(ExitStatus)에는 리스너가 스텝 완료 시 반환하는 종료 코드를 수정할 수 있도록 afterStep 반환 타입이 있다.

이 인터페이스에 해당하는 어노테이션은 다음과 같다:

  • @BeforeStep
  • @AfterStep

ChunkListener

“청크(chunk)”는 트랜잭션 범위 내에서 처리되는 아이템이다. 커밋 인터벌(commit interval)마다 트랜잭션을 커밋하면 청크가 커밋된다. 다음 인터페이스 정의와 같이 청크리스너(ChunkListener)를 사용하여 청크 처리를 시작하기 전 또는 청크가 성공적으로 완료된 후의 로직을 수행할 수 있다:

  public interface ChunkListener extends StepListener {
    void beforeChunk(ChunkContext context);
    void afterChunk(ChunkContext context);
    void afterChunkError(ChunkContext context);
  }

beforeChunk 메서드는 트랜잭션이 시작된 후 아이템리더(ItemReader)에서 읽기가 시작되기 전에 호출된다. 반대로, afterChunk는 청크가 커밋(또는 롤백이 있는 경우 전혀 호출되지 않음)된 후 호출된다.

이 인터페이스에 해당하는 어노테이션은 다음과 같다:

  • @BeforeChunk
  • @AfterChunk
  • @AfterChunkError

청크 선언이 없을 때 청크리스너(ChunkListener)를 적용할 수 있다. 태스크릿스텝(TaskletStep)청크리스너(ChunkListener) 호출을 담당하므로, 아이템 지향이 아닌(non-item-oriented) 태스크릿(tasklet)에도 적용(tasklet 전후에 호출됨)된다.

ItemReadListener

이전에 스킵(skip) 로직을 논의할 때, 스킵된 레코드를 나중에 처리할 수 있도록 기록하는 것이 도움이 될 수 있다고 언급했다. 읽기 오류의 경우, 다음 인터페이스 정의와 같이 아이템리더리스너(ItemReaderListener)를 사용하여 이를 수행할 수 있다:

  public interface ItemReadListener<T> extends StepListener {
    void beforeRead();
    void afterRead(T item);
    void onReadError(Exception ex);
  }

beforeRead 메서드는 아이템리더(ItemReader)에서 각각의 아이템을 읽기 전에 호출된다. afterRead 메서드는 읽기의 성공 후 호출되며 읽은 아이템이 전달된다. 읽는 동안 오류가 발생하면, onReadError 메서드가 호출된다. 발생한 예외는 기록할 수 있도록 한다.

이 인터페이스에 해당하는 어노테이션은 다음과 같다:

  • @BeforeRead
  • @AfterRead
  • @OnReadError

ItemProcessListener

아이템리드리스너(ItemReadListener)와 마찬가지로, 아이템 처리(processing)는 다음 인터페이스 정의와 같이, “듣을(listened)” 수 있다:

  public interface ItemProcessListener<T, S> extends StepListener {
    void beforeProcess(T item);
    void afterProcess(T item, S result);
    void onProcessError(T item, Exception e);
  }

beforeProcess 메서드는 아이템프로세서(ItemProcessor)에서 처리하기 전에 호출되어 처리할 아이템을 전달받는다. afterProcess 메서드는 아이템이 성공적으로 처리된 후 호출된다. 처리 중 오류가 발생하면, onProcessError 메서드가 호출된다. 발생한 예외 및 처리를 시도한 아이템이, 기록될 수 있다. s 이 인터페이스에 해당하는 어노테이션은 다음과 같다:

  • @BeforeProcess
  • @AfterProcess
  • @OnProcessError

ItemWriteListener

다음 인터페이스 정의에서 볼 수 있듯이, 아이템라이터리스너(ItemWriteListener)를 사용하여 항목 쓰기를 “듣을(listened)” 수 있다:

  public interface ItemWriteListener<S> extends StepListener {
    void beforeWrite(List<? extends S> items);
    void afterWrite(List<? extends S> items);
    void onWriteError(Exception exception, List<? extends S> items);
  }

beforeWrite 메소드는 아이템라이터(ItemWriter)에 쓰기 전 호출되며 작성된 아이템 목록을 전달받는다. afterWrite 메서드는 아이템이 성공적으로 작성된 후 호출된다. 작성하는 동안 오류가 발생하면 onWriteError 메서드가 호출된다. 발생한 예외 및 작성을 시도한 항목이 기록될 수 있다.

이 인터페이스에 해당하는 어노테이션은 다음과 같다:

  • @BeforeWrite
  • @AfterWrite
  • @OnWriteError

SkipListener

아이템리드리스너(ItemReadListener), 아이템프로세스리스너(ItemProcessListener)아이템라이트리스너(ItemWriteListener)는 모두 오류를 알리는 메커니즘을 제공하지만, 실제로 레코드를 스킵(skip)했음을 알려주는 메커니즘은 없다. 예를 들어 onWriteError는 아이템이 재시도되고 성공한 경우에도 호출된다. 이러한 이유로 다음 인터페이스 정의와 같이 스킵한 아이템을 추적하기 위한 별도의 인터페이스가 있다:

  public interface SkipListener<T,S> extends StepListener {
    void onSkipInRead(Throwable t);
    void onSkipInProcess(T item, Throwable t);
    void onSkipInWrite(S item, Throwable t);
  }

onSkipInRead는 읽는 동안 아이템을 스킵할 때마다 호출된다. 롤백으로 인해 동일한 아이템이 두 번 이상 스킵한 것으로 등록될 수 있다. onSkipInWrite는 쓰는 동안 아이템을 스킵할 때 호출된다. 아이템을 성공적으로 읽었기 때문에(스킵하지 않았으므로), 아이템 자체도 아규먼트로 제공된다.

이 인터페이스에 해당하는 어노테이션은 다음과 같다:

  • @OnSkipInRead
  • @OnSkipInWrite
  • @OnSkipInProcess
SkipListeners and Transactions

스킵리스너(SkipListener)의 가장 일반적인 사례 중 하나는 스킵한 아이템을 로그 아웃하여, 다른 배치 프로세스 또는 인간이 직접 처리하여 스킵 문제를 진단하고 수정할 수 있도록 하는 것이다. 트랜잭션이 롤백되는 경우가 많기 때문에, 스프링 배치는 두 가지를 보장한다:

  • 적절한 스킵(skip) 메서드(오류 발생 시기에 따라 다름)는 아이템당 한 번만 호출된다.
  • 스킵리스너(SkipListener)는 항상 트랜잭션이 커밋되기 전에 호출된다. 이는 리스너가 호출하는 모든 트랜잭션 리소스가 아이템라이터(ItemWriter) 내 오류로 인해 롤백되지 않도록 하기 위한 것이다.

5.2. TaskletStep

스텝에서 청크 지향 처리가 유일한 방법은 아니다. 스텝이 저장 프로시저 호출로 구성되어야 하는 경우 어떻게 해야할까? 호출을 아이템리더(ItemReader)로 구현하고 절차가 완료된 후 null을 반환할 수 있다. 그러나, 그렇게 하는 것은 약간 부자연스러운데, 왜냐하면 무작동(no-op) 아이템라이터(ItemWriter)가 필요하기 때문이다. 스프링 배치는 이런 시나리오를 위해 태스크릿스텝(TaskletStep)을 제공한다.

태스크릿(Tasklet) 인터페이스에는 RepeatStatus.FINISHED를 반환하거나 예외를 발생시켜 실패 신호를 보낼 때까지, 태스크릿스텝(TaskletStep)에 의해 반복적으로 호출되는 execute라는 메서드가 하나 있다. 태스크릿(Tasklet)의 각 호출은 트랜잭션으로 래핑된다. 태스크릿(Tasklet) 구현체는 저장 프로시저, 스크립트 또는 SQL 업데이트 문을 호출할 수 있다.

태스크릿스텝(TaskletStep)을 생성하려면 스텝와 연결된 빈(네임스페이스를 사용할 때 ref 애트리뷰트을 사용며 자바 구성을 사용할 때 tasklet 메서드에 전달됨)은 태스크릿(Tasklet) 인터페이스를 구현해야 한다. 다음 예제는 간단한 tasklet을 보여준다:

XML 구성

  <step id="step1">
    <tasklet ref="myTasklet"/>
  </step>

자바 구성

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

스텝리스너(StepListener) 인터페이스를 구현하는 경우, 태스크릿스텝(TaskletStep)은 자동으로 tasklet을 스텝리스너(StepListener)로 등록한다.

5.2.1. TaskletAdapter

아이템리더(ItemReader)아이템라이터(ItemWriter) 인터페이스에 대한 다른 어댑터와 마찬가지로, 태스크릿(Tasklet) 인터페이스에는 기존 클래스에 적응할 수 있는 태스크릿어댑터(TaskletAdapter)와 같은 구현체가 포함되어 있다. 이것이 유용하게 사용되는 예시가 레코드 집합에서 플래그를 업데이트하는 데 사용되는 기존 DAO이다. 태스크릿(Tasklet) 인터페이스용 어댑터를 작성하지 않고도 태스크릿어댑터(TaskletAdapter)를 사용하여 이 클래스를 호출할 수 있다.

다음 예제는 XML에서 태스크릿어댑터(TaskletAdapter)를 정의하는 방법을 보여준다:

XML 구성

  <bean id="myTasklet" class="o.s.b.core.step.tasklet.MethodInvokingTaskletAdapter">
    <property name="targetObject">
      <bean class="org.mycompany.FooDao"/>
    </property>
    <property name="targetMethod" value="updateFoo" />
  </bean>

다음 예제는 자바에서 태스크릿어댑터(TaskletAdapter)를 정의하는 방법을 보여준다:

자바 구성

  @Bean
  public MethodInvokingTaskletAdapter myTasklet() {
    MethodInvokingTaskletAdapter adapter = new MethodInvokingTaskletAdapter();
    adapter.setTargetObject(fooDao());
    adapter.setTargetMethod("updateFoo");
    return adapter;
  }

5.2.2. Example Tasklet Implementation

많은 배치 잡에는 메인 처리가 시작되기 전, 다양한 리소스를 설정하기 위해 또는 처리가 완료된 후 해당 리소스를 정리하기 위해 수행해야 하는 스텝이 포함되어 있다. 파일 작업이 많은 잡의 경우, 특정 파일을 다른 위치에 성공적으로 업로드한 후 로컬에서 삭제해야 하는 경우가 많다. 다음 예제(스프링 배치 샘플 프로젝트에서 가져옴)는 그러한 책임이 있는 태스크릿(Tasklet) 구현체이다:

  public class FileDeletingTasklet implements Tasklet, InitializingBean {
    private Resource directory;

    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
      File dir = directory.getFile();
      Assert.state(dir.isDirectory());
      File[] files = dir.listFiles();
      for (int i = 0; i < files.length; i++) {
        boolean deleted = files[i].delete();
        if (!deleted) {
          throw new UnexpectedJobExecutionException("Could not delete file " + files[i].getPath());
        }
      }
      return RepeatStatus.FINISHED;
    }

    public void setDirectoryResource(Resource directory) {
      this.directory = directory;
    }

    public void afterPropertiesSet() throws Exception {
      Assert.state(directory != null, "directory must be set");
    } 
  }

위의 태스크릿(tasklet) 구현체는 지정된 디렉토리 내의 모든 파일을 삭제한다. execute 메서드는 한 번만 호출된다는 점에 유의해야 한다. 남은 것은 스텝에서 태스크릿(tasklet)을 참조하는 것이다.

다음 예는 XML의 스텝에서 태스크릿(tasklet)을 참조하는 방법을 보여준다:

XML 구성

  <job id="taskletJob">
    <step id="deleteFilesInDir">
      <tasklet ref="fileDeletingTasklet"/>
    </step>
  </job>

  <beans:bean id="fileDeletingTasklet" class="org.springframework.batch.sample.tasklet.FileDeletingTasklet">
    <beans:property name="directoryResource">
      <beans:bean id="directory" class="org.springframework.core.io.FileSystemResource">
        <beans:constructor-arg value="target/test-outputs/test-dir" />
      </beans:bean>
    </beans:property>
  </beans:bean>

다음 예는 자바의 스텝에서 태스크릿(tasklet)을 참조하는 방법을 보여준다:

자바 구성

  @Bean
  public Job taskletJob(JobRepository jobRepository) {
    return new JobBuilder("taskletJob", jobRepository)
            .start(deleteFilesInDir())
            .build();
  }

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

  @Bean
  public FileDeletingTasklet fileDeletingTasklet() {
    FileDeletingTasklet tasklet = new FileDeletingTasklet();
    tasklet.setDirectoryResource(new FileSystemResource("target/test-outputs/test-dir"));
    return tasklet;
  }

5.3. Controlling Step Flow

소유한 잡 내에서 스텝을 함께 그룹화할 수 있으므로 잡이 한 스텝에서 다른 스텝로 “플로우(flow)” 방식을 제어할 수 있어야 한다. 스텝의 실패가 의 실패를 의미하지는 않는다. 또한 다음에 실행해야 하는 스텝를 결정하는 “성공(“success”)” 타입이 두 가지 이상 있을 수 있다. 스텝 그룹이 구성되는 방식에 따라 특정 스텝이 전혀 처리되지 않을 수도 있다.

5.3.1. Sequential Flow

가장 간단한 플로우(flow) 시나리오는 다음 이미지와 같이, 모든 스텝이 순차적으로 실행되는 잡이다:

연속적인 흐름

이미지 16. 연속적인 흐름

이는 스텝에서 next를 사용하여 달성할 수 있다.

다음 예는 XML에서 next 애트리뷰트를 사용하는 방법을 보여준다:

XML 구성

  <job id="job">
    <step id="stepA" parent="s1" next="stepB" />
    <step id="stepB" parent="s2" next="stepC"/>
    <step id="stepC" parent="s3" />
  </job>

다음 예제는 자바에서 next() 메소드를 사용하는 방법을 보여준다:

자바 구성

  @Bean
  public Job job(JobRepository jobRepository) {
      return new JobBuilder("job", jobRepository)
                  .start(stepA())
                  .next(stepB())
                  .next(stepC())
                  .build();
  }

위의 시나리오에서, stepA는 나열된 첫 번째 스텝이므로 먼저 실행된다. stepA가 정상적으로 완료되면, stepB가 실행된다. 그러나 stepA가 실패하면 전체 잡이 실패하고 stepB는 실행되지 않는다.

스프링 배치 XML 네임스페이스를 사용하면, 은 구성에 나열된 순서대로 첫 번째 스텝을 실행한다. 다른 스텝 엘리먼트의 순서는 중요하지 않지만, 첫 번째 스텝은 항상 XML에서 첫 번째로 나타나야 한다.

5.3.2. Conditional Flow

앞의 예에서는, 두 가지 결과만 있다:

  1. 스텝이 성공했고, 다음 스텝을 실행한다.
  2. 스텝이 실패했으므로 이 실패한다.

대부분, 이 정도면 충분하다. 그러나, 스텝이 실패하는 시나리오는 어떤가? 실패하지 않고 다른 스텝을 실행해야 하면? 다음 이미지는 이러한 흐름을 보여준다.

이미지 17. 조건부 흐름

보다 복잡한 시나리오를 처리하기 위해, 스프링 배치 XML 네임스페이스를 사용하면 스텝(step) 엘리먼트 내에서 트랜지션 엘리먼트(transitions elements)를 정의할 수 있다. 이러한 트랜지션 중 하나는 next 엘리먼트이다. next 애트리뷰트와 마찬가지로, next 엘리먼트는 다음에 실행할 스텝에 알려준다. 그러나, 애트리뷰트와 달리, 지정된 스텝에서 여러 개의 next 엘리먼트가 허용되며, 실패 시 기본 동작은 없다. 즉, 트랜지션 엘리먼트를 사용하는 경우 스텝 트랜지션에 대한 모든 동작을 명시적으로 정의해야 한다. 또한 하나의 스텝은 next 애트리뷰트와 트랜지션 엘리먼트를 모두 가질 수 없다.

next 엘리먼트는 다음 예제와 같이 일치시킬 패턴과 다음에 실행할 스텝를 지정한다:

XML 구성

  <job id="job">
    <step id="stepA" parent="s1">
      <next on="*" to="stepB" />
      <next on="FAILED" to="stepC" />
    </step>
    <step id="stepB" parent="s2" next="stepC" />
    <step id="stepC" parent="s3" />
  </job>

자바 API는 흐름을 지정하고 스텝이 실패할 때 수행할 작업을 지정할 수 있는 메서드 집합을 제공한다. 다음 예에서는 한 스텝(stepA)를 지정한 다음 stepA의 성공 여부에 따라 서로 다른 두 스텝(stepB 또는 stepC) 중 하나로 진행하는 방법을 보여준다:

자바 구성

  @Bean
  public Job job(JobRepository jobRepository) {
      return new JobBuilder("job", jobRepository)
                  .start(stepA())
                  .on("*").to(stepB())
                  .from(stepA()).on("FAILED").to(stepC())
                  .end()
                  .build();
  }

XML 구성을 사용하는 경우, 트랜지션(transition) 엘리먼트의 on 애드리뷰트는 간단한 패턴 일치 체계를 사용하여 스텝의 실행 결과인 엑시트스태이터스(ExitStatus)와 매칭한다.

자바 구성을 사용하는 경우, on() 메서드는 간단한 패턴 일치 체계를 사용하여 스텝 실행 결과인 엑시트스태이터스(ExitStatus)와 매칭한다.

패턴에는 두 개의 특수 문자만 허용된다:

  • * 0개 이상의 문자와 일치
  • ? 정확히 하나의 문자와 일치

예를 들어 c*tcatcount와 일치하는 반면 c?tcat과 일치하지만 count와는 일치하지 않다.

스텝의 트렌지션 엘리먼트(transition element) 수에는 제한이 없지만, 스텝 실행 결과 엘리먼트에 포함되지 않는 엑시트스태이터스(ExitStatus)가 발생하면 프레임워크에서 예외가 발생하고 잡이 실패한다. 프레임워크는 자동으로 가장 구체적인 것에서 가장 덜 구체적인 것으로의 트렌지션 엘리먼트(transition element)를 지정한다. 즉, 앞의 예에서 stepA의 순서가 바뀌더라도 FAILED엑시트스태이터스(ExitStatus)는 여전히 stepC로 이동한다.

Batch Status Versus Exit Status

조건부 흐름에 대한 잡을 구성할 때, 배치스테이터스(BatchStatus)엑시트스테이터스(ExitStatus)의 차이점을 이해하는 것이 중요하다. 배치스테이터스(BatchStatus)잡익스큐션(JobExecution)스텝익스큐션(StepExecution)의 프로퍼티인 열거형(enumeration)이며 프레임워크에서 잡 또는 스텝의 상태를 기록하는 데 사용된다. COMPLETED, STARTING, STARTED, STOPPING, STOPPED, FAILED, ABANDONED 또는 UNKNOWN 값 중 하나 이다. 대부분은 설명이 필요 없다. COMPLETED는 스텝 또는 잡이 성공적으로 완료되었을 때, 설정되는 상태이고 FAILED는 실패할 때 설정된다.

다음 예는 XML 구성을 사용할 때 next 엘리먼트를 포함하는 것을 보여준다:

<next on="FAILED" to="stepB" />

다음 예제에는 자바 구성을 사용할 때 on 엘리먼트를 포함하는 것을 보여준다:

  ...
  .from(stepA()).on("FAILED").to(stepB())
  ...

얼핏 보면, on은 자신이 속한 스텝배치스테이터스(BatchStatus)를 확인하는 것처럼 보인다. 그러나, 실제로는 스텝엑시트스태이터스(ExitStatus)를 확인한다. 이름에서 알 수 있듯이, 엑시트스태이터스(ExitStatus)는 실행이 완료된 후 스텝의 상태를 나타낸다.

보다 구체적으로, XML 구성을 사용하는 경우, 이전 XML 구성 예제에 표시된 next 엘리먼트는 엑시트스태이터스(ExitStatus)의 종료 코드를 확인한다.

자바 구성을 사용하는 경우, 앞의 자바 구성 예제에 표시된 on() 메서드는 엑시트스태이터스(ExitStatus)의 종료 코드를 확인한다.

영어로는, “종료 코드가 FAILED인 경우 stepB로 이동”이라고 되어 있다. 기본적으로, 종료 코드는 항상 스텝의 배치스테이터스(BatchStatus)와 동일하므로, 이전과 동일하게 작동한다.

그러나, 종료 코드가 달라야 하는 경우에는 어떻게 해야할까? 좋은 예시는 샘플 프로젝트의 skip sample job에서 나온다:

다음 예는 XML에서 다른 종료 코드로 작업하는 방법을 보여준다:

XML 구성

  <step id="step1" parent="s1">
      <end on="FAILED" />
      <next on="COMPLETED WITH SKIPS" to="errorPrint1" />
      <next on="*" to="step2" />
  </step>

다음 예는 자바에서 다른 종료 코드로 작업하는 방법을 보여준다:

자바 구성

  @Bean
  public Job job(JobRepository jobRepository) {
      return new JobBuilder("job", jobRepository)
              .start(step1()).on("FAILED").end()
              .from(step1()).on("COMPLETED WITH SKIPS").to(errorPrint1())
              .from(step1()).on("*").to(step2())
              .end()
              .build();
  }

step1에는 세 가지 결과가 있다:

  • 스텝이 실패했다. 이 경우 잡이 실패해야 한다.
  • 스텝이 성공적으로 완료되었다.
  • 스텝이 성공적으로 완료되었지만 종료 코드는 COMPLETED WITH SKIPS이다. 이 경우 오류를 처리하기 위해 다른 스텝를 실행해야 한다.

이전 구성은 잘 작동한다. 그러나 다음 예제와 같이 건너뛴(skip) 레코드가 있는 실행 조건에 따라 종료 코드를 변경해야 한다:

  public class SkipCheckingListener extends StepExecutionListenerSupport {
    public ExitStatus afterStep(StepExecution stepExecution) {

      String exitCode = stepExecution.getExitStatus().getExitCode();

      if (!exitCode.equals(ExitStatus.FAILED.getExitCode()) && stepExecution.getSkipCount() > 0) {
        return new ExitStatus("COMPLETED WITH SKIPS");
      } else {
        return null;
      }
    }
  }

앞의 코드는 먼저 스텝이 성공했는지 확인한 다음 스텝익스큐션(StepExecution)의 건너뛰기(skip) 횟수가 0보다 큰지 확인하는 스텝익스큐션리스너(StepExecutionListener)이다. 두 조건이 모두 충족되면 종료 코드 COMPLETED WITH SKIPS인 새 엑시트스테이터스(ExitStatus)가 반환된다.

5.3.3. Configuring for Stop

배치스테이터스(BatchStatus)엑시트스테이터스(ExitStatus)에 대해 논의 후, 잡에 대한 배치스테이터스(BatchStatus)엑시트스테이터스(ExitStatus)가 어떻게 결정되는지 궁금할 수 있다. 스텝의 상태(status)는 실행되는 코드에 따라 결정되지만, 잡에 대한 상태는 구성에 따라 결정된다.

지금까지, 논의한 모든 잡 구성에는 트랜지션(transition)이 없는 최종 스텝이 하나 이상 있었다.

다음 XML 예제에서는, 스텝이 실행된 후 이 종료된다:

  <step id="stepC" parent="s3"/>

다음 자바 예제에서는, 스텝이 실행된 후 이 종료된다:

  @Bean
  public Job job(JobRepository jobRepository) {
      return new JobBuilder("job", jobRepository)
                  .start(step1())
                  .build();
  }

스텝에 대해 정의된 트랜지션이 없는 경우 잡의 상태는 다음과 같이 정의된다:

  • 스텝이 엑시트스테이터스(ExitStatus)FAILED로 끝나는 경우 배치스테이터스(BatchStatus)엑시트스테이터스(ExitStatus)는 모두 FAILED이다.
  • 그렇지 않으면, 잡의 배치스테이터스(BatchStatus)엑시트스테이터스(ExitStatus)가 모두 COMPLETED이다.

배치 잡을 종료하는 이 방법은 간단한 순차적인 스텝 잡과 같은, 일부 배치 잡에 충분하지만, 커스텀한 잡 중지 시나리오가 필요할 수 있다. 이를 위해 스프링 배치는 을 중지하기 위한 세 가지 트랜지션(transition) 엘리먼트(이전에 논의한 next 엘리먼트 외에도)를 제공한다. 이러한 각 중지 엘리먼트는 특정 배치스테이터스(BatchStatus)가 있는 을 중지한다. 중지 트랜지션 엘리먼트는 내 모든 스텝배치스테이터스(BatchStatus) 또는 엑시트스테이터스(ExitStatus)에 영향을 미치지 않는다는 점에 유의해야 한다. 이러한 엘리먼트는 의 최종 상태에만 영향준다. 예를 들어 잡의 모든 스텝이 FAILED 상태일 수 있지만 잡의 상태는 COMPLETED일 수 있다.

Ending at a Step

스텝을 종료하면 배치스테이터스(BatchStatus)COMPLETED로 잡이 중지된다. COMPLETED 상태로 완료된 잡은 재시작할 수 없다(프레임워크에서 잡인스턴스올레디컴플릿익셉션(JobInstanceAlreadyCompleteException) 발생).

XML 구성을 사용하는 경우, 이 작업에 end 엘리먼트를 사용할 수 있다. end 엘리먼트는 잡의 엑시트스테이터스(ExitStatus)를 커스텀하는 데 사용할 수 있는 옵셔널한 exit-code 애트리뷰트도 허용한다. exit-code 애트리뷰트가 지정되지 않은 경우, 배치스테이터스(BatchStatus)와 일치하도록 엑시트스테이(ExitStatus)는 기본적으로, COMPLETED이다.

자바 구성을 사용하는 경우, 이 작업에 end 메서드를 사용할 수 있다. end 메서드는 잡의 엑시트스테이터스(ExitStatus)를 커스텀하는 데 사용할 수 있는 옵셔널한 엑시트스테이터스(exitStatus) 파라미터도 허용한다. 엑시트스테이터스(exitStatus) 값이 제공되지 않으면, 배치스테이터스(BatchStatus)와 일치하도록 엑시트스테이터스(ExitStatus)는 기본적으로 COMPLETED이다.

다음 시나리오를 생각해보자: step2가 실패하면 잡은 배치스테이터스(BatchStatus)COMPLETED이고 엑시트스테이터스(ExitStatus)COMPLETED로 중지되고 step3은 실행되지 않는다. 그렇지 않으면, step3이 실행된다. step2가 실패하면 잡을 (상태가 COMPLETED이므로)재시작할 수 없다.

다음 예는 XML의 시나리오를 보여준다:

  <step id="step1" parent="s1" next="step2">
  <step id="step2" parent="s2">
      <end on="FAILED"/>
      <next on="*" to="step3"/>
  </step>
  <step id="step3" parent="s3">

다음 예는 자바의 시나리오를 보여준다:

  @Bean
  public Job job(JobRepository jobRepository) {
    return new JobBuilder("job", jobRepository)
            .start(step1())
            .next(step2())
            .on("FAILED").end()
            .from(step2()).on("*").to(step3())
            .end()
            .build();
  }

Failing a Step

지정된 지점에서 실패하도록 스텝를 구성하면 배치스테이터스(BatchStatus_FAILED인 상태로 잡이 중지된다. 종료와 달리, 잡이 실패해도 잡이 재시작이 가능하다.

XML로 구성하는 경우, 페일(fail) 엘리먼트는 잡의 엑시트스테이터스(ExitStatus)를 커스텀하는 데 사용할 수 있는 옵셔널한 exit-code 애트리뷰트도 허용한다. exit-code 애트리뷰트가 지정되지 않았을 경우, 배치스테이터스(BatchStatus)와 일치하도록 엑시트스테이터스(ExitStatus)는 기본적으로 FAILED이다.

다음 시나리오를 생각해보자: step2가 실패하면, 배치스테이터스(BatchStatus)FAILED이고 엑시트스테이터스(ExitStatus)얼리 터미네이션(EARLY TERMINATION)인 잡이 중지되고 step3가 실행되지 않는다. 그렇지 않으면, step3이 실행된다. 또한, step2가 실패면 step2부터 잡이 재시작된다.

다음 예는 XML의 시나리오를 보여준다:

XML 구성

  <step id="step1" parent="s1" next="step2">
  <step id="step2" parent="s2">
    <fail on="FAILED" exit-code="EARLY TERMINATION"/>
    <next on="*" to="step3"/>
  </step>
  <step id="step3" parent="s3">

다음 예는 자바의 시나리오를 보여준다:

자바 구성

  @Bean
  public Job job(JobRepository jobRepository) {
    return new JobBuilder("job", jobRepository)
            .start(step1())
            .next(step2()).on("FAILED").fail()
            .from(step2()).on("*").to(step3())
            .end()
            .build();
  }

Stopping a Job at a Given Step

특정 스텝에서 중지하도록 잡을 구성하면 배치스테이터스(BatchStatus)STOPPED인 상태로 잡이 중지된다. 잡의 처리를 일시적으로 중단하면 운영자가 잡을 재시작하기 전에 조치를 취할 수 있다.

XML로 구성하는 경우, stop 엘리먼트에는 이 재시작될 때 실행해야 하는 스텝을 선택할 수 있는 restart 애트리뷰트가 필요하다.

다음 시나리오를 고려해보자: step1COMPLETE로 완료되면, 잡이 중지된다. 재시작하면, step2에서 시작된다.

다음 목록은 XML로 된 시나리오를 보여준다:

  <step id="step1" parent="s1">
    <stop on="COMPLETED" restart="step2"/>
  </step>
  <step id="step2" parent="s2"/>

다음 목록은 자바로 된 시나리오를 보여준다:

  @Bean
  public Job job(JobRepository jobRepository) {
    return new JobBuilder("job", jobRepository)
            .start(step1()).on("COMPLETED").stopAndRestart(step2())
            .end()
            .build();
  }

5.3.4. Programmatic Flow Decisions

경우에 따라, 다음에 실행할 스텝를 결정하기 위해 엑시트스테이터스(ExitStatus)보다 더 많은 정보가 필요할 수 있다. 이 경우, 다음 예제와 같이, 잡익스큐션디사이더(JobExecutionDecider)를 사용하여 결정할 수 있다:

  public class MyDecider implements JobExecutionDecider {
    public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
      String status;
      if (someCondition()) {
        status = "FAILED";
      } else {
        status = "COMPLETED";
      }
      return new FlowExecutionStatus(status);
    }
  }

다음 샘플 잡 구성에서, decision은 사용할 decider와 모든 트랜지션(transition)을 지정한다: XML 구성

  <job id="job">
    <step id="step1" parent="s1" next="decision" />

    <decision id="decision" decider="decider">
      <next on="FAILED" to="step2" />
      <next on="COMPLETED" to="step3" />
    </decision>

    <step id="step2" parent="s2" next="step3"/>
    <step id="step3" parent="s3" />
  </job>

  <beans:bean id="decider" class="com.MyDecider"/>

다음 예제에서, 자바를 구성으로 사용할 때 잡익스큐션디사이더(JobExecutionDecider)를 구현한 빈은 호출 시 직접 전달한다: 자바 구성

  @Bean
  public Job job(JobRepository jobRepository) {
      return new JobBuilder("job", jobRepository)
              .start(step1())
              .next(decider()).on("FAILED").to(step2())
              .from(decider()).on("COMPLETED").to(step3())
              .end()
              .build();
  }

5.3.5. Split Flows

지금까지 설명한 모든 시나리오에는 순차적으로 한 번에 한 스텝씩 실행하는 잡이었다. 이러한 일반적인 스타일 외에도 스프링 배치는 을 병렬 흐름으로 구성할 수 있다.

XML 네임스페이스를 사용하면 split 엘리먼트를 사용할 수 있다. 다음 예에서 볼 수 있듯이, split 엘리먼트에는 전체 개별 흐름을 정의할 수 있는 하나 이상의 flow 엘리먼트가 포함되어 있다. split 엘리먼트에는 next 애트리뷰트나 next, end 또는 fail 엘리먼트와 같이 이전에 논의된 트랜지션 엘리먼트가 포함될 수도 있다.

  <split id="split1" next="step4">
    <flow>
      <step id="step1" parent="s1" next="step2"/>
      <step id="step2" parent="s2"/>
    </flow>
    <flow>
      <step id="step3" parent="s3"/>
    </flow>
  </split>
  
  <step id="step4" parent="s4"/>

자바로 구성하면 제공된 빌더(builder)를 통해 split 을 구성할 수 있다. 다음 예에서 볼 수 있듯이, split 엘리먼트에는 전체 개별 흐름을 정의할 수 있는 하나 이상의 flow 엘리먼트가 포함되어 있다. split 엘리먼트에는 next 애트리뷰트나 next, end 또는 fail 엘리먼트와 같이 이전에 논의된 트랜지션(transition) 엘리먼트가 포함될 수도 있다.

  @Bean
  public Flow flow1() {
    return new FlowBuilder<SimpleFlow>("flow1")
            .start(step1())
            .next(step2())
            .build();
  }

  @Bean
  public Flow flow2() {
    return new FlowBuilder<SimpleFlow>("flow2")
            .start(step3())
            .build();
  }

  @Bean
  public Job job(Flow flow1, Flow flow2) {
      return this.jobBuilderFactory.get("job")
                .start(flow1)
                .split(new SimpleAsyncTaskExecutor())
                .add(flow2)
                .next(step4())
                .end()
                .build();
  }

5.3.6. Externalizing Flow Definitions and Dependencies Between Jobs

잡 플로우의 일부는 별도의 빈 정의로 외부화한 다음 재사용할 수 있다. 두 가지 방법이 있다. 첫 번째는 플로우(flow)을 다른 곳에서 정의된 플로우(flow)에 대해 참조하는 것이다.

다음 XML 예제는 플로우(flow)을 다른 곳에서 정의된 흐름에 대한 참조하는 방법을 보여준다:

XML 구성

  <job id="job">
    <flow id="job1.flow1" parent="flow1" next="step3"/>
    <step id="step3" parent="s3"/>
  </job>
  <flow id="flow1">
    <step id="step1" parent="s1" next="step2"/>
    <step id="step2" parent="s2"/>
  </flow>

다음 자바 예제는 플로우(flow)을 다른 곳에서 정의된 흐름에 대한 참조하는 방법을 보여준다:

자바 구성

  @Bean
  public Job job(JobRepository jobRepository) {
    return new JobBuilder("job", jobRepository)
            .start(flow1())
            .next(step3())
            .end()
            .build();
  }

  @Bean
  public Flow flow1() {
    return new FlowBuilder<SimpleFlow>("flow1")
            .start(step1())
            .next(step2())
            .build();
  }

앞의 예에서와 같이, 외부 플로우(flow)을 정의하는 효과는, 외부 플로우의 스텝를 인라인으로 선언된 것처럼 잡에 넣을 수 있다는 것이다. 이러한 방식으로, 많은 잡이 동일한 템플릿 플로우을 참조하고 이러한 템플릿을 서로 다른 논리적 플로우로 구성할 수 있다.

외부화 플로우의 다른 형태는 잡스텝(JobStep)을 사용하는 것이다. 잡스텝(JobStep)플로우스텝(FlowStep)과 유사하지만 실제로 지정된 플로우 스텝에 대해 별도의 잡 실행을 생성하고 시작한다.

다음 예제는 XML의 잡스텝(JobStep) 예제를 보여준다: XML 구성

 <job id="jobStepJob" restartable="true">
    <step id="jobStepJob.step1">
      <job ref="job" job-launcher="jobLauncher" job-parameters-extractor="jobParametersExtractor"/>
    </step>
  </job>

  <job id="job" restartable="true">...</job>

  <bean id="jobParametersExtractor" class="org.spr...DefaultJobParametersExtractor">
    <property name="keys" value="input.file"/>
  </bean>

다음 예제는 자바의 잡스텝(JobStep) 예제를 보여준다: 자바 구성

  @Bean
  public Job jobStepJob(JobRepository jobRepository) {
    return new JobBuilder("jobStepJob", jobRepository)
            .start(jobStepJobStep1(null))
            .build();
  }

  @Bean
  public Step jobStepJobStep1(JobLauncher jobLauncher, JobRepository jobRepository) {
    return new StepBuilder("jobStepJobStep1", jobRepository)
            .job(job())
            .launcher(jobLauncher)
            .parametersExtractor(jobParametersExtractor())
            .build();
  }

  @Bean
  public Job job(JobRepository jobRepository) {
    return new JobBuilder("job", jobRepository)
            .start(step1())
            .build(); 
  }
  
  @Bean
  public DefaultJobParametersExtractor jobParametersExtractor() {
      DefaultJobParametersExtractor extractor = new DefaultJobParametersExtractor();

      extractor.setKeys(new String[]{"input.file"});

      return extractor;
  }

잡 파라미터 추출기(extractor)는 스텝익스큐션컨텍스트(ExecutionContext)가 실행되는 잡파라미터(JobParameter)로 변환하는 방법을 결정하는 전략이다. 잡스텝은 잡 및 스텝을 모니터링하고 리포팅하기 위한 좀 더 세분화된 옵션을 원할 때 유용하다. 잡스텝을 사용하면 “잡 간 의존성을 어떻게 생성할까?” 큰 시스템을 더 작은 모듈로 나누고 잡의 흐름을 제어하는 ​​좋은 방법이다.

5.4. Late Binding of Job and Step Attributes

이전 XML 및 플랫 파일(flat file) 예제는 파일을 얻기 위해 스프링 리소스(Resource) 추상화를 사용했다. 이는 리소스(Resource)java.io.File을 리턴하는 getFile 메소드가 있기 때문에 작동한다. 표준 스프링 구성을 사용하여 XML 및 플랫 파일 리소스를 모두 구성할 수 있다:

다음 예에서는 XML의 레이트(late) 바인딩을 보여준다:

XML 구성

  <bean id="flatFileItemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
      <property name="resource" value="file://outputs/file.txt" />
  </bean>

다음 예에서는 자바의 레이트 바인딩(late binding)을 보여준다:

자바 구성

  @Bean
  public FlatFileItemReader flatFileItemReader() {
      FlatFileItemReader<Foo> reader = new FlatFileItemReaderBuilder<Foo>()
              .name("flatFileItemReader")
              .resource(new FileSystemResource("file://outputs/file.txt"))
              ...
  }

앞의 리소스(Resource)는 지정된 경로에서 파일을 로드한다. 절대 경로는 이중 슬래시(//)로 시작해야 한다. 대부분의 스프링 애플리케이션에서, 이러한 리소스명은 컴파일 시간에 알려지기 때문에, 이정도면 충분하다. 그러나 배치에서 파일 이름은 런타임에 잡에 대한 파라미터로 지정될 수도 있다. 이것은 시스템 프로퍼티을 읽기 위해 -D 파라미터를 사용하여 해결할 수 있다.

다음 예에서는 XML의 프로퍼티에서 파일 이름을 읽는 방법을 보여준다: XML 구성

  <bean id="flatFileItemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
    <property name="resource" value="${input.file.name}" />
  </bean>

다음 예에서는 자바의 프로퍼티에서 파일 이름을 읽는 방법을 보여준다: 자바 구성

  @Bean
  public FlatFileItemReader flatFileItemReader(@Value("${input.file.name}") String name) {
      return new FlatFileItemReaderBuilder<Foo>()
              .name("flatFileItemReader")
              .resource(new FileSystemResource(name))
              ...
  }

이 솔루션이 작동하는 데 필요한 모든 것은 시스템 아규먼트(예: -Dinput.file.name="file://outputs/file.txt")이다.

여기에서 PropertyPlaceholderConfigurer를 사용할 수 있지만, 스프링의 리소스에디터(ResourceEditor)가 이미 시스템 프로퍼티스를 필터링하고 자리 표시자(placeholder) 대체하기 때문에 시스템 프로퍼티스가 항상 설정되어 있으면 필요하지 않다.

종종, 배치 설정에서는, 잡의 잡파라미터(JobParameter)에서 파일 이름을 파라미터화(시스템 프로퍼티스를 통하지 않고)하는 방식으로 접근하는 것이 좋다. 이를 달성하기 위해, 스프링 배치는 다양한 스텝 애트리뷰트의 레이트 바인딩(late binding)을 허용한다.

다음 예는 XML에서 파일명을 파라미터화하는 방법을 보여준다: XML 구성

  <bean id="flatFileItemReader" scope="step" class="org.springframework.batch.item.file.FlatFileItemReader">
    <property name="resource" value="#{jobParameters['input.file.name']}" />
  </bean>

다음 예는 자바에서 파일명을 파라미터화하는 방법을 보여준다: 자바 구성

  @StepScope
  @Bean
  public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters['input.file.name']}") String name) {
      return new FlatFileItemReaderBuilder<Foo>()
              .name("flatFileItemReader")
              .resource(new FileSystemResource(name))
              ...
  }

동일한 방식으로 잡익스큐션(JobExecution)스텝익스큐션(StepExecution) 레벨 및 익스큐션컨텍스트(ExecutionContext)에 모두 접근할 수 있다.

다음 예제는 XML에서 익스큐션컨텍스트(ExecutionContext)에 접근하는 방법을 보여준다: XML 구성

  <bean id="flatFileItemReader" scope="step" class="org.springframework.batch.item.file.FlatFileItemReader">
    <property name="resource" value="#{jobExecutionContext['input.file.name']}" />
  </bean>

다음 예제는 자바에서 익스큐션컨텍스트(ExecutionContext)에 접근하는 방법을 보여준다: 자바 구성

@StepScope
  @Bean
  public FlatFileItemReader flatFileItemReader(@Value(
  "#{jobExecutionContext['input.file.name']}") String name) {
      return new FlatFileItemReaderBuilder<Foo>()
              .name("flatFileItemReader")
              .resource(new FileSystemResource(name))
              ...
}

레이트 바인딩(late binding)을 사용하는 모든 빈은 scope=”step”으로 선언해야 한다. 자세한 내용은 스텝범위(step-scoped)를 참조하자. 스텝 빈은 스텝​​범위(step-scoped)가 아니어야 한다. 스텝 정의에 레이트 바인딩이 필요한 경우, 해당 스텝의 구성 컴포넌트(태스크릿(tasklet), 아이템 리더(item reader) 또는 라이터(writer) 등)의 범위(scoped)가 대신 지정되어야 한다.

스프링 3.0(이상)을 사용하는 경우 스텝 스코프(step-scoped) 빈의 표현식은 흥미롭고 기능이 많은 범용 언어인 스프링 익스프레션 랭귀지(Expression Language)에 있다. 이전 버전과의 호환성을 제공하기 위해, 스프링 배치가 이전 버전의 스프링을 감지하면, 덜 강력하고 구문 분석 규칙이 약간 다른 기본 표현 언어를 사용한다. 주요한 차이점은 위 예제의 맵 키(map keys)가 스프링 2.5에서는 인용될 필요가 없지만 스프링 3.0에서는 인용이 필수라는 점이다.

5.4.1. Step Scope

이전에 표시된 모든 레이트 바인딩(late binding) 예제에는 빈에 선언된 스텝(step) 스코프(scope)가 있다.

다음 예는 XML에서 스텝 스코프에 바인딩하는 예를 보여준다: XML 구성

  <bean id="flatFileItemReader" scope="step" class="org.springframework.batch.item.file.FlatFileItemReader">
    <property name="resource" value="#{jobParameters[input.file.name]}" />
  </bean>

다음 예는 자바에서 스텝 스코프에 바인딩하는 예를 보여준다: 자바 구성

  @StepScope
  @Bean
  public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters[input.file.name]}") String name) {
      return new FlatFileItemReaderBuilder<Foo>()
              .name("flatFileItemReader")
              .resource(new FileSystemResource(name))
              ...
  }

애트리뷰트를 찾을 때까지, 스텝이 시작될 때까지 빈을 실제로 인스턴스화할 수 없기 때문에, 래이트 바인딩(late binding)을 사용하려면 스텝 스코프를 사용해야 한다. 기본적으로 스프링 컨테이너의 일부가 아니기 때문에, 배치(batch) 네임스페이스를 사용하거나, 스텝스코프(StepScope)에 대한 빈을 명시적으로 포함하거나, @EnableBatchProcessing 어노테이션을 사용하여 스코프를 명시적으로 추가해야 한다. 메서드 중 하나만 사용하자. 다음 예제는 배치(batch) 네임스페이스사용을 보여준다:

  <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:batch="http://www.springframework.org/schema/batch"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="...">
    <batch:job .../>
    ...
  </beans>

다음 예제는 빈 명시를 보여준다:

  <bean class="org.springframework.batch.core.scope.StepScope" />

5.4.2. Job Scope

스프링 배치 3.0에 도입된, 스코프(Job scope)는, 스텝 스코프(Step scope)와 구성이 유사하지만 컨텍스트(Job context)의 스코프이므로 실행 중인 잡당 해당 빈의 인스턴스가 하나만 있다. 또한, #{..} 자리 표시자(placeholder)를 사용하여 잡컨텍스트(JobContext)에서 접근할 수 있는 레퍼런스의 레이트 바인딩이 지원된다. 이 기능을 사용하여, 잡 또는 잡 익스큐션 컨텍스트와 잡 파라미터에서 빈 프로퍼티스를 가져올 수 있다.

다음 예는 XML에서 잡 스코프에 바인딩하는 예를 보여준다:

XML 구성

  <bean id="..." class="..." scope="job">
    <property name="name" value="#{jobParameters[input]}" />
  </bean>

XML 구성

  <bean id="..." class="..." scope="job">
    <property name="name" value="#{jobExecutionContext['input.name']}.txt" />
  </bean>

다음 예는 자바에서 잡 스코프에 바인딩하는 예를 보여준다:

자바 구성

  @JobScope
  @Bean
  public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters[input]}") String name) {
      return new FlatFileItemReaderBuilder<Foo>()
              .name("flatFileItemReader")
              .resource(new FileSystemResource(name))
              ...
  }

자바 구성

  @JobScope
  @Bean
  public FlatFileItemReader flatFileItemReader(@Value("#{jobExecutionContext['input.name']}") String name) {
      return new FlatFileItemReaderBuilder<Foo>()
              .name("flatFileItemReader")
              .resource(new FileSystemResource(name))
              ...
  }

기본적으로 스프링 컨테이너의 일부가 아니기 때문에, 배치(batch) 네임스페이스를 사용하거나 잡스코프(JobScope)에 대한 빈을 명시하거나, @EnableBatchProcessing 어노테이션(하나의 접근 방식만 선택)을 사용하여 스코프를 명시적으로 추가해야 한다.

다음 예제에서는 배치(batch) 네임스페이스를 사용한다:

  <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:batch="http://www.springframework.org/schema/batch"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="...">
    <batch:job .../>
    ...
  </beans>

다음 예제에는 잡스코프(JobScope)를 명시적으로 정의하는 빈이 있다:

  <bean class="org.springframework.batch.core.scope.JobScope" />

멀티 스레드 또는 파티션된 스텝(partitioned step)에서 잡 스코프 빈(job-scoped bean)을 사용하는 데는 몇 가지 실질적인 제한이 있다. 스프링 배치는 이러한 사례에서 생성된 스레드를 제어하지 않으므로 해당 빈을 사용하도록 올바르게 설정할 수 없다. 따라서 멀티 스레드 또는 파티션된 스텝(partitioned step)에서 잡 스코프 빈을 사용하지 않는 것이 좋다.

5.4.3. Scoping ItemStream components

자바 구성에서 잡 또는 스텝 스코프의 아이템스트림(ItemStream) 빈을 정의할 때, 빈 메소드의 리턴 타입은 아이템스트림(ItemStream)이어야 한다. 이는 스프링 배치가 이 인터페이스를 구현하는 프록시를 올바르게 생성하고 예상대로 open, updateclose 메소드를 호출하기 위해 필요하다. 다음 예제와 같이 이러한 빈의 빈 메소드가 가장 구체적인 구현체를 리턴하도록 하는 것이 좋다:

구체적인 리턴 타입인 스텝 스코프 빈 정의

  @Bean
  @StepScope
  public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters['input.file.name']}") String name) {
      return new FlatFileItemReaderBuilder<Foo>()
              .resource(new FileSystemResource(name))
              // 아이템 리더의 다른 프로퍼티스 설정
              .build();
  }