공공부하자개발 · 영어 학습 노트
자바
실무 배치대용량 데이터 처리0/6 완료
  • 01배치 프로세스 개념과 아키텍처
  • 02대용량 파일 I/O (NIO, Buffered)
  • 03Chunk 단위 처리와 OOM 방지
  • 04트랜잭션: 커밋과 롤백 시뮬레이션
  • 05Skip과 Retry 로직 구현
  • 06메서드 활용 패턴 (배치 유틸)
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 배치 › 05 / 6

Skip과 Retry 로직 구현

섹션 7진행 0 / 6
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 실패 유형 분류 — 모든 설계의 출발점

유형 예 특징 대응
일시적(transient) 네트워크 타임아웃, DB 락 타임아웃, 커넥션 풀 고갈, 외부 API 429/503, 데드락 시간이 지나면 저절로 해결됨. 같은 입력으로 다시 하면 성공할 가능성이 높음 재시도 (횟수 제한 + 백오프)
영구적(permanent) 숫자 컬럼에 문자, 필수 필드 누락, 존재하지 않는 참조 키, API 400 Bad Request 입력 데이터 자체의 문제. 몇 번을 다시 해도 같은 결과 스킵 (기록 후 건너뜀)
치명적(fatal) DB 접속 불가, 디스크 풀, 설정 파일 없음, OOM, 인증 만료 이 건만의 문제가 아님. 다음 건도 전부 실패할 것 중단 (즉시 FAILED, 알림)

분류 기준은 "같은 입력으로 다시 하면 결과가 달라지는가"와 "다른 건도 영향을 받는가"입니다.

text
                        다시 하면 결과가 달라지는가?
                          ┌────────┴────────┐
                         예                 아니오
                          │                  │
                       재시도          다른 건도 실패하는가?
                      (transient)      ┌──────┴──────┐
                                      예             아니오
                                       │               │
                                     중단            스킵
                                    (fatal)       (permanent)

분류는 예외 타입으로 표현합니다. TransientApiException 은 재시도, PermanentApiException 과 IllegalArgumentException 은 스킵, 그 밖의 예외는 중단. 예외 계층을 이렇게 설계하는 것이 배치 코드의 절반입니다.

2.2 재시도 원리 — 횟수, 백오프, 지터

text
시도 1 ──✗──▶ 대기 100ms ──▶ 시도 2 ──✗──▶ 대기 200ms ──▶ 시도 3 ──✗──▶ 대기 400ms ──▶ 시도 4 ──✓
                                                                                        (maxAttempts=4)
시도 4 ──✗──▶ RetryExhaustedException (원인 예외를 cause 로 보존)
요소 왜 필요한가
최대 횟수 무한 재시도는 배치를 영원히 매달아 둔다. 3~5회가 관행
백오프(대기) 즉시 재시도는 상대가 아직 회복되지 않은 상태에서 다시 때리는 것. 락 경합이라면 상대 트랜잭션이 끝날 시간을 줘야 한다
지수 증가 대기를 100, 200, 400, 800 으로 늘리면 짧은 장애는 빨리, 긴 장애는 상대에 부담 없이 기다린다
최대 대기 지수 증가가 무한히 커지지 않도록 상한(예: 5초)
재시도 가능 예외 판별 영구 오류를 재시도하면 시간만 낭비. 화이트리스트로 명시
리스너 재시도가 일어났다는 사실을 로그로 남겨야 "왜 30분 더 걸렸는지" 알 수 있다

지터(jitter)가 필요한 이유 — thundering herd

외부 API 가 1초간 죽었다가 살아났습니다. 워커 100개가 동시에 실패하고, 모두 정확히 200ms 후에 재시도하면 API 는 같은 순간에 100개 요청을 받아 다시 죽습니다. 그리고 400ms 후에 또 동시에... 이것이 thundering herd(우르르 몰려드는 무리) 문제입니다.

text
지터 없음:   |100 요청|        |100 요청|                |100 요청|         ← 매번 스파이크
지터 있음:   | 3| 7|12| 9|15|11| 8|10| 6|13| 4| 2| ...                    ← 분산

대기 시간에 랜덤(예: 50%~100%)을 곱하면 재시도가 시간축에 흩어집니다. AWS 가 권장하는 "full jitter" 는 random(0, base × 2^attempt) 입니다.

정책 대기 시간 (attempt 1, 2, 3, 4) 용도
fixed(200) 200, 200, 200, 200 단순, 내부 시스템
exponential(100, 2, 5000) 100, 200, 400, 800 외부 API 기본
exponentialWithJitter(100, 2, 5000) 50100, 100200, 200400, 400800 워커 다수, 외부 API 권장

2.3 재시도가 안전하려면 — 멱등성 재등장

재시도는 "같은 요청을 다시 보내는 것"입니다. 첫 요청이 실제로는 성공했는데 응답만 유실된 경우(타임아웃의 흔한 실체), 재시도는 이중 처리가 됩니다. 결제 API 를 재시도해 두 번 결제되는 사고가 이렇게 납니다. 04 레슨의 멱등 키가 여기서 다시 필요합니다. 재시도하는 모든 외부 호출에는 멱등 키가 있어야 하고, 없다면 재시도 대신 스킵 + 수동 확인이 안전합니다.

2.4 스킵 원리 — 한도와 데드 레터

text
건 처리 ──✗──▶ 스킵 가능 예외?
                ├─ 아니오 → 던짐 (중단)
                └─ 예     → skipCount++
                            ├─ skipCount > skipLimit → SkipLimitExceededException (중단)
                            └─ 아니면 → 데드 레터에 기록, 다음 건으로
요소 왜 필요한가
스킵 가능 예외 화이트리스트 아무 예외나 스킵하면 치명적 오류(DB 다운)도 "건너뛰고" 100만 건을 전부 스킵한 뒤 정상 종료한다
skipLimit 0.1% 오류는 정상, 10% 면 입력 파일 문제. 한도를 넘으면 실패시켜 사람이 보게 한다
데드 레터(dead letter) 스킵 건을 사유·원본과 함께 별도 기록. 기록 없는 스킵은 데이터 유실
스킵 건수 리포트 총 건수 대비 스킵 비율을 매일 비교. 갑자기 늘면 상류 시스템 변경 신호

"데드 레터"는 메시지 큐 용어에서 왔습니다. 배달할 수 없는 편지를 모아 두는 우체국의 사서함입니다.

2.5 청크 처리에 스킵/재시도 통합 — 전체 흐름

04 레슨에서 청크 안의 한 건이 실패하면 청크 전체를 롤백했습니다. 이제 실패 건만 제외합니다.

text
청크 (200건) 시작
  │
  ├─ 건 1: parse ✓ → api.send ✓                          → sent 에 추가
  ├─ 건 2: parse ✗ (NumberFormatException)
  │        └ 스킵 가능? 예 → skipCount++ (한도 검사) → 데드 레터 기록
  ├─ 건 3: parse ✓ → api.send ✗ Transient
  │        └ 재시도 1 (2ms) ✗ → 재시도 2 (4ms) ✓         → sent 에 추가
  ├─ 건 4: parse ✓ → api.send ✗ Transient ✗ ✗ ✗
  │        └ RetryExhausted (cause=Transient) → 스킵 가능? 예 → 데드 레터
  ├─ 건 5: parse ✓ → api.send ✗ Permanent (400)
  │        └ 재시도 대상 아님 → 즉시 → 스킵 가능? 예 → 데드 레터
  ├─ 건 6: db 접속 불가 (SQLException)
  │        └ 재시도 대상 아님 → 스킵 가능? 아니오 → 던짐 → 배치 FAILED
  │
  └─ (건 6 이 없었다면) writer.write(sent) → 커밋: 200건 중 197건
단계 판단 결과
1 재시도 가능 예외인가? 예 → 백오프 후 재시도 (최대 N회)
2 재시도 소진 또는 재시도 불가 스킵 정책에 넘김
3 스킵 가능 예외인가? (RetryExhausted 는 cause 로 판별) 예 → 한도 검사 → 데드 레터 → 다음 건
4 스킵 불가 던짐 → 청크 롤백 → 배치 FAILED

Spring Batch 는 여기에 한 단계가 더 있습니다. Writer(청크 쓰기)에서 실패하면 어느 건이 원인인지 모르므로, 청크를 롤백한 뒤 한 건씩 다시 처리하며 실패 건을 찾아 스킵합니다(scan 모드). 이 레슨은 건별 처리 단계에서 실패를 잡는 단순한 형태를 다룹니다.

2.6 재시도와 트랜잭션의 관계

재시도 범위가 어디까지인지가 중요합니다.

재시도 범위 설명 주의
건별 연산(API 호출 하나) 이 레슨의 방식. 가볍고 안전 호출이 멱등해야 함
청크 전체 청크 롤백 후 청크 처음부터 다시. DB 데드락 처리에 사용 청크 안의 외부 호출이 반복됨 → 멱등 키 필수
Step/Job 전체 04 의 재시작. 사람이 또는 스케줄러가 트리거 체크포인트 필요

DB 데드락(SQLTransientException)은 건별이 아니라 청크 트랜잭션 전체가 롤백되므로 청크 단위 재시도가 맞습니다. 외부 API 타임아웃은 건별 재시도가 맞습니다. 둘을 혼동하면 "API 는 성공했는데 청크 재시도로 또 호출"이 됩니다.

2.7 서킷 브레이커 — 재시도의 상위 개념

외부 API 가 완전히 죽어 있는데 100만 건 각각을 4번씩 재시도하면 400만 번 실패하고 백오프까지 합쳐 몇 시간을 낭비합니다. 서킷 브레이커(circuit breaker)는 연속 실패가 임계치를 넘으면 회로를 열어 일정 시간 동안 호출 자체를 하지 않고 즉시 실패시킵니다.

text
CLOSED (정상) ──연속 실패 ≥ 5──▶ OPEN (즉시 실패, 30초) ──시간 경과──▶ HALF-OPEN (1건 시험)
   ▲                                                                       │
   └──────────────────── 성공 ◀───────────────────────────────────────────┘
                                                            실패 → 다시 OPEN

배치에서는 "OPEN 이 되면 배치를 FAILED 로 중단하고 나중에 재시작"이 보통 맞습니다. 상대가 죽었을 때 계속 두드리는 것보다 체크포인트를 남기고 멈추는 것이 낫습니다. 구현은 Resilience4j 같은 라이브러리가 있지만, 원리는 "연속 실패 카운터 + 상태 + 타이머"뿐입니다.

2.8 최종 리포트 — 배치가 남겨야 할 숫자

항목 의미 이상 징후
총 건수 읽은 건수 어제보다 크게 다르면 입력 문제
성공 쓰기까지 완료 -
스킵 데드 레터 건수 비율 급증 = 상류 변경
재시도 횟수 일시 오류 총량 급증 = 외부 시스템/네트워크 불안정
재시도 소진 끝내 실패 0 이 아니면 확인
소요 시간 / 처리 속도 시간 창 관리 추세 상승 = 용량 계획

총 건수 = 성공 + 스킵 이 맞아떨어져야 합니다. 안 맞으면 어딘가에서 건이 조용히 사라진 것입니다.

핵심 원리
  • 2.1 실패 유형 분류 — 모든 설계의 출발점
  • 2.2 재시도 원리 — 횟수, 백오프, 지터
  • 2.3 재시도가 안전하려면 — 멱등성 재등장
  • 2.4 스킵 원리 — 한도와 데드 레터
  • 2.5 청크 처리에 스킵/재시도 통합 — 전체 흐름
  • 2.6 재시도와 트랜잭션의 관계
  • 2.7 서킷 브레이커 — 재시도의 상위 개념
  • 2.8 최종 리포트 — 배치가 남겨야 할 숫자
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제