[Spring] Spring Batch 사용해보기(2) - step(skip, retry)
이전 글에서는 간단하게 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 이 발생합니다.
- ItemReader
- item 을 읽던 도중 예외가 발생하게 되면 해당 item 을 skip 하고 다음 item 을 읽습는다. - ItemProcessor
- item 을 처리하던 도중 예외가 발생하면 해당 Chunk 의 첫 단계로 돌아가서 itemReader로부터 다시 데이터를 전달받습니다.
- itemProcessor 는 다시 item 을 받아 실행하는데 이전 실행에서 발생한 예외 정보가 내부적으로 남아있어 위의 그림처럼 item2 의 차례가 온다면 처리하지 않고 넘어갑니다. - itemWriter
- Writer 에서 예외가 발생하게 되면 다시 Chunk 단위로 ItemReader 로 돌아갑니다.
@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 를 사용하시면 될 것으로 생각됩니다.
- ItemProcessor
- 예외가 발생한다면 다시 Chunk 처음부터 처리합니다. - ItemWriter
- Skip 과 다르게 원래대로 List 로 한 번에 처리합니다.
@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://jojoldu.tistory.com/331?category=902551
- https://oingdaddy.tistory.com/183
- https://backtony.github.io/spring/2022-01-28-spring-batch-10/