우리 회사 정산 시스템에서 Spring Batch를 쓰고 있다.
배치에서 오류가 나면 어떻게 처리하고 있을까?
코드를 확인해보니 Spring의 .retry()나 RetryTemplate을 쓰지 않고,
Skip과 별도 Retry Job으로 오류를 처리하고 있었다.
왜 이런 전략을 택했는지 분석해봤다.
🔍 Skip — 데이터 오류 스킵이 아니라 멱등성 보장
정산 서버의 배치 Job 10개 이상에서 Skip 설정을 확인했는데,
대부분 같은 패턴이었다.
JobInstanceAlreadyCompleteException은 이미 완료된 Job을 다시 실행하려 할 때 발생한다.
즉, 데이터 품질 문제를 건너뛰는 게 아니라
"같은 배치를 두 번 돌려도 에러 없이 통과시키는" 용도였다.
정산 배치는 장애 시 재실행이 일상적이다.
네트워크 끊김, DB 타임아웃, 배포 중 중단 등으로 같은 Job을 다시 돌려야 하는 경우가 많다.
이때 "이미 완료된 건"에서 에러가 나면 배치 전체가 멈추므로,
완료된 건은 조용히 넘어가고 미처리 건만 처리하도록 설계한 것이다.
송금 처리는 다르다 — 무제한 Skip + 커스텀 정책
송금 처리 Job은 더 공격적인 Skip 전략을 쓰고 있었다.
커스텀 SkipPolicy 내부:
송금 Job은 chunk-size = 1로 건건이 커밋한다.
이미 송금 완료된 건을 다시 처리하려 하면 DB UNIQUE 제약에 걸려
ConstraintViolationException이 발생한다.이때 skipLimit이 3이면, 재실행 시 완료된 건이 4개만 넘어도 배치가 멈춘다.
송금 배치가 중복 건 때문에 멈추면 미처리 송금이 지연되므로, 중복은 무제한 스킵하고 신규 건만 처리하는 전략이다.
🔁 Retry — Step 안에서 재시도하지 않는다
가장 흥미로운 부분이었다.
정산 시스템 전체에서 Spring의 .retry()나 RetryTemplate을 아예 쓰지 않는다.
대신 이렇게 동작한다:
점진적 재시도 간격
1. 외부 시스템 장애는 즉시 재시도해도 똑같이 실패한다.
송금은 외부 API 호출이다.
네트워크 타임아웃으로 실패했으면, 10초 뒤에 바로 재시도해도 같은 결과일 가능성이 높다.
Step 안에서 3번 즉시 재시도하는 것보다 시간을 두고 재시도하는 게 현실적이다.
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으로 무시
외부 시스템 장애로 실패하면 → 별도 Job에서 시간을 두고 재시도
21번 재시도해도 안 되면 → 사람에게 알린다
'Programming > Java & Spring 관련 내용 정리' 카테고리의 다른 글
| [Spring] Thread-safe 에 대해서 고찰해 보다. (0) | 2023.03.20 |
|---|---|
| [Spring] 멀티모듈 구성 방법 (0) | 2022.10.28 |
| [Spring Batch] 로그인한지 1년 지난 회원 -> 휴면회원으로 상태변경하는 배치구현 (0) | 2022.09.22 |
| [Spring] Filter와 Interceptor의 차이가 무엇인가요? (+ AOP) (0) | 2022.08.15 |
| [Spring] @Component와 @Bean의 차이가 뭔가요? (0) | 2022.08.01 |