TIL

FaultTolerant

Spring Batch의 기본 오류 처리

청크 지향 처리의 구조적 한계

태스크릿 지향 처리는 내결함성 기능의 지원 대상이 아니다. Tasklet.execute 내부에 전체 로직을 작성하기에 try-catch로 제어 가능하기 때문

재시도(Retry)

RetryTemplate

RetryPolicy

ItemReader 재시도는 없다

내결함성 최적화 - 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();
}
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) {
        // 모든 재시도가 끝난 후 호출 (성공/실패 여부와 무관)
    }
}

내결함성 동작 차이


@Bean
public Step terminationRetryStep() {
    return new StepBuilder("terminationRetryStep", jobRepository)
            //...
            .faultTolerant()
            .retry(TerminationFailedException.class)
            .retryLimit(3)
            .listener(retryListener())
            .processorNonTransactional() // ItemProcessor 비트랜잭션 처리
            .build();
}

다양한 재시도 정책 적용 (RetryPolicy)

백오프 정책(BackOffPolicy)

// BackOffPolicy 설정
backOffPolicy(new FixedBackOffPolicy() {
    {
        setBackOffPeriod(1000); // 1초
    }
})

// 또는 
backOffPolicy(new ExponentialBackOffPolicy() {
    {
        setInitialInterval(1000L);  // 초기 대기 시간
        setMultiplier(2.0);        // 대기 시간 증가 배수
        setMaxInterval(10000L);     // 최대 대기 시간
    }
})

건너뛰기(Skip)

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(new AlwaysSkipItemSkipPolicy())

ItemProcessor에서의 건너뛰기

정상적인 필터링과 건너뛰기 모두 ItemWriter에 데이터가 전달되지 않는다는 점은 동일하지만, Spring Batch는 이를 구분하여 각각 filterCountskipCount에 기록한다.

skipLimit 초과 시 동작

ItemWriter 건너뛰기와 스캔 모드

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) {
    }
}

noRollback()

.faultTolerant()
.noRollback(NonFatalBusinessException.class)