09. Backup Batch - 5
- -
이번 포스트에서는 Batch 작업 실패 시 대처를 위한 fault tolerant에 대해 살펴보자.
FaultTolerant 전략
Tolerant란 관대한이라는 뜻이다. FaultTolerant는 따라서 실패에 관대한 내 결함성을 의미한다.
지난 포스트에서 살펴봤던 시나리오를 다시 생각해보자.
중간에 작은 오류가 발생하더라도 예외가 발생하고 배치 작업은 중단된다. 물론 다지 중단지점에서 재 시작 할 수는 있지만 일단 기존의 프로세스는 중단된다.
긴 배치 작업을 처리하다 보면 일시적인 네트워크 오류 상황이나 DB 커넥션 풀이 부족한 상황, 외부 API 호출 실패 상황 등 순간적인 장애 등이 발생하는 경우가 왕왕 있다. 이때마다 그냥 배치를 중단 시킨다기 보다는 관대하게 몇 초 있다가 다시 시도(retry)하거나 별로 중요하지 않은 데이터는 건너뛰고 배치 작업을 이어가는 편이 나을 수 있다.
이런 상황에서 Retry나 Skip 전략을 사용할 수 있다.
Retry
Retry 전략
Retry 전략은 일시적인 네트워크 오류 상황이나 DB 커넥션 풀이 부족한 상황, 외부 API 호출 실패 상황 등 순간적인 장애 등이 발생했을 때 유효할 수 있다.
RetryPolicy 객체는 다음과 같이 builder를 통해서 만들 수 있으며 방식이 직관적이기 때문에 설명은 주석으로 대체한다.
RetryPolicy retryPolicy = RetryPolicy.builder()
.includes(RuntimeException.class) // 재시도할 예외 유형
.maxRetries(3) // 최대 재시도 횟수
.multiplier(3.0) // 지수 백오프 배율 1초 > 3초 > 9초
.delay(Duration.ofSeconds(1)) // 초기 지연 시간
.build();
Retry 전략 적용
Retry 전략을 적용할 때는 Step에 faultTolerant() 설정이 필요하며 retryPolicy 메서드로 전략을 전달해주면 된다.
@Bean
Step backupPaymentFailStep(
JobRepository jobRepository,
@Qualifier("quietjunTransactionManager") PlatformTransactionManager transactionManager,
MyBatisCursorItemReader<Payment> reader,
@Qualifier("paymentFailProcessor") ItemProcessor<Payment, Payment> processor,
MyBatisBatchItemWriter<Payment> writer) {
RetryPolicy retryPolicy = RetryPolicy.builder()
.includes(RuntimeException.class) // 재시도할 예외 유형
.maxRetries(2) // 최대 재시도 횟수
.multiplier(3.0) // 지수 백오프 배율 1초 > 3초 > 9초
.delay(Duration.ofSeconds(1)) // 초기 지연 시간
.build();
return new StepBuilder("backupPaymentStep", jobRepository)
.<Payment, Payment>chunk(CHUNK_SIZE)
.transactionManager(transactionManager)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant() // 오류 처리 활성화
.retryPolicy(retryPolicy)
.build();
}
위와 같은 경우 오류 발생 시 2번 까지는 재시도 해보고 3번째 시도에서 오류가 발생하면 fail로 처리된다.
Skip
skip 전략
Skip 전략은 문제되는 데이터가 있다면 해당 데이터는 건너뛰는 방법이다.
Skip이 유리한 상황은 일부 데이터 오류가 전체 배치를 중단할 정도의 심각도가 없을 경우이다. 90% 정도만 잘 돼도 성공으로 생각할 수 있는 정도?로 생각해볼 수 있고 필요하다면 오류 데이터는 나중에 별도로 처리할 수 있다.
Skip 전략을 사용할 때 주의 사항은 금융 데이터 처럼 한건 한건이 중요한 상황에서는 주의해야 하며 Skip된 데이터를 잘 추적해야 한다. 또한 무한정의 Skip을 방지하고 Skip의 한도를 설정해주는 것이 권장된다.
다음은 SkipPolicy와 SkipListener를 통해 Skip 되는 데이터를 추적하기 위한 객체 구성 예이다.
Set<Class<? extends Throwable>> skippableExceptions = Set.of(RuntimeException.class);
var skipPolicy = new LimitCheckingExceptionHierarchySkipPolicy(skippableExceptions, 3L);
SkipListener<Payment, Payment> skipListener = new SkipListener<Payment, Payment>() {
@Override
public void onSkipInProcess(Payment item, Throwable t) {
log.error("처리 생략 paymentId: {} 원인: {}", item.getPaymentId(), t.getMessage());
}
};
Retry & Skip 전략 적용
다음은 위에서 설정한 Retry와 Skip 전략을 함께 적용한 모습이다. 두 전략이 함께 적용된 경우 일단 retry 를 해보고 나서 skip 처리한다.
@Bean
Step backupPaymentFailStep(
JobRepository jobRepository,
@Qualifier("quietjunTransactionManager") PlatformTransactionManager transactionManager,
MyBatisCursorItemReader<Payment> reader,
@Qualifier("paymentFailProcessor") ItemProcessor<Payment, Payment> processor,
MyBatisBatchItemWriter<Payment> writer) {
RetryPolicy retryPolicy = RetryPolicy.builder()
.includes(RuntimeException.class) // 재시도할 예외 유형
.maxRetries(2) // 최대 재시도 횟수
.multiplier(3.0) // 지수 백오프 배율 1초 > 3초 > 9초
.delay(Duration.ofSeconds(1)) // 초기 지연 시간
.build();
Set<Class<? extends Throwable>> skippableExceptions = Set.of(RuntimeException.class);
var skipPolicy = new LimitCheckingExceptionHierarchySkipPolicy(skippableExceptions, 3L);
SkipListener<Payment, Payment> skipListener = new SkipListener<Payment, Payment>() {
@Override
public void onSkipInProcess(Payment item, Throwable t) {
log.error("처리 생략 paymentId: {} 원인: {}", item.getPaymentId(), t.getMessage());
}
};
return new StepBuilder("backupPaymentStep", jobRepository)
.<Payment, Payment>chunk(CHUNK_SIZE)
.transactionManager(transactionManager)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant()// 오류 처리 활성화
.retryPolicy(retryPolicy)
.skipPolicy(skipPolicy)
.skipListener(skipListener)
.build();
}
동작 결과 log를 살펴보면 총 1156건 중 1건을 건너 뛰어서 1155건이 백업됨을 확인할 수 있다.
Payment 백업 배치 완료!
========================================
대상 월: 2005-05
읽은 건수: 1156건
저장 건수: 1155건
백업 확인: 1155건
제외 건수: 1건 (paymentId null 또는 처리 실패)
========================================
전략의 비교
restart나 retry, skip은 상황에 따라 적절히 섞어서 사용할 수 있다.
예를 들어 민감한 데이터는 Retry 3회 후 종료 --> Restart 로 처리할 수 있고 일부 유실이 괜찮은 경우는 Retry 3회 --> Skip 10건 --> Restart 로 처리할 수 있다. 상황에 맞춰서 case-by-case이다.
지난 포스트부터 논의했던 restart, retry, skip을 정리하면 다음과 같다.
| 구분 | Retry (재시도) | Skip (건너뛰기) | Restart (재시작) |
| 주체 | 프레임워크가 자동 수행 | 프레임워크가 자동 수행 | 사람/스케줄러가 수행 |
| 시점 | 실행 도중 (Runtime) | 실행 도중 (Runtime) | 실행 종료 후 (After Fail) |
| 대상 | 일시적 오류 (네트워크 등) | 치명적 데이터 오류 (파싱 등) | 작업 전체의 실패 |
| 목적 | 성공시키려고 노력함 | 실패를 인정하고 무시함 | 끊긴 곳부터 복구함 |
| 코드 위치 | Step 설정 (.faultTolerant()) | Step 설정 (.faultTolerant()) | Job 실행 시점 |
'Spring Batch' 카테고리의 다른 글
| 10. RestApi Batch (0) | 2026.01.10 |
|---|---|
| 08. Backup Batch - 4 (0) | 2026.01.08 |
| 07. Backup Batch - 3 (0) | 2026.01.07 |
| 06. Backup Batch - 2 (0) | 2026.01.06 |
| 05. Backup Batch - 1 (0) | 2026.01.05 |
소중한 공감 감사합니다