9. Repeat

9.1. RepeatTemplate

배치의 간단한 최적화 또는 잡의 일부로, 반복되는 작업에 관한 것이다. 반복을 전략화하고 일반화하는 이터레이터(iterator) 프레임워크를 제공하기 위해, 스프링 배치에는 리피트오퍼레이션(RepeatOperations) 인터페이스가 있다. 리피트오퍼레이션(RepeatOperations) 인터페이스는 다음과 같다:

  public interface RepeatOperations {
    RepeatStatus iterate(RepeatCallback callback) throws RepeatException;
  }

콜백은 반복할 비즈니스 로직를 삽입할 수 있는 아래 표시된 인터페이스이다:

  public interface RepeatCallback {
    RepeatStatus doInIteration(RepeatContext context) throws Exception;
  }

콜백은 구현체가 반복 종료를 결정할 때까지 계속 실행된다. 이러한 인터페이스의 반환 값은 RepeatStatus.CONTINUABLE 또는 RepeatStatus.FINISHED일 수 있는 이뉴머레이션(enumeration) 값이다. 리피트스테이터스(RepeatStatus) 이뉴머레이션은 작업이 남았는지 여부를 반복 작업 호출자(caller)에게 전달한다. 일반적으로, 리피트오퍼레이션(RepeatOperations) 구현체는 리피트스테이터스(RepeatStatus)를 검사하고 반복 종료 결정의 일부로 사용해야 한다. 남은 작업이 없음을 호출자(caller)에게 알리는 모든 콜백은 RepeatStatus.FINISHED를 반환할 수 있다.

리피트오퍼레이션(RepeatOperations)의 가장 간단한 범용 구현체는 리피트템플릿(RepeatTemplate)이다:

  RepeatTemplate template = new RepeatTemplate();

  template.setCompletionPolicy(new SimpleCompletionPolicy(2));

  template.iterate(new RepeatCallback() {
    public RepeatStatus doInIteration(RepeatContext context) {
        // 배치 처리...
        return RepeatStatus.CONTINUABLE;
    } 
  });

위 예시에서 우리는 RepeatStatus.CONTINUABLE을 반환하여 할 일이 더 있음을 보여준다. 콜백은 RepeatStatus.FINISHED를 반환하여, 호출자(caller)에게 남아 있는 작업이 없음을 알릴 수도 있다. 일부 반복은 콜백에서 수행되는 작업의 고려 사항에 의해 종료될 수 있다. 콜백에 관해 다른 것들은 사실상 무한 루프이며, 완료 결정은 위 예제에 표시된 경우와 같이 외부 정책에 위임한다.

9.1.1. RepeatContext

리피트콜백(RepeatCallback)의 메소드 파라미터는 리피트컨텍스트(RepeatContext)이다. 많은 콜백은 컨텍스트를 무시한다. 그러나, 반복하는 동안 필요한 경우 임시 데이터를 저장하는 애트리뷰트 모음으로 사용할 수 있다. iterate 메서드 반환 후 컨텍스트는 더 이상 존재하지 않는다.

진행 중인 중첩 반복이 있는 경우, 리피트컨텍스트(RepeatContext)에 상위 컨텍스트가 있다. 상위 컨텍스트는 때때로 반복 호출 사이에 공유해야 하는 데이터를 저장하는 데 유용하다. 예를 들어, 반복 작업에서 이벤트 발생 횟수를 세고 후속 호출에서 이를 기억해야하는 경우가 있다.

9.1.2. RepeatStatus

리피트스테이터스(RepeatStatus)는 처리 완료 여부를 나타내기 위해 스트링 배치에서 사용하는 이뉴머레이션(enumeration)이다. 두 가지 사용 가능한 리피트스테이터스(RepeatStatus) 값이 있다:

테이블 18. RepeatStatus 프로퍼티스

ValueDescription
CONTINUABLE해야 할 작업이 남은 경우.
FINISHED더 이상의 반복하지 말아야함.

리피트스테이터스(RepeatStatus)에서 and() 메서드를 사용하여 리피트스테이터스(RepeatStatus) 값을 AND 연산할 수 있다. 이것은 플래그에 AND 연산을 수행하는 것이다. 즉, 상태가 FINISHED이면 결과는 FINISHED이다.

9.2. Completion Policies

리피트템플릿(RepeatTemplate) 내에서, iterate 메서드의 루프 종료는 리피트컨텍스트(RepeatContext)의 팩터리이기도 한 컴플리션폴리시(CompletionPolicy)에 의해 결정된다. 리피트템플릿(RepeatTemplate)은 현재 정책을 사용하여 리피트컨텍스트(RepeatContext)를 생성하고 모든 반복 단계에서 이를 리피트콜백(RepeatCallback)에 전달할 책임이 있다. 콜백이 doInIteration을 완료한 후, 리피트템플릿(RepeatTemplate)컴플리션폴리시(CompletionPolicy)를 호출하여 리피트컨텍스트(RepeatContext)에 저장된 상태를 업데이트하도록 요청해야 한다. 그런 다음 반복이 완료되었는지 정책에 묻는다.

스프링 배치는 컴플리션폴리시(CompletionPolicy)의 범용 구현체를 제공한다. 심플컴플리션폴리시(SimpleCompletionPolicy)RepeatStatus.FINISHED는 언제든지 조기 완료를 강제하며, 최대 고정 횟수까지 실행을 허용한다.

사용자는 복잡한 결정을 위해 완료 정책을 구현해야 할 수 있다. 예를 들어, 온라인 시스템이 사용 중일 때 배치 잡이 실행되지 않도록 하는 배치 프로세싱 윈도우(batch processing window)에는 커스텀 정책이 필요하다.

9.3. Exception Handling

리피트콜백(RepeatCallback) 내에서 예외가 발생하면 리피트템플릿(RepeatTemplate)은 예외를 받아서 다시 발생(rethrow)시키는 것을 결정할 수 있는 익셉션핸들러(ExceptionHandler)를 참고한다.

다음은 익셉션핸들러(ExceptionHandler) 인터페이스를 보여준다:

  public interface ExceptionHandler {
    void handleException(RepeatContext context, Throwable throwable) throws Throwable;
  }

일반적인 사례는 주어진 타입의 예외 수를 세고 한계에 도달하면 실패하는 것이다. 이를 위해, 스프링 배치는 심플리미트익셉션핸들러(SimpleLimitExceptionHandler)와 약간 더 유연한 리스로우온쓰레스홀드익셉션핸들러(RethrowOnThresholdExceptionHandler)를 제공한다. 제공된 타입의 모든 하위 클래스도 카운트된다. 지정된 타입의 예외는 한계에 도달할 때까지 무시된 다음 다시 발생시킨다. 다른 타입의 예외는 항상 다시 발생(rethrown)시킨다.

심플리미트익셉션핸들러(SimpleLimitExceptionHandler)의 중요한 옵셔널 프로퍼티는 useParent라는 불 플래그(boolean flag)이다. 기본값은 false이므로 제한은 현재 리피트컨텍스트(RepeatContext)에서만 고려된다. true로 설정하면 중첩된 반복(예: 스텝 내부의 청크 집합)에서 형제 컨텍스트(sibling contexts) 간 제한이 유지된다.

9.4. Listeners

종종, 다른 반복에 걸쳐있는 문제에 대해 추가 콜백을 수신할 수 있는 것은 유용하다. 이를 위해, 스프링 배치는 리피트리스너(RepeatListener) 인터페이스를 제공한다. 리피트템플릿(RepeatTemplate)을 사용하면 사용자가 리피트리스너(RepeatListener) 구현체를 등록할 수 있으며, 반복 중 사용 가능한 경우 리피트컨텍스트(RepeatContext)리피트스테이터스(RepeatStatus)와 함께 콜백이 제공된다.

리피트리스너(RepeatListener) 인터페이스는 다음과 같다:

  public interface RepeatListener {
    void before(RepeatContext context);
    void after(RepeatContext context, RepeatStatus result);
    void open(RepeatContext context);
    void onError(RepeatContext context, Throwable e);
    void close(RepeatContext context);
  }

openclose 콜백은 전체 반복 전후에 발생한다. before, afteronError는 개별 리피트콜백(RepeatCallback) 호출에 적용된다.

리스너가 둘 이상인 경우, 리스너 목록에 순서가 있다. 이 경우 openbefore는 같은 순서로 호출되고 after, onError, close는 역순으로 호출된다.

9.5. Parallel Processing

리피트오퍼레이션(RepeatOperations)의 구현체는 콜백을 순차적으로 실행하는 것에만 국한되어 있지 않다. 일부 구현에서 콜백을 병렬로 실행할 수 있다는 것은 매우 중요하다. 이를 위해 스프링 배치는 리피트콜백(RepeatCallback)을 실행하기 위해 스프링 태스크익스큐터(TaskExecutor) 전략을 사용하는 태스크익스큐터리피트템플릿(TaskExecutorRepeatTemplate)을 제공한다.

리피트템플릿(RepeatTemplate)과 같이, 기본값은 동일한 스레드에서 전체 반복을 실행하는 싱크러너스태스크익스큐터(SynchronousTaskExecutor)를 사용하는 것이다.

9.6. Declarative Iteration

때때로, 발생할 때마다 반복하고 싶은 비즈니스 프로세스가 있다. 이에 대한 적절한 예시는 메시지 파이프라인의 최적화이다. 대량 메시지가 자주 도착하는 경우, 모든 메시지에 대해 별도의 트랜잭션 비용을 부담하는 것보다 메시지를 통으로 한 번에 처리하는 것이 더 효율적이다. 스프링 배치는 이를 위해 리피트오퍼레이션(RepeatOperations) 객체에서 메서드 호출을 래핑하는 AOP 인터셉터를 제공한다. 리피트오퍼레이션인터셉터(RepeatOperationsInterceptor)는 가로챈 메서드를 실행하고 제공된 리피트템플릿(RepeatTemplate)컴를리션폴리시(CompletionPolicy)에 따라 작업을 반복한다.

다음 예제는 스프링 AOP 네임스페이스를 사용하여 processMessage라는 메서드에 대한 서비스 호출을 반복하는 선언적 반복을 보여준다(AOP 인터셉터를 구성하는 방법에 대한 자세한 내용은 «https://docs.spring.io/spring-framework/reference/core/aop-api.html»사용자 가이드를 참고하자):

  <aop:config>
    <aop:pointcut id="transactional" expression="execution(* com..*Service.processMessage(..))" />
    <aop:advisor pointcut-ref="transactional" advice-ref="retryAdvice" order="-1"/>
  </aop:config>

  <bean id="retryAdvice" class="org.spr...RepeatOperationsInterceptor"/>

다음 예제는 자바에서 스프링 AOP 네임스페이스를 사용하여 processMessage라는 메서드에 대한 서비스 호출을 반복하는 선언적 반복을 보여준다(AOP 인터셉터를 구성하는 방법에 대한 자세한 내용은 «https://docs.spring.io/spring-framework/reference/core/aop-api.html»사용자 가이드를 참고하자):

  @Bean
  public MyService myService() {
    ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader());
    factory.setInterfaces(MyService.class);
    factory.setTarget(new MyService());

    MyService service = (MyService) factory.getProxy();
    JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
    pointcut.setPatterns(".*processMessage.*");

    RepeatOperationsInterceptor interceptor = new RepeatOperationsInterceptor();

    ((Advised) service).addAdvisor(new DefaultPointcutAdvisor(pointcut, interceptor));

    return service;
  }

앞의 예제에서는 인터셉터 내에서 리피트템플릿(RepeatTemplate)을 사용한다. 정책, 리스너 및 기타 세부 정보를 변경하려면 인터셉터에 리피트템플릿(RepeatTemplate) 인스턴스를 삽입할 수 있다.

인터셉트된 메서드가 void를 반환하면, 인터셉터는 항상 RepeatStatus.CONTINUABLE을 반환한다. 따라서 컴플리션폴리시(CompletionPolicy)에 유한한 엔드 포인트가 없으면 무한 루프의 위험이 있다. 엔드 포인트가 있다면, 인터셉트된 메서드의 반환 값이 null이 될 때까지 RepeatStatus.CONTINUABLE을 반환한다. 결과적으로, 대상 메서드 내부의 비즈니스 로직은 null을 반환하거나 제공된 리피트템플릿(RepeatTemplate)익셉션핸들러(ExceptionHandler)에 의해 다시 발생된 예외를 발생(throw)시켜 더 이상 수행할 작업이 없음을 알릴 수 있다.