Spring Framework/Spring
[Spring Batch] skip / retry
덴마크초코우유
2025. 2. 13. 20:54
반응형
- Spring Batch에서 일부 오류 데이터 때문에 전체 배치가 실패하는 문제를 해결하는 방법
- 가령 10만 건 데이터 처리 중 1건의 오류로 전체 작업이 중단되거나, 일시적인 네트워크 오류로 배치가 실패하는 상황을 방지할 수 있다.
알아볼 것
- Spring Batch skip/retry 기능을 실제 프로젝트에 적용
- Reader/Processor/Writer 각 단계별 오류 처리 동작 방식 이해
- 상황별 적절한 오류 처리 전략 수립
skip
- 잘못된 데이터를 제외하고 나머지 처리를 계속해야 할 경우
retry
- 네트워크 오류, DB 연결 실패 등 재시도로 해결 가능한 문제일 경우
skip / retry
skip
- 예외 발생 시 해당 아이템만 제외하고 나머지 정상 데이터는 계속 처리한다.
- 오류 발생 → 해당 아이템 제외 → 다음 아이템 계속 처리
- 설정한 skipLimit을 초과하면 해당 Job은 실패한다.
return new StepBuilder(STEP_NAME, jobRepository)
.<String, String>chunk(10, transactionManager)
.reader(simpleItemReader)
.processor(simpleItemProcessor)
.writer(simpleItemWriter)
.faultTolerant()
.skip(ValidationException.class)
.skipLimit(10)
.build();
retry
- 같은 아이템에 대해 설정된 횟수만큼 재시도 후 횟수 초과 시 실패 처리한다.
- 오류 발생 → 같은 아이템으로 재시도 → 성공 시 다음 단계 진행
- retryLimit 초과 시 해당 아이템에 대해 실패 처리
return new StepBuilder(STEP_NAME, jobRepository)
.<String, String>chunk(10, transactionManager)
.reader(simpleItemReader)
.processor(simpleItemProcessor)
.writer(simpleItemWriter)
.faultTolerant()
.retry(TransientException.class)
.retryLimit(3)
.build();
skip
- 각 단계에서 skip이 어떻게 동작하는지 확인해 보기위한 간단한 Job 작성
- 전체 데이터 크기는 20, chunkSize는 5로 설정
- 1~5: 정상 처리
- 6~10: ItemReader에서 예외 발생 (7번에서)
- 11~15: ItemProcessor에서 예외 발생 (13번에서)
- 16~20: ItemWriter에서 예외 발생 (18번에서)
ItemReader
- 예외 발생한 아이템(7번)은 즉시 건너뛰고 다음 아이템을 읽는다.
- Processor와 Writer에는 정상 데이터만 전달된다.
// Reader에서 예외 발생 시
simpleItemReader : 6
simpleItemReader : 7 - Reader에서 예외 발생: 7 // 건너뛰고 계속 진행
simpleItemReader : 8
simpleItemReader : 9
simpleItemReader : 10
simpleItemReader : 11
simpleItemProcessor : 6
simpleItemProcessor : 8 // 7번은 처리되지 않음
simpleItemProcessor : 9
simpleItemProcessor : 10
simpleItemProcessor : 11
simpleItemWriter : [items=[6, 8, 9, 10, 11], skips=[]]
ItemProcessor
- 예외 발생 시 해당 chunk 전체를 재처리
- 예외 발생한 아이템만 제외하고 나머지는 정상 처리
- Writer에는 정상 처리된 아이템만 전달한다.
// Processor에서 예외 발생 시
simpleItemReader : 12
simpleItemReader : 13
simpleItemReader : 14
simpleItemReader : 15
simpleItemReader : 16
simpleItemProcessor : 12
simpleItemProcessor : 13 - Processor에서 예외 발생: 13
// 해당 chunk를 재시도
simpleItemProcessor : 12
simpleItemProcessor : 13 - skip 처리
simpleItemProcessor : 14
simpleItemProcessor : 15
simpleItemProcessor : 16
simpleItemWriter : [items=[12, 14, 15, 16], skips=[]] // 13번 제외
ItemWriter
- chunk 단위 처리에서 예외 발생 시 개별 아이템 단위로 재처리
- 예외 발생한 아이템만 skip하고 나머지는 정상 처리
- Processor부터 다시 실행하여 Writer로 전달한다.
// Writer에서 예외 발생 시
simpleItemReader : 17
simpleItemReader : 18
simpleItemReader : 19
simpleItemReader : 20
simpleItemProcessor : 17
simpleItemProcessor : 18
simpleItemProcessor : 19
simpleItemProcessor : 20
simpleItemWriter : [items=[17, 18, 19, 20], skips=[]]
- Writer에서 예외 발생: [17, 18, 19, 20] 시도 중 18에서 예외 발생
// 이후 개별 아이템에 대해 실행
simpleItemProcessor : 17
simpleItemWriter : [items=[17], skips=[]]
simpleItemProcessor : 18
simpleItemWriter : [items=[18], skips=[]] // 예외 발생 부분 제외
simpleItemProcessor : 19
simpleItemWriter : [items=[19], skips=[]]
simpleItemProcessor : 20
simpleItemWriter : [items=[20], skips=[]]
skip 사용 시 주의사항
- skipLimit을 너무 높게 설정할 경우 데이터 품질이 떨어질 수 있다.
- ItemWriter에서 skip 발생 시 개별 처리로 전환되어 성능이 저하될 수 있다.
retry
- ItemProcessor와 ItemWriter에서만 Retry를 지원
ItemReader에서는 Retry가 동작하지 않는다.
- Reader는 대용량 데이터의 첫 번째 관문이므로 빨라야 한다.
- 읽기 작업 특성 상 특정 시점부터 재시도를 구현하기 어렵다.
ItemProcessor
- 예외 발생 시 해당 chunk 전체를 재처리
- 같은 아이템에 대해 retryLimit까지 재시도
- 성공하면 chunk의 나머지 아이템 계속 처리
- retryLimit 초과 시 Job 실패
// Processor에서 오류 발생 시
simpleItemReader : 11
simpleItemReader : 12
simpleItemReader : 13
simpleItemReader : 14
simpleItemReader : 15
simpleItemProcessor : 11
simpleItemProcessor : 12
simpleItemProcessor : 13
Processor attempt 1 for item 13 - Processor에서 예외 발생: 13
// 1차 재시도: chunk 전체를 다시 처리
simpleItemProcessor : 11
simpleItemProcessor : 12
simpleItemProcessor : 13
Processor attempt 2 for item 13 - Processor에서 예외 발생: 13
// 2차 재시도: chunk 전체를 다시 처리
simpleItemProcessor : 11
simpleItemProcessor : 12
simpleItemProcessor : 13
Processor attempt 3 for item 13 - 성공: 13
simpleItemProcessor : 14
simpleItemProcessor : 15
// 성공 후 Writer 작업 수행
simpleItemWriter : [items=[11, 12, 13, 14, 15], skips=[]]
ItemWriter
- 해당 chunk의 Processor부터 재시도한다. : 같은 chunk 전체를 다시 처리
- Writer가 성공할 때까지 전체 과정 반복
- retryLimit 초과 시 Job 실패
// Writer에서 오류 발생 시
simpleItemReader : 16
simpleItemReader : 17
simpleItemReader : 18
simpleItemReader : 19
simpleItemReader : 20
simpleItemProcessor : 16
simpleItemProcessor : 17
simpleItemProcessor : 18
simpleItemProcessor : 19
simpleItemProcessor : 20
// 1차 Writer 시도 실패
simpleItemWriter : [items=[16, 17, 18, 19, 20], skips=[]]
Writer attempt 1 for item 18 - Writer에서 예외 발생: 18
// 1차 재시도: Processor부터 다시 실행
simpleItemProcessor : 16
simpleItemProcessor : 17
simpleItemProcessor : 18
simpleItemProcessor : 19
simpleItemProcessor : 20
simpleItemWriter : [items=[16, 17, 18, 19, 20], skips=[]]
Writer attempt 2 for item 18 - Writer에서 예외 발생: 18
// 2차 재시도: Processor부터 다시 실행
simpleItemProcessor : 16
simpleItemProcessor : 17
simpleItemProcessor : 18
simpleItemProcessor : 19
simpleItemProcessor : 20
simpleItemWriter : [items=[16, 17, 18, 19, 20], skips=[]]
Writer attempt 3 for item 18 - 성공
주의 사항
- Processor와 Writer는 여러 번 실행되어도 같은 결과를 보장해야 한다.
- chunk 전체가 재처리되므로 큰 chunk는 성능에 영향을 줄 수 있다.
어떻게 적용할까
skip과 retry를 함께 사용해 더욱 견고한 배치 시스템을 구축할 수 있다.
- retry : 해결 가능한 오류는 먼저 재시도
- skip : 재시도로도 해결되지 않는 데이터 품질 문제는 건너뛰기
return new StepBuilder(STEP_NAME, jobRepository)
.<String, String>chunk(jobParameter.getChunkSize(), transactionManager)
.reader(simpleItemReader)
.processor(simpleItemProcessor)
.writer(simpleItemWriter)
.faultTolerant()
// retry 설정: 일시적 오류에 대한 재시도
.retry(TransientDataAccessException.class)
.retry(ConnectException.class)
.retry(SocketTimeoutException.class)
.retryLimit(3)
// skip 설정: 데이터 품질 문제는 건너뛰기
.skip(IllegalArgumentException.class)
.skip(NumberFormatException.class)
.skip(ValidationException.class)
.skipLimit(50)
.build();
Skip 선택 기준:
- 데이터 자체에 문제가 있어 수정이 불가능한 경우
- 일부 데이터 손실이 비즈니스적으로 허용 가능한 경우
- 전체 처리 완료가 개별 데이터 정확성보다 중요한 경우
Retry 선택 기준:
- 외부 시스템 의존성으로 인한 일시적 오류
- 시간이 지나면 해결될 가능성이 높은 문제
- 데이터 손실이 허용되지 않는 경우
반응형