스프링 배치 장애 방어 전략
해당 글은 수강 후기 공유 시스템의 학과 데이터 주입, 갱신 배치를 개발하며 고민한 과정을 기점으로 서술합니다.
왜 방어 전략이 필요한가?
대용량 데이터를 처리하는 배치 시스템에서 가장 큰 고민은 "하나의 실패가 전체를 망치는 상황" 입니다.
기존의 위험한 코드:
@Override
public void write(Chunk<? extends Student> chunk) {
chunk.getItems().forEach(item -> {
userRepository.save(item); // 하나만 실패해도 전체 롤백!
});
}
실제 운영에서 발생할 수 있는 문제들은 아래와 같습니다.
- 1000건 중 1건 실패 -> 999건도 함께 실패
- 네트워크 일시 장애로 전체 배치 중단
- 어떤 데이터가 왜 실패했는지 추적 불가
- 새벽 배치 실패 시 다음날까지 복구 지연

이런 문제들을 방치하면, 비용 측면이나 운영 측면이나 비즈니스 임팩트가 큽니다.
저희 수강 후기 공유 시스템의 학과 데이터 주입, 갱신 배치 또한 이런 문제를 직면할 수 있기에, 배치의 기본동작은 해당 문제 요인들을 고려하여 구성하게 되었습니다.
배치 처리에 실패한 데이터에 대해서 어떻게 처리하지?
문제 상황 : 500건의 데이터를 처리하는 배치 작업이 한창 진행 중이었습니다. 약 50%가 주입되었을 무렵, 갑작스러운 DB 연결 장애로 배치가 중단되는 상황이 발생했습니다. 이로 인해 두 가지 심각한 딜레마에 빠졌습니다.
- 데이터 정합성 : 이미 커밋된 250건의 데이터는 어떻게 처리 해야 할까? 그대로 둔 채 배치를 재시작하면 Primary Key 중복과 같은 데이터 충돌이 발생합니다. 그렇다고 전부 롤백하자니 이미 성공한 작업까지 무효가 됩니다.
- 작업의 멱등성 : 어디서부터 다시 시작해야 할까? 처음부터 다시 실행하면 이미 처리된 데이터를 중복으로 처리하게 되어 성능 저하는 물론, 의도치 않은 비즈니스 로직 오류를 유발할 수 있습니다.
이 문제는 결국 장애의 성격에 따라 대응 전략이 달라져야 한다는 결론에 이르게 했습니다.
- 일시적 장애 (Transient Failure) : 네트워크 불안정, DB 커넥션 풀 부족 등, 재시도하면 성공할 수 있는 문제.
- 영구적 장애 (Permanent Failure) : 데이터 자체의 오류(포맷, 제약조건 위반), 복구 불가능한 시스템 다운, 데이터베이스 서버 장애 등, 재시도해도 계속 실패하는 문제.
목표는 명확했습니다. "일시적 장애는 시스템이 스스로 복구하고, 영구적 장애는 운영자가 안전하게 후속 조치를 할 수 있도록 하자."
1차 방어선 : 재시도(Retry)
가장 먼저, 빈번하게 발생하지만 치명적이지 않은 '일시적 장애'를 처리하기 위한 방어막을 구축하기로 했습니다. 저희는 이를 RetryService 라는 이름의 재시도 로직을 구현했습니다.

public ProcessingResult executeWithRetry(Supplier<ProcessingResult> operation, int maxRetryAttempts, long retryDelayMs) {
// Supplier로 받은 task를 실행
retrun lastResult; // 재시도 실패한 결과
}
RetryService는 TransientDataAccessException와 같은 예외를 감지하고, 작업을 즉시 실패 처리하는 대신 최대 3회까지 동일한 작업을 재시도합니다. 일시적 장애를 해소하기 위한 것이며, 만약 해결되었다면 배치의 동작을 그대로 이어갈 수 있습니다.
** 왜 Supplier<T> 를 사용했는가?
답변: 재시도 로직을 구현할 때, 특정 작업에 종속되지 않도록 설계하여 여러 배치 작업들의 재시도 로직을 공통화 시켰습니다. Supplier를 활용하면 '실행할 작업' 자체를 외부에서 주입받을 수 있습니다. RetryService는 마치 내용물이 무엇인지 모르는 '택배 상자'를 전달받아, 배송에 실패하면 다시 배송을 시도할 뿐입니다.
3번의 재시도에도 불구하고 계속 실패한다면, 가벼운 장애가 아님을 의미합니다.
3번의 재시도후에도 실패하면 어떻게 해결할 것인가?
RetryService 가 최종적으로 실패를 선언하면, 해당 예외는 Spring Batch 프레임워크로 전파됩니다.

- 트랜잭션 롤백 : 예외가 발생한 해당 Chunk의 트랜잭션은 완벽하게 롤백됩니다. 즉, 201번째부터 250번째 데이터까지 처리하던 중 장애가 났다면, 해당 50개 데이터에 대한 모든 DB 변경 사항은 원상 복구됩니다. 이로써 '부분적으로 커밋된' 데이터는 절대 발생하지 않습니다.
- 상태 기록 및 작업 중단 : 트랜잭션 롤백 후, Job은 완전하게 중단됩니다. 그리고 Spring Batch의 핵심이
JobRepository는 "이 Job은 몇 번째 Chunk에서, 어떤 이유로 실패했다" 는 모든 메타데이터를 DB에 기록합니다.
이후, 운영자가 DB 서버를 복구하거나 문제가 되는 데이터를 수정하는 등 근본 원인을 해결했다고 가정해봅시다.
그리고 동일한 파라미터로 실패헀던 Job을 다시 실행 시키면, Spring Batch의 재시작 기능이 동작합니다.

Spring Batch 재시작 메커니즘
- Spring Batch는 Job 실행 요청을 받으면, JobRepository 를 먼저 확인하여 동일한 파라미터로 실행된 이력이 있는지 확인합니다.
- 해당 파라미터에 FAILED 상태로 끝난걸 확인하면, 실패지점 Chunk 를 찾아옵니다.
- Spring Batch는 이미 COMPLETED 상태인 1,2번 Chunk는 건너뛰고(Skip) , 실패가 기록된 3번째 Chunk부터 작업을 자동으로 재개합니다.
마무리
배치 시스템에서 예측 불가능한 장애 앞에서 대응할 수 있는 두 가지 방어 수단을 개발하고 테스트 해보았습니다. 장애는 피할 수 없지만, 잘 설계된 구조에서 그 장애를 우아하게 다룰 수 있다는 것을 이번 경험을 통해서 알게 되었습니다.