[ 정수원 스프링 배치 #14 ] - 예외 처리 : Retry

정동욱·2023년 11월 4일
0
post-thumbnail

이번 글에서는 예외 발생 시 Retry하는 기능에 대해 알아볼텐데요, Skip과 대체적인 개념과 사용법은 유사하지만 RetryItemProcessor/Writer에서만 사용 가능합니다.

먼저 ItemProcessor에서 예외 발생 시, 다시 1번째 Processor로 돌아가 재시작합니다. 이 때, 예외가 발생한 Processor도 포함해 모두 재실행합니다. 역시 ItemReader에서 받은 Chunk<input>은 캐시에 저장된 것을 가져와 사용합니다.

테스트를 위한 간단한 코드를 작성해볼텐데요, ItemReader에서 1~10까지의 숫자를 문자열로 읽습니다. 그리고 ItemProcessor에서는 받은 문자열을 그대로 넘겨주고, 만약 문자열이 "2"이거나 "3"이면 RuntimeException이 발생됩니다. 그리고 ItemWriter에서는 받은 문자열을 그대로 출력합니다. 이때, retry() API를 통해 RuntimeException이 발생하면 2번까지는 Retry하도록 설정했습니다.

@Configuration
@RequiredArgsConstructor
public class JobConfiguration4 {

    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

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

    @Bean
    public Step step4() {
        return stepBuilderFactory.get("step4")
                .<String, String>chunk(5)
                .reader(listItemReader())
                .processor(processor())
                .writer(items -> items.forEach(item -> System.out.println("item = " + item)))
                .faultTolerant()
                .retry(RuntimeException.class)
                .retryLimit(3)
                .build();
    }

    @Bean
    public ListItemReader<String> listItemReader() {
        List<String> items = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            items.add(String.valueOf(i));
        }
        return new ListItemReader<>(items);
    }

    @Bean
    public ItemProcessor<? super String, String> processor() {
        return new RetryItemProcessor();
    }
}
public class RetryItemProcessor implements ItemProcessor<String, String> {

    private int count = 0;

    @Override
    public String process(String item) throws Exception {
        if (item.equals("2") || item.equals("3")) {
            count++;
            throw new RuntimeException("failed count = " + count);
        }

        return item;
    }
}

실행을 해보면 정상적으로 실행되지 않고 예외가 발생해 프로그램이 종료됩니다.

Caused by: java.lang.RuntimeException: failed count = 2
	at io.spring.springbatch.job.RetryItemProcessor.process(RetryItemProcessor.java:13) ~[main/:na]
	at io.spring.springbatch.job.RetryItemProcessor.process(RetryItemProcessor.java:5) ~[main/:na]
	at org.springframework.batch.core.step.item.SimpleChunkProcessor.doProcess(SimpleChunkProcessor.java:134) ~[spring-batch-core-4.3.9.jar:4.3.9]
	at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor$1.doWithRetry(FaultTolerantChunkProcessor.java:239) ~[spring-batch-core-4.3.9.jar:4.3.9]
	at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:329) ~[spring-retry-1.3.4.jar:na]
	... 44 common frames omitted

그 이유는 ItemProcessor에 "2"와 "3"이라는 문자열이 오면 다시 재처리하게 되는데, 이때 3번째 재처리 때 retryLimit을 넘어버리기 때문에 예외가 발생돼 프로그램이 종료되는 것입니다. 그럼 이걸 어떻게 해야 할까요? 바로 skip() API를 추가해 발생한 예외에 대해서는 재처리하지 않고 건너뛰도록 설정해야 합니다.

@Bean
public Step step4() {
    return stepBuilderFactory.get("step44")
            .<String, String>chunk(5)
            .reader(listItemReader())
            .processor(processor())
            .writer(items -> items.forEach(item -> System.out.println("item = " + item)))
            .faultTolerant()
            .skip(RuntimeException.class)
            .skipLimit(2)
            .retry(RuntimeException.class)
            .retryLimit(2)
            .allowStartIfComplete(true)
            .build();
}

이렇게 skip() API도 설정한 후 실행시켜보면 "2"와 "3"의 Item을 건너뛰고 ItemProcessor가 정상적으로 실행되는 걸 볼 수 있습니다.

item = 0
item = 1
item = 4
item = 5
item = 6
item = 7
item = 8
item = 9

retry() API 대신 retryPolicy() API를 사용해 다른 방식으로 구성할 수도 있습니다. 그러니까 API단에서 예외와 횟수를 설정하지 않고 재시도할 예외와 횟수를 설정한 SimpleRetryPolicy 구현체를 사용하는 건데요, 코드로 보겠습니다.

@Bean
public Step step4() {
    return stepBuilderFactory.get("step44")
            .<String, String>chunk(5)
            .reader(listItemReader())
            .processor(processor())
            .writer(items -> items.forEach(item -> System.out.println("item = " + item)))
            .faultTolerant()
            .skip(RuntimeException.class)
            .skipLimit(2)
            .retryPolicy(retryPolicy())
            .allowStartIfComplete(true)
            .build();
}

@Bean
public RetryPolicy retryPolicy() {
    Map<Class<? extends Throwable>, Boolean> excepttionClass = new HashMap<>();
    excepttionClass.put(RuntimeException.class, true);

    return new SimpleRetryPolicy(2, excepttionClass);
    }
}

이번 글에서는 배치에서의 Retry에 대해 알아봤습니다. 이전의 Skip과 거의 유사하며, 동시에 때에 따라 Skip을 필요로 합니다. 다음 글에서는 배치의 멀티 쓰레드 프로세싱에 대해 알아보겠습니다.

profile
거인의 어깨 위에서 탭댄스를

0개의 댓글

관련 채용 정보