ItemReader/ItemProcessor/ItemWriter에서 발생한 예외가 Step으로 전파되어 전체 Job 실패로 이어진다.Step에 맡긴다.
ItemReader/ItemProcessor/ItemWriter를 구현할 뿐태스크릿 지향 처리는 내결함성 기능의 지원 대상이 아니다.
Tasklet.execute내부에 전체 로직을 작성하기에try-catch로 제어 가능하기 때문
RetryTempplate.execute()
canRetry(): True → retryCallbackcanRetry(): False → recoveryCallbackretryCallback
retryCallback을 통해 수행된다.recoverCallback
ItemProcessor/ItemWriter 호출 로직이 retryCallback 안으로 패키징된다.
RetryTemplate을 통해 수행되는 것RetryPolicy를 통해 재시도 여부를 결정SimpleRetryPolicy 정책을 사용
ItemReader에서 발생한 예외는 재시도 되지 않는다.Mutable한 데이터 소스(ex. RabbitMQ, SQS 등)로부터 읽은 데이터는 재시도할 수 없기 때문
Mutable하다는 뜻은 읽으면 사라지는 데이터 소스를 의미ItemReader도 재시도가 가능해질 수도 있다.ItemReader의 기본 규약은 forward only
Step은 청크 버퍼링에서 RetryTemplate에 input Chunk를 다시 전달한다.
@Bean
public Step terminationRetryStep() {
return new StepBuilder("terminationRetryStep", jobRepository)
.<Scream, Scream>chunk(3, transactionManager)
.reader(terminationRetryReader())
.processor(terminationRetryProcessor())
.writer(terminationRetryWriter())
.faultTolerant() // 내결함성 기능 ON
.retry(TerminationFailedException.class) // 재시도 대상 예외 추가
.retryLimit(3)
.listener(retryListener())
.build();
}
faultTolerant()
SimpleRetryPolicy가 사용된다.
retry() - 재시도 대상 예외 지정, 연속해서 호출하면 여러 예외를 지정할 수도 있다.retryLimit() - 최대 시도 횟수 지정, 기본값은 0noRetry() - 특정 예외를 무시하고 싶을 때 지정하면 해당 예외의 하위 타입 예외는 무시된다.listener()
RetryListener 인터페이스의 구현체를 등록해야 한다.public interface RetryListener {
default <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
return true; // 재시도 시작 전에 호출. false를 반환하면 재시도를 중단
}
default <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback,
Throwable throwable) {
// 재시도 중 오류 발생할 때마다 호출
}
default <T, E extends Throwable> void onSuccess(RetryContext context, RetryCallback<T, E> callback, T result) {
// 재시도 성공 시 호출
}
default <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback,
Throwable throwable) {
// 모든 재시도가 끝난 후 호출 (성공/실패 여부와 무관)
}
}
ItemProcessor와 ItemWriter는 완전히 다른 재시도 방식을 사용한다.ItemProcessor의 재시도
retryLimit만큼 다시 처리(process)된다.ItemProcessor의 재시도는 청크 단위가 아닌 아이템 단위로 재시도 컨텍스트가 관리된다.
retryLimit번째 재시도를 실행해야 재시도가 종료되고, 실패하는 아이템 앞 순서에서 재처리되는 아이템들은 retryLimit + 1번만큼 재실행되게 된다.process()를 호출하게 된다.ItemPrcoessor가 멱등하게 동작해야 재시도 과정에서 문제를 일으키지 않는다.processorNonTransactional() 설정을 통해 성공 아이템들은 캐시 결과를 재사용하도록 할 수 있다.
process 내부에서 비용이 큰 작업을 수행했다면 이를 방지 가능
@Bean
public Step terminationRetryStep() {
return new StepBuilder("terminationRetryStep", jobRepository)
//...
.faultTolerant()
.retry(TerminationFailedException.class)
.retryLimit(3)
.listener(retryListener())
.processorNonTransactional() // ItemProcessor 비트랜잭션 처리
.build();
}
ItemWriter의 재시도
ItemWriter는 아이템을 청크 단위로 한 번에 쓴다.ItemProcessor와 달리 재시도 횟수가 청크 단위로 관리되고 특정 아이템만 재시도를 할 수 없다.ItemWriter에서 예외 발생 후 재시도가 시작되면 ItemProcessor부터 다시 시작한다.RetryPolicy를 지정할 수도 있다.
retryPolicy(new CustomRetryPolicy())retry(), retryLimit()와 retryPolicy()를 함께 사용하면 AND 조건으로 모든 정책이 재시도 가능이라 판단해야 재시도가 실행된다.
BackOffPolicy로 재시도 간격을 조정할 수 있다.
NoBackOffPolicy가 적용// BackOffPolicy 설정
backOffPolicy(new FixedBackOffPolicy() {
{
setBackOffPeriod(1000); // 1초
}
})
// 또는
backOffPolicy(new ExponentialBackOffPolicy() {
{
setInitialInterval(1000L); // 초기 대기 시간
setMultiplier(2.0); // 대기 시간 증가 배수
setMaxInterval(10000L); // 최대 대기 시간
}
})
RetryTempplate.canRetry가 false를 반환하면 recoveryCallback이 실행된다 했다.Step에 skip 기능이 활성화되면 recoveryCallback을 통해 예외를 건너뛸 수 있다.
ItemProcessor의 필터링과 skip은 다르다. 필터링은 일부 아이템을 의도적으로 걸러내는 것이고 skip은 예외가 발생했을 때 건너뛰는 예외 처리 방식이다.
@Bean
public Step terminationRetryStep() {
return new StepBuilder("terminationRetryStep", jobRepository)
.<Scream, Scream>chunk(3, transactionManager)
.reader(terminationRetryReader())
.processor(terminationRetryProcessor())
.writer(terminationRetryWriter())
.faultTolerant()
.skip(TerminationFailedException.class)
.skipLimit(2)
.build();
}
SkipPolicy 기반으로 동작하며 별도 설정이 없으면 기본적으로 LimitCheckingItemSkipPolicy가 사용된다.
skip(): 건너뛸 예외를 지정skipLimit(): Step에서 허용할 최대 건너뛰기 횟수 지정
SkipPolicy는 아래와 같이 지정할 수 있다..skipPolicy(new AlwaysSkipItemSkipPolicy())
ItemProcessor에서의 건너뛰기는 단순하다.
SkipPolicy가 건너뛰기 가능하다고 판단하면 null을 반환ItemProcessor에서 null을 반환하면 해당 데이터는 필터링되어 ItemWriter에 전달되지 않는다.정상적인 필터링과 건너뛰기 모두
ItemWriter에 데이터가 전달되지 않는다는 점은 동일하지만, Spring Batch는 이를 구분하여 각각filterCount와skipCount에 기록한다.
RetryTemplate이 recoveryCallback 호출 → 건너뛰기 실행ItemWriter에 스킵된 아이템 제외하고 전달skipLimit을 초과하면 SkipLimitExceededException이 발생하고 작업이 실패한다.
skipLimit은 스텝 전체 기준으로 관리되므로 실패 빈도를 고려하여 적절히 설정해야 한다.ItemWriter에선 개별 아이템이 아닌 청크 단위로 동작한다.
ItemProcessor가 아이템 하나를 처리하고 바로 ItemWriter에 전달ItemWriter는 단일 아이템만 쓰고 성공 시 즉시 커밋SkipPolicy에 따라)ItemProcessor에서 철저한 검증 로직으로 문제 데이터를 사전 필터링.processorNonTransactional() 설정을 사용하면 ItemProcessor의 중복 실행을 방지할 수 있다.
ItemReader에는 재시도 기능이 없지만 건너뛰기는 가능하다.ItemReader의 건너뛰기는 단순하다.
read() 중 예외 발생 시 이를 catch 후 SkipPolicy로 판단한다.ItemReader에서의 건너뛰기는 청크 사이즈에 영향을 미치지 않는다.
SkipListener를 통해 건너뛰기 대상을 추적하고 조치를 취할 수 있다.
public interface SkipListener<T, S> extends StepListener {
default void onSkipInRead(Throwable t) {
}
default void onSkipInWrite(S item, Throwable t) {
}
default void onSkipInProcess(T item, Throwable t) {
}
}
ItemReader에서 건너뛰기가 발생한 경우에만 해당 아이템 객체에 대한 정보를 제공받을 수 없다.
RetryListener와 달라 SkipListener는 ItemWriter의 청크 쓰기 바로 다음에 일괄적으로 진행된다.noRollback()
.faultTolerant()
.noRollback(NonFatalBusinessException.class)
ItemReader에서의 noRollback 예외 처리
skip()과 noRollback() 모두 설정하면 건너뛰기가 우선된다.
skipLimit를 모두 소진한 다음에야 noRollback() 설정대로 동작한다.noRollback 예외로만 지정된 경우 트랜잭션 롤백 없이 예외를 무시하고 다음 아이템을 읽는다.noRollback 동작은 건너뛰기와 유사하지만 건너뛰기와는 다르게 skipCount와 같은 통계 정보가 기록되지 않는다.ItemProcessor에서의 noRollback 예외 처리
noRollback 예외가 아니라면 바로 전파하여 청크 트랜잭션을 롤백한다.noRollback 예외라면 건너뛰기를 시도하지만 skip() 설정도 함께 설정해야 건너뛰기가 진행된다.skip 없이 noRollback을 설정하면 예외가 발생한다.
NonSkippableProcessExceptionItemWriter에서의 noRollback 예외 처리
ItemWriter에선 스캔 모드의 개별 쓰기 작업 중에 발생한다.skipLimit까지 건너뛰고 소진되면 스텝이 실패한다.noRollback 예외라면 롤백 없이 무시하고 넘어간다.
skipCount 증가도 없다.noRollback보다 우선순위가 높아 둘 다 설정되면 건너뛰기만 동작한다.
skipLimit가 소진되면 바로 스텝이 실패한다.