WEB/Spring Batch

스프링 배치 반복 및 오류 제어

Tony Lim 2023. 3. 25. 19:21
728x90

Repeat

  • Spring Batch는 얼마나 작업을 반복해야 하는지 알려 줄수 있는 기능을 제공한다. 
  • 특정 조건이 충족 될 때까지 (또는 특정 조건이 아직 충족되지 않을 때까지) Job 또는 Step 을 반복하도록 배치 애플리케이션을 구성 할 수 있다. 
  • 스프링 배치에서는 Step 의 반복과 Chunk 반복을 RepeatOperation 을 사용해서 처리하고 있다
  • 기본 구현체로 RepeatTemplate 를 제공한다

 

반복 종료를 결정하는 3가지 항목

RepeatStatus

  • 스프링 배치의 처리가 끝났는지 판별하기 위한 열거형(enum)
    • CONTINUABLE - 작업이 남아 있음
    • FINISHED - 더 이상의 반복 없음

CompletionPolicy

  • RepeatTemplate 의 iterate 메소드 안에서 반복을 중단할지 결정
  • 실행 횟수 또는 완료시기, 오류 발생시 수행 할 작업에 대한 반복여부 결정
  • 정상 종료를 알리는데 사용된다

ExceptionHandler

  • RepeatCallback 안에서 예외가 발생하면 RepeatTemplate 가 ExceptionHandler 를 참조해서 예외를 다시 던질지 여부 결정
  • 예외를 받아서 다시 던지게 되면 반복 종료
  • 비정상 종료를 알리는데 사용된다

RepeatCallback 에 작성된 비지니스 로직을 반복적으로 호출하면서 종료여부를 매번 판단하게 된다.

3조건중 하나라도 종료를 원하면 종료를 하게되고 다 종료를 원치않으면 계속 반복문을 유지하게 된다.

SimpleCompletionPolicy 가 default로 제공이 되고 chunkSize가 실제 Chunksize를 의미하는것이 아니라 그냥 1개의 chunk안의 원소를 몇번 반복해서 처리할것이냐를 의미하는 것 같다. 실제로 해보니까

예외를 던질것인지 로그만 남기고 계속적으로 반복문을 수행할 것인지 결정하게 된다.

CompositeCompletionPolicy completionPolicy = new CompositeCompletionPolicy();
CompletionPolicy[] completionPolicies = new CompletionPolicy[]{
        new TimeoutTerminationPolicy(3000),
        new SimpleCompletionPolicy(2)};
completionPolicy.setPolicies(completionPolicies);
template.setCompletionPolicy(completionPolicy);

3초 나 chunk의 원소를 2번 수행하게 되면 종료된다. OR 의 개념이 적용된다.


FaultTolerant

  • 스프링 배치는 Job 실행 중에 오류가 발생할 경우 장애를 처리하기 위한 기능을 제공하며 이를 통해 복원력을 향상시킬 수 있다
  • 오류가 발생해도 Step 이 즉시 종료되지 않고 Retry 혹은 Skip 기능을 활성화 함으로써 내결함성 서비스가 가능하도록 한다
  • 프로그램의 내결함성을 위해 Skip 과 Retry 기능을 제공한다


Skip

  • ItemReader / ItemProcessor / ItemWriter 에 적용 할 수 있다

Retry

  • ItemProcessor / ItemWriter 에 적용할 수 있다
  • FaultTolerant 구조는 청크 기반의 프로세스 기반위에 Skip 과 Retry 기능이 추가되어 재정의 되어 있다

 

 

process ,write에서는 retry , skip 둘다 적용이 되고 read에서는 skip만적용이 된다.
앞에 둘은 RetryTemplate안에서 돌기 때문이다.

retry를 다하고서 skip할것인지 확인을 하게 된다.


Skip

  • Skip은 데이터를 처리하는 동안 설정된 Exception이 발생했을 경우, 해당 데이터 처리를 건너뛰는 기능이다. 
  • 데이터의 사소한 오류에 대해 Step의 실패처리 대신 Skip을 함으로써, 배치수행의 빈번한 실패를 줄일 수 있게 한다

  • 오류 발생 시 스킵 설정에 의해서 Item2 번은 건너뛰고 Item3번부터 다시 처리한다
  • ItemReader 는 예외가 발생하면 해당 아이템만 스킵하고 계속 진행한다
  • ItemProcessor 와 ItemWriter 는 예외가 발생하면 Chunk 의 처음으로 돌아가서 스킵된 아이템을 제외한 나머지 아이템들을 가지고 처리하게 된다
  • ItemReader가 캐싱을 해놓기 때문에 따로 io작업을 또하지 않고 processor가 캐싱된것들을 받고 item2를 처리할시 skip한다.
  • itemwriter도 마찬가지로 ItemProcessor가 캐싱해놓은 값을 가져가서 Item4가 예외였으면 Item4만 skip하게 된다.

 

Exception이 터졌을때 Map에서 확인하고 skip 가능한 Exception이면 skip count가 2번 이하이면 skip 허용

NoSkippException은 예외를 그대로 발생시켜버린다.

Skip은 총횟수 즉 , Reader, Processor ,Writer 에서 다 일어난 Skip 횟수가 넘으면 Exception을 그대로 던지고 batch 를 종료하게 된다.

/*
 * We only want to process the first item if there is a scan for a
 * failed item.
 */
if (data.scanning()) {
   while (cacheIterator != null && cacheIterator.hasNext()) {
      outputs.add(cacheIterator.next());
   }
   // Only process the first item if scanning
   break;
}

또한 cache를 사용할때 첫번째 Chunk element 는 직접 process하고 그 이후 chunk element는 캐시에서 가져오게 된다.

나중에 Spring batch pr남겨보자 왜 그런건지?

 


Retry

  • Retry는 ItemProcess, ItemWriter 에서 설정된 Exception이 발생했을 경우, 지정한 정책에 따라 데이터 처리를 재시도하는 기능이다. 
  • Skip 과 마찬가지로 Retry를 함으로써, 배치수행의 빈번한 실패를 줄일 수 있게 한다

Reader는 retry기능이 없다.

RetryTemplate에서 ItemProcessor, ItemWriter가 수행이 된다. 오류 발생시 chunk단계의 처음부터 다시 시작하게 된다. 

 

retry 횟수만 큼 retry하고 다 소모되면 recover 콜백을 호출한다.

재시도 대상에 포함된 예외인지 , 재시도 카운터를 체크해서 retry 할껀지 말건지를 결정하게 된다. 더 이상 retry할수없으면 recover callback을 수행하게 된다.

BackOffPolicy 는 재시도를 몇 초 지연후에 시도해볼것인지 를 결정한다.

skip가 비슷한 방식으로 retry 시도 프로세스가 결정된다.

while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {

   try {
      if (this.logger.isDebugEnabled()) {
         this.logger.debug("Retry: count=" + context.getRetryCount());
      }
      // Reset the last exception, so if we are successful
      // the close interceptors will not think we failed...
      lastException = null;
      return retryCallback.doWithRetry(context);
   }

내부적으로 RetryContext를 들고 다니면서 retry를 시도하게 되는데
여기에 chunk의 element와 retry count를 지니고 있는다.

retry count를 확인하고 정해진 재시도 횟수를 넘게되면
위 구문(retryCallback) 에서 빠져나와 recovery callback 에서 처리된다.

 

Custom RetryTemplate

    @Bean
    public RetryTemplate retryTemplate() {

        Map<Class<? extends Throwable>, Boolean> exceptionClass = new HashMap<>();
        exceptionClass.put(RetryableException.class, true);

//        FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
//        backOffPolicy.setBackOffPeriod(2000); //지정한 시간만큼 대기후 재시도 한다.

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(2,exceptionClass);
        RetryTemplate retryTemplate = new RetryTemplate();
//        retryTemplate.setBackOffPolicy(backOffPolicy);
        retryTemplate.setRetryPolicy(retryPolicy);

        return retryTemplate;
    }
public class RetryItemProcessor2 implements ItemProcessor<String, Customer> {

   @Autowired
   private RetryTemplate retryTemplate;

   @Override
   public Customer process(String item) throws Exception {

      Classifier<Throwable, Boolean> rollbackClassifier = new BinaryExceptionClassifier(true);

      Customer result = retryTemplate.execute(new RetryCallback<Customer, RuntimeException>() {
         @Override
         public Customer doWithRetry(RetryContext context) throws RuntimeException {
            // 설정된 조건 및 횟수만큼 재시도 수행
            if(item.equals("1") || item.equals("2")){
               throw new RetryableException("failed");
            }
            return new Customer(item);
         }
      }, new RecoveryCallback<Customer>() {
         @Override
         public Customer recover(RetryContext context) throws Exception {
            // 재시도가 모두 소진되었을 때 수행
            return new Customer(item);
         }
      },
            new DefaultRetryState(item, rollbackClassifier));
      //template - state 추가, skip 추가, backoff 추가,
      return result;
   }
}

retrytemplate에서 retry할것과 retry횟수를 다소진한 이후에 recover에서 할 일들을 custom하게 정의해서 사용할 수 있다.

spring batch의 기본 recover 메소드에서는 skip 설정했는지 확인하고 설정이 존재하면 다시 첨 부터 가는게 아니라 현재 retry 횟수 넘은 item을 skip하고 다음 item부터 진행하게 되었다.

DefaultRetryState를 작성하지않고 null로 넘겨주면 위에서는 retry횟수가 넘어도 새롭게 Processor의 output을 전달하기에 skip되지 않고 완벽하게 진행된다.

하지만 위예시에서는 DefaultRetryState를 넘겨줬기에 

if (shouldRethrow(retryPolicy, context, state)) {
   if (this.logger.isDebugEnabled()) {
      this.logger.debug("Rethrow in retry for policy: count=" + context.getRetryCount());
   }
   throw RetryTemplate.<E>wrapIfNecessary(e);
}

Spring Batch의 RetryTemplate#doExecute에서 sholudRethrow에 걸려 Exception을 던지게 되어 Chunk의 처음으로 돌아가게 된다. 

이후 2번 item 차례가 오면 retry횟수를 이미 다 넘었고 recover메소드로 빠지는데 skip설정도 안했으니 예외를 던지고 종료된다.

이렇게 RetryState를 null을 줘서도 할 수 있다 이지 , 이게 베스트라는것이 아니다.


Skip & Retry 아키텍처

ItemReader

ItemReader는 retry 기능이 존재하지 않는다. 예외가 발생했을 경우에 skip 이 설정되어있으면 skip한다.

chunk가 10개였으면 1~10 중 5를 skip하면 1~11까지 10개의 size는 맞추게 된다.

 

ItemProcessor

예외가 발생하면 Step 재시도를 하게되지만 retry 횟수를 따지는 조건에서 이미 limit을 초과했으면 RecoveryCallback으로 간 후에 skip이 설정되어있으면 해당 item은 이제부터 skip하게 된다.

 

ItemWriter

ItemWriter 는 예외가 발생하지않으면 마지막 프로세스이니 Step이 성공적으로 종료된다.

이외에는 ItemProcessor와 유사하게 동작한다. skip의 doScan에서는 Writer의 경우 list로 받기에 해당 item만 제거한 list를 processor로부터 새로 받는다.

 

 

728x90