BackEnd/Spring Batch

[Spring] Spring Batch 사용해보기(2) - step(skip, retry)

Wonol 2022. 9. 14. 15:41
반응형

이전 글에서는 간단하게 Spirng Batch 환경을 구성하고 동작하는 것을 확인해보았습니다.

이번 글에서는 Spring Batch 의 기능 중 Step 에 대해 활용하여 사용해보려고 합니다.


0. 다양한 Step 설정

- 해당 테스트를 위해 간단하게 controller 를 선언하여 배치를 수행하도록 추가하겠습니다.

@Controller
@Slf4j
@RequiredArgsConstructor
public class TestController {

    private final Job StepTestJob;

    private final JobLauncher jobLauncher;

    @SneakyThrows
    @GetMapping("/test/{number}")
    public void test(@PathVariable Long number){

        log.info(number +  " 번 째 실행");

        jobLauncher.run(StepTestJob, new JobParameters());
    }
}

1. Step 에서 startLimit 사용

- 만약 외부와의 통신 또는 DB 작업 도중 연결(커넥션)이 끊기면서 작업이 실패하는 경우가 있을 수 있습니다.

- 이러한 경우 해당 배치를 재시작해야 하는데, 기본적으로 재시작 가능 횟수는 1회로 설정되어 있습니다.

- SpringBatch 는 startLimit() 으로 재시작 횟수를 지정할 수 있습니다.

- 만약 startLimit() 에 지정한 횟수보다 더 이후 실행에서는 Exception 이 발생합니다.

@Slf4j
@Configuration
@RequiredArgsConstructor
public class StepTestBatchJob {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job simpleJob() {
        return jobBuilderFactory.get("simpleJob")
                .start(simpleStep1())
                .build();
    }

    @Bean
    @JobScope
    public Step simpleStep1() {
        return stepBuilderFactory.get("simpleStep1")
                .startLimit(3)	//	재시작 3번 가능
                .tasklet((contribution, chunkContext) -> {
                    log.info(">>>>> This is SimpleStep1");

                    //  에러 발생
                    int error = 5/0;

                    return RepeatStatus.FINISHED;
                })
                .build();
    }
}

- 기본적으로 실행시 ArithmeticException 이 발생하도록 하여 테스트하였습니다.

- 매번 실행할 때마다 해당 Exception 이 발생하게 됩니다.

- 만약 4번 째 재시도 시에는 StartLimitExceededException 이 발생하는 것을 확인하실 수 있습니다.

2. Step 에서 Skip 사용

- Skip 은 데이터를 처리하는 동안 설정된 Exception 이 발생한 경우, 해당 데이터 처리를 건너뛰는 기능입니다.

- 데이터의 작은 오류에 대해서 Step 의 실패 처리 대신 Skip 을 함으로써, 배치 수행의 빈번한 실패를 줄일 수 있습니다.

- 만약 skipLimit() 에 지정한 횟수보다 더 이후 실행에서는 Exception 이 발생합니다.

https://backtony.github.io/spring/2022-01-28-spring-batch-10/#api

  • ItemReader
    - item 을 읽던 도중 예외가 발생하게 되면 해당 item 을 skip 하고 다음 item 을 읽습는다.
  • ItemProcessor
    - item 을 처리하던 도중 예외가 발생하면 해당 Chunk 의 첫 단계로 돌아가서 itemReader로부터 다시 데이터를 전달받습니다.
    - itemProcessor 는 다시 item 을 받아 실행하는데 이전 실행에서 발생한 예외 정보가 내부적으로 남아있어 위의 그림처럼 item2 의 차례가 온다면 처리하지 않고 넘어갑니다.
  • itemWriter
    - Writer 에서 예외가 발생하게 되면 다시 Chunk 단위로 ItemReader 로 돌아갑니다.

https://backtony.github.io/spring/2022-01-28-spring-batch-10/#api

@Slf4j
@Configuration
@RequiredArgsConstructor
public class StepTestBatchJob extends MemberProcessor {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job simpleJob() {
        return jobBuilderFactory.get("simpleJob1")
                .start(simpleStep1())
                .build();
    }

    @Bean
    public Step simpleStep1() {
        return stepBuilderFactory.get("simpleStep1")
                .<String, String>chunk(5)
                .reader(reader())
                .processor(processor())
                .writer(writer())
                .faultTolerant()                    //  내결함성 기능 활성화
                .skipLimit(2)                       //  skip 허용 횟수, 해당 횟수 초과 시 Error 발생, Skip 사용시 필수 선언
                .skip(ArithmeticException.class)    //  ArithmeticException 에 대해서는 skip
                .skip(SQLException.class)           //  SQLException 에 대해서는 skip
                .noSkip(NullPointerException.class) //  NullPointerException 에 대해서는 skip 하지 않음
                //.skipPolicy(new CustomPolicy)     //  사용자가 커스텀하여 Skip Policy 설정 가능
                .build();
    }
    
    //	writer, read, processor 는 아래 코드 참고
    ....
    
 }

- read, writer, processor 에서 각각의 skip 을 확인해보겠습니다.

2-1. ItemReader 에서 Skip

@Bean
public ItemReader<String> reader() {
    log.info("ItemReader 에서는 데이터를 불러오는 로직 작성");
    return new ItemReader<String>() {
        int i = 0;

        @SneakyThrows
        @Override
        public String read() throws ArithmeticException {
           i++;

           if (i == 2){
               log.error("ItemReader ArithmeticException 발생");
               throw new ArithmeticException("에러 발생");
           }

           if (i == 4){
               log.error("ItemReader SQLException 발생");
               throw new SQLException("에러 발생");
           }

           log.info("itemReader >>> " + i);
           return i > 20 ? null : String.valueOf(i);
        }
    };
}

- 2번째 데이터를 읽을 때 ArithmeticException 예외가 발생하고, 4번째 데이터를 읽을 때도 SQLException 이 발생하지만 skipLimit(2) 이므로 skip 하고 진행하게 됩니다.(skipLimit 가 1인 경우 4번째 데이터를 읽을 때 Exception 이 발생합니다.)

- Chunk 사이즈가 5 이기 때문에 첫 번째 읽기 작업에서는 1,3,5,6,7 이후 다음 작업으로 넘어가게 됩니다.

- 만약 중간에 i == 3 인 경우에 NullPointerException 이 발생하게 시킨다면, noSkip 에 따라 Exception 이 발생합니다.

@Bean
public ItemReader<String> reader() {
    log.info("ItemReader 에서는 데이터를 불러오는 로직 작성");
    return new ItemReader<String>() {
        int i = 0;

        @SneakyThrows
        @Override
        public String read() throws ArithmeticException {
           i++;

           if (i == 2){
               log.error("ItemReader ArithmeticException 발생");
               throw new ArithmeticException("에러 발생");
           }
           
           if (i == 3){
               log.error("ItemReader NullPointerException 발생");
               throw new NullPointerException("에러 발생");
           }

           if (i == 4){
               log.error("ItemReader SQLException 발생");
               throw new SQLException("에러 발생");
           }

           log.info("itemReader >>> " + i);
           return i > 20 ? null : String.valueOf(i);
        }
    };
}

2-2. ItemProcessor 에서 Skip

@Bean
public ItemProcessor<String, String> processor() {
    log.info("ItemProcessor 에서는 데이터를 처리 하는 로직 작성");
    return item -> {
        log.info("itemPrcoessor >>> " + item);

        if(item.equals("3")){
            log.error("ItemProcessor 에러 발생");
            throw new SQLException();
        }

        return item;
    };
}

- 3번 째 데이터를 처리할 때 SQLException 이 발생하지만, 해당 Exception 은 Skip 하도록 되어 있으니 넘어가게 됩니다.

- 그러나, 넘어가더라도 데이터 처리를 위해 재동작을 하게 되는데 이때, ItemReader 는 캐싱된 데이터를 다시 itemProcssor 로 넘기므로 로그는 다시 찍히지 않습니다.

- 로그에서와 같이 itemProcessor 가 다시 Chunk 단위로 재시작 되는 것을 확인하실 수 있습니다.

2-3. ItemWriter 에서 Skip

@Bean
public ItemWriter<String> writer() {
    log.info("ItemWriter 에서는 DB 저장과 같은 Transactional 한 로직 작성");

    return items -> {
        for (String item : items){
            if(item.equals("3")){
                log.error("ItemWriter 에러 발생");
                throw new SQLException();
            }
        }
        log.info("items => " + items);
    };
}

- 3번 째 데이터를 처리할 때 SQLException 이 발생하지만, 해당 Exception 은 Skip 하도록 되어 있으니 넘어가게 됩니다.

- 그러나, 넘어가더라도 데이터 처리를 위해 재동작을 하게 되는데 이때, ItemProcessor 는 ItemWriter 로 리스트가 아니라 한 건씩만 보내서 처리하는 것을 확인하실 수 있습니다.

 

3. Step 에서 Retry 사용

- Retry 는 위에 Skip 과 크게 다르지 않습니다.

- Skip 은 넘어간다면, Retry 는 재시도를 하게 됩니다.

- Skip 과 다르게 ItemReader 에서는 지원하지 않으며, ItemProcessor, ItemWriter 에서만 Retry 가 가능합니다.

- Retry Count 는 Item 마다 각각 존재합니다.

아래 예제에서는 Retry 시 무조건 예외가 발생하게 됩니다.(무조건 throw 를 하기 때문...)
실무에서는 서버 간 통신이 중간에 끈기 거나 그런 경우에 재시도하여 정상 통신되는 경우를 대비해 Retry 를 사용하시면 될 것으로 생각됩니다.

https://backtony.github.io/spring/2022-01-28-spring-batch-10/#api

  • ItemProcessor
    - 예외가 발생한다면 다시 Chunk 처음부터 처리합니다.
  • ItemWriter
    - Skip 과 다르게 원래대로 List 로 한 번에 처리합니다.

https://backtony.github.io/spring/2022-01-28-spring-batch-10/#api

@Slf4j
@Configuration
@RequiredArgsConstructor
public class StepTestBatchJob extends MemberProcessor {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    @Bean
    public Job simpleJob() {
        return jobBuilderFactory.get("simpleJob1")
                .start(simpleStep1())
                .build();
    }

    @Bean
    public Step simpleStep1() {
        return stepBuilderFactory.get("simpleStep1")
                .<String, String>chunk(5)
                .reader(reader())
                .processor(processor())
                .writer(writer())
                .faultTolerant()            // 내결함성 기능 활성화
                .retry(SQLException.class)  // SQLException에 대해서는 Retry
                .retryLimit(2)              // Retry 횟수
                .build();
    }
    
    @Bean
    public ItemReader<String> reader() {
        log.info("ItemReader 에서는 데이터를 불러오는 로직 작성");
        return new ItemReader<String>() {
            int i = 0;

            @SneakyThrows
            @Override
            public String read() throws ArithmeticException {
               i++;

               log.info("itemReader >>> " + i);
               return i > 10 ? null : String.valueOf(i);
            }
        };
    }
    
    //	writer, processor 는 아래 코드 참고
}

3-1. ItemProcessor 에서 Retry

@Bean
public ItemProcessor<String, String> processor() {
    log.info("ItemProcessor 에서는 데이터를 처리 하는 로직 작성");
    return item -> {
        log.info("itemPrcoessor >>> " + item);

        if(item.equals("4")) {
            log.error("ItemProcessor 에서 SQLException 발생");
            throw new SQLException();
        }

        return item;
    };
}

- 4번 째 데이터를 처리할 때 SQLException 이 발생하지만, 해당 Exception 은 Retry 하도록 되어 있으니 재시도하게 됩니다.

- Skip 과 동일하게 ItemReader 를 사용하기에 로그에는 찍히지 않습니다.(코드상에서는 주석 처리함...)

- 로그에서와 같이 itemProcessor 가 다시 Chunk 단위로 재시작되는 것을 확인하실 수 있습니다.

- RetryLimit(2) 로 세 번째 재시도 시에도 Excepion 이 발생하게 되면서 RetryException 이 최종적으로 발생합니다.

 

3-2. ItemWriter 에서 Retry

@Bean
public ItemWriter<String> writer() {
    log.info("ItemWriter 에서는 DB 저장과 같은 Transactional 한 로직 작성");

    return items -> {
        for (String item : items) {
            if (item.equals("4")) {
                log.error("ItemWriter 에서 SQLException 발생");
                throw new SQLException();
            }
        }

        log.info("items => " + items);
    };
}

- 3번 째 데이터를 처리할 때 SQLException 이 발생하지만, 해당 Exception 은 Retry 하도록 되어 있으니 재시도하게 됩니다.

- 그러나, Skip 과 다르게 재시작되어도 Processor 에서 한 개씩 보내는 게 아닌 List 로 한 번에 처리하게 됩니다.


참고

- https://khj93.tistory.com/entry/Spring-Batch%EB%9E%80-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

- https://jojoldu.tistory.com/331?category=902551

- https://oingdaddy.tistory.com/183

- https://backtony.github.io/spring/2022-01-28-spring-batch-10/

반응형