내결함성, 건너뛰기 및 재시도 정책
일시적인 데이터 오류에도 작업이 견고하게 동작하도록 건너뛰기, 재시도 및 재시작 의미론을 구성합니다.
내결함성, 건너뛰기 및 재시도 정책은(는) CoddyKit의 무료 Spring Boot 4 Complete Guide 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Spring Boot 4 Complete Guide 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Spring Boot 4 Complete Guide 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Why Fault Tolerance Matters
Batch jobs process huge volumes of data, and real-world data is messy. A single malformed row, a momentary database deadlock, or a flaky downstream call can blow up a job that has already processed millions of records.
Spring Batch gives a chunk-oriented step fault tolerance so it can survive these issues without aborting the whole run. The three core tools are:
- Skip — discard records that cause unrecoverable errors (e.g. bad data) and keep going.
- Retry — re-attempt an operation that failed due to a transient error (e.g. a lock timeout).
- Restart — resume a failed job instance from where it stopped instead of starting over.
Used together, these turn a brittle job into a resilient one.
Enabling Fault Tolerance on a Step
Fault tolerance is opt-in. When you build a chunk step, call .faultTolerant() on the step builder to switch to the fault-tolerant variant. Only then can you declare skip and retry rules.
Without .faultTolerant(), any exception thrown by a reader, processor, or writer rolls back the chunk and fails the step immediately.
@Bean
public Step importStep(JobRepository jobRepository,
PlatformTransactionManager txManager,
ItemReader<Customer> reader,
ItemProcessor<Customer, Customer> processor,
ItemWriter<Customer> writer) {
return new StepBuilder("importStep", jobRepository)
.<Customer, Customer>chunk(100, txManager)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant() // unlocks skip & retry configuration
.build();
}Configuring Skip Policies
Skipping lets the step throw away an individual item that can never succeed — typically a parsing or validation failure — and continue with the next one.
You declare which exceptions are skippable and a global limit:
.skip(Exception.class)— mark an exception type as skippable..noSkip(Exception.class)— explicitly exclude a subtype from skipping..skipLimit(n)— total number of skips allowed before the step fails.
Once the cumulative skip count exceeds skipLimit, the step aborts. This prevents a job from silently swallowing thousands of bad records.
return new StepBuilder("importStep", jobRepository)
.<Customer, Customer>chunk(100, txManager)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant()
.skip(FlatFileParseException.class) // bad CSV line
.skip(ValidationException.class) // failed bean validation
.noSkip(FileNotFoundException.class) // never skip this
.skipLimit(50) // fail after 50 skips
.build();How Skip Interacts with Chunks
Skip behaviour depends on where the exception is thrown:
- Reader skip: the bad item is dropped and reading continues — cheap, no rollback.
- Processor skip: the chunk transaction rolls back, then Spring Batch re-processes the chunk item-by-item, skipping only the offending item.
- Writer skip: same scan-and-retry — the chunk rolls back and items are re-written one at a time so the single bad item can be isolated and skipped.
Because processor/writer skips trigger a rollback and a single-item replay, they are far more expensive than reader skips. Keep validation in the reader/processor where possible so failures are caught early.
A Custom SkipPolicy
The declarative .skip()/.skipLimit() API covers most cases, but you can implement SkipPolicy for fully custom logic — for example, allow more skips for one exception type than another, or inspect the exception message.
The shouldSkip method receives the thrown Throwable and the current skip count; return true to skip, or throw SkipLimitExceededException to fail the step.
public class CustomSkipPolicy implements SkipPolicy {
@Override
public boolean shouldSkip(Throwable t, long skipCount)
throws SkipLimitExceededException {
if (t instanceof FileNotFoundException) {
return false; // fatal: never skip
}
if (t instanceof ValidationException && skipCount < 100) {
return true; // tolerate up to 100 bad records
}
if (t instanceof FlatFileParseException && skipCount < 20) {
return true;
}
return false;
}
}When to Retry vs Skip
The decision between skip and retry comes down to the nature of the error:
- Retry a transient error that may succeed if attempted again: deadlock victim, lock timeout, optimistic locking conflict, brief network blip.
- Skip a deterministic error that will always fail: malformed input, a failed business validation, a constraint that the data itself violates.
Retrying a deterministic error just wastes attempts before failing; skipping a transient error throws away data that would have succeeded. Classify your exceptions correctly — this is the key design decision of the lesson.
Configuring Retry Policies
Retry re-attempts the failing operation up to a configured number of times before giving up. On a fault-tolerant step you declare:
.retry(Exception.class)— exception types that are retryable..noRetry(Exception.class)— exclude a subtype..retryLimit(n)— maximum attempts per item (including the first try).
When an item fails with a retryable exception, the chunk transaction rolls back and the item is replayed up to retryLimit times. If it still fails, the exception propagates — at which point it may be skipped if it is also declared skippable.
return new StepBuilder("importStep", jobRepository)
.<Customer, Customer>chunk(100, txManager)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant()
.retry(DeadlockLoserDataAccessException.class)
.retry(OptimisticLockingFailureException.class)
.retryLimit(3) // up to 3 attempts per item
.skip(ValidationException.class)
.skipLimit(50)
.build();Backoff Between Retries
Hammering a contended resource with immediate retries often makes contention worse. A backoff policy inserts a delay between attempts. ExponentialBackOffPolicy grows the wait multiplicatively, spreading out load.
You attach a custom RetryPolicy or BackOffPolicy via .retryPolicy(...) / by configuring a RetryTemplate. Below, the wait starts at 200ms and doubles each attempt up to 5s.
@Bean
public RetryTemplate retryTemplate() {
ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy();
backOff.setInitialInterval(200); // 200 ms
backOff.setMultiplier(2.0); // 200, 400, 800, ...
backOff.setMaxInterval(5000); // cap at 5 s
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(3,
Map.of(DeadlockLoserDataAccessException.class, true));
RetryTemplate template = new RetryTemplate();
template.setBackOffPolicy(backOff);
template.setRetryPolicy(retryPolicy);
return template;
}Listeners: Observing Skips and Retries
Silently skipping records is dangerous — you need an audit trail. SkipListener callbacks fire for each skipped item so you can log it, write it to a dead-letter table, or alert.
onSkipInRead— a read failure was skipped.onSkipInProcess— gives you the item and the exception.onSkipInWrite— the item that could not be written.
Register it with .listener(skipListener) on the step builder. There is also RetryListener for observing retry attempts.
public class LoggingSkipListener implements SkipListener<Customer, Customer> {
private static final Logger log =
LoggerFactory.getLogger(LoggingSkipListener.class);
@Override
public void onSkipInRead(Throwable t) {
log.warn("Skipped unreadable record: {}", t.getMessage());
}
@Override
public void onSkipInProcess(Customer item, Throwable t) {
log.warn("Skipped {} in process: {}", item.getId(), t.getMessage());
}
@Override
public void onSkipInWrite(Customer item, Throwable t) {
log.warn("Skipped {} in write: {}", item.getId(), t.getMessage());
}
}Restartability and the Job Repository
Skip and retry handle errors within a run; restart handles a run that failed completely. Because Spring Batch persists each step's ExecutionContext and read/write counts in the job repository, relaunching the same JobInstance resumes from the last committed chunk rather than reprocessing everything.
Key rules:
- A
JobInstanceis identified by its identifying job parameters; reuse them to restart, change them to start a fresh instance. - Only jobs in a non-
COMPLETEDstate (e.g.FAILED,STOPPED) can be restarted. - Mark a step
.allowStartIfComplete(true)to force already-completed steps to re-run on restart. - Cap retries with
.startLimit(n)so a broken step is not relaunched forever.
Putting It All Together
A production-grade resilient step combines all three concerns: retry transient failures with backoff, skip deterministic bad data within a bounded limit, audit every skip, and rely on the job repository for restart.
Note the layering: an item that fails is first retried; if it still fails and the exception is skippable, it is skipped (and the listener records it). Exceptions can be both retryable and skippable — retry exhausts first, then skip applies.
return new StepBuilder("resilientImport", jobRepository)
.<Customer, Customer>chunk(100, txManager)
.reader(reader)
.processor(processor)
.writer(writer)
.faultTolerant()
// transient -> retry with backoff
.retry(DeadlockLoserDataAccessException.class)
.retryLimit(3)
// deterministic bad data -> skip
.skip(FlatFileParseException.class)
.skip(ValidationException.class)
.skipLimit(100)
// audit + restart safety
.listener(new LoggingSkipListener())
.startLimit(3)
.build();Quick Check
Test your understanding of skip vs. retry semantics.
Recap
You made a Spring Batch step resilient to transient and deterministic failures:
- Enable tolerance with
.faultTolerant()before declaring any skip/retry rules. - Skip deterministic bad data with
.skip()+.skipLimit(); processor/writer skips cost a rollback and single-item replay, so validate early. - Retry transient errors with
.retry()+.retryLimit(), adding an exponential backoff to ease contention. - Classify carefully: retry transient (deadlock, lock timeout), skip deterministic (parse/validation). When an exception is both, retry runs first, then skip.
- Audit every skip with a
SkipListenerso nothing disappears silently. - Restart failed instances from the last committed chunk via the job repository; control re-runs with
allowStartIfCompleteandstartLimit.
자주 묻는 질문
“내결함성, 건너뛰기 및 재시도 정책” 강의는 무료인가요?
네 — “내결함성, 건너뛰기 및 재시도 정책” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Spring Boot 4 Complete Guide 강의 전체를 잠금 해제할 수 있습니다. Spring Boot 4 Complete Guide 강의에는 총 4개의 강의가 포함되어 있습니다.
“내결함성, 건너뛰기 및 재시도 정책”에서 뭘 배우나요?
일시적인 데이터 오류에도 작업이 견고하게 동작하도록 건너뛰기, 재시도 및 재시작 의미론을 구성합니다. 브라우저에서 직접 실행하는 실습 코드로 Spring Boot 4 Complete Guide을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Spring Boot 4 Complete Guide을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Spring Boot 4 Complete Guide은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“내결함성, 건너뛰기 및 재시도 정책” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Spring Boot 4 Complete Guide 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Spring Boot 4 Complete Guide 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 작업, 단계 및 JobRepository 모델
- 청크 중심 Reader-Processor-Writer 흐름
- 내결함성, 건너뛰기 및 재시도 정책
- 파티셔닝과 병렬 단계 실행