
우리 회사 정산 시스템에서 Spring Batch를 쓰고 있다.
배치에서 오류가 나면 어떻게 처리하고 있을까?
내가 확인한 배치 Job 설정에서는 Step 단위의 .retry()나 RetryTemplate 기반 즉시 재시도는 사용하지 않고,
Skip과 별도 Retry Job으로 오류를 처리하고 있었다.
왜 이런 전략을 택했는지 분석해봤다.
🔍 Skip — 어떤 예외를 건너뛰고 있나
정산 서버의 배치 Job 10개 이상에서 Skip 설정을 확인했는데,
대부분 같은 패턴이었다.
JobInstanceAlreadyCompleteException은 이미 완료된 Job Instance를 다시 실행하려 할 때 발생한다.
의도는 이미 완료된 실행을 재실행할 때 전체 흐름이 깨지는 것을 줄이려는 것으로 보인다.
다만 이 예외는 일반적인 row 단위 데이터 오류와 다르므로, 실제로 어느 실행 경로에서 발생하는지는 별도 확인이 필요하다.
정산 배치는 장애 시 재실행이 일상적이다.
네트워크 끊김, DB 타임아웃, 배포 중 중단 등으로 같은 Job을 다시 돌려야 하는 경우가 많다.
이 설정은 재실행 시 이미 완료된 인스턴스가 전체 흐름을 깨뜨리지 않도록 하려는 의도로 보인다.
송금 처리는 다르다 — 무제한 Skip + 커스텀 정책
송금 처리 Job은 더 공격적인 Skip 전략을 쓰고 있었다.
커스텀 SkipPolicy 내부:
송금 Job은 chunk-size = 1로 건건이 커밋한다.
이미 송금 완료된 건을 다시 처리하려 하면 DB UNIQUE 제약에 걸려
ConstraintViolationException이 발생한다.중복 송금 자체는 DB UNIQUE 제약이 막는다.
SkipPolicy는 그 예외를 "허용 가능한 중복"으로 보고 다음 건을 처리하게 하는 역할이다.
skipLimit이 3이면 재실행 시 완료된 건이 4개만 넘어도 배치가 멈추므로,
중복 때문에 배치가 멈추면 미처리 송금이 지연된다. 그래서 무제한 스킵 + 신규 건만 처리하는 전략이다.
🔁 Retry — Step 안에서 재시도하지 않는다
가장 흥미로운 부분이었다.
정산 시스템 전체에서 Spring의 .retry()나 RetryTemplate을 아예 쓰지 않는다.
대신 이렇게 동작한다:
점진적 재시도 간격
1. 외부 API 장애는 즉시 재시도해도 성공을 보장할 수 없다.
송금은 외부 API 호출이다.
타임아웃의 경우 실제 외부 시스템에서는 성공했을 가능성도 있다.
같은 Step 안에서 무작정 재호출하기보다, 실패 상태를 남기고 간격을 두고 재처리하는 편이
추적성과 안정성 면에서 유리하다.
2. 재시도 대상 건이 원래 Job의 처리를 막으면 안 된다.
RetryTemplate 방식은 재시도하는 동안 해당 chunk의 다른 건도 대기해야 한다.
오류 건을 별도 테이블로 빼면, 원래 Job은 신규 건만 빠르게 처리하고
오류 건은 별도 Job이 여유 있게 처리할 수 있다.
3. 실패 이력을 추적할 수 있다.
오류 테이블에 failCount, 마지막 실패 시간, 에러 내용이 남으므로
어떤 건이 몇 번 실패했는지 조회할 수 있고, 21회 초과 시 Slack으로 알림이 온다.
📖 기본 개념 복습: Skip과 Retry
위 코드를 이해하기 위한 기본 개념을 정리한다.
Skip이란
chunk 처리 중 설정된 Exception이 발생하면 해당 건만 건너뛰고 다음 건을 처리한다.
skipLimit을 초과하면 Step이 실패한다.
예를 들어 100건을 chunk-size 10으로 처리할 때,
10번째 chunk에서 skipLimit을 초과하면:
1~9번째 chunk(90건)는 이미 커밋되어 있고, 10번째 chunk만 롤백된다.
Retry란
DB DeadLock, 네트워크 타임아웃처럼 재시도하면 성공할 여지가 있는 예외에 사용한다.
📝 정리
우리 시스템은 오류를 모두 같은 방식으로 처리하지 않는다.
이미 처리된 중복성 오류 → Skip으로 배치 중단을 막는다
외부 연동 실패 → 에러 테이블에 상태를 남기고 별도 Retry Job이 재처리
21번 재시도해도 안 되면 → CRITICAL_ERROR + 운영자에게 알린다
금전 도메인에서는 "빨리 다시 시도"보다
"중복 없이, 추적 가능하게, 운영자가 개입할 수 있게"가 더 중요하기 때문이다.
'Programming > Java & Spring 관련 내용 정리' 카테고리의 다른 글
| 우리 정산 시스템은 왜 Chunk와 Tasklet을 반반 썼을까 (0) | 2026.06.23 |
|---|---|
| 대규모 트래픽 티켓팅 시스템 설계 해보기 (0) | 2026.06.05 |
| "주문이 사라졌다!" Oracle → Kafka → DynamoDB 파이프라인 캐시 불일치 (0) | 2026.03.23 |
| Push 서버 재구성: 유연한 이벤트성 푸시 발송 시스템 구현 (1) | 2024.08.27 |
| MDC를 활용해 쓰레드 전환을 이해해보다. (4) | 2024.08.27 |