Skip과 Retry 로직 구현
5. 자주 하는 실수 (Tip)
❌ 실수 1: 모든 예외를 재시도
new RetryTemplate(5, backoff, Set.of(Exception.class), null); // 전부 재시도NumberFormatException 을 5번 재시도하며 백오프까지 기다립니다. 100만 건 중 1만 건이 형식 오류면 5만 번의 헛된 재시도와 수십 분의 대기. 더 나쁜 것은 OutOfMemoryError... 는 Exception 이 아니라 그나마 다행이지만, DB 접속 불가(SQLException)를 건마다 5번씩 재시도하면 배치가 몇 시간 동안 "실행 중"으로 보입니다.
✅ 재시도는 일시적 오류 타입만 화이트리스트로. 나머지는 즉시 던져서 스킵 정책이 판단하게 합니다.
Set.of(SocketTimeoutException.class, SQLTransientException.class, TransientApiException.class)❌ 실수 2: 스킵 한도 없이 스킵
catch (Exception e) { log.warn("skip", e); continue; } // 한도 없음, 타입 구분 없음입력 파일 인코딩이 바뀌어 전 행이 파싱 실패해도 배치는 "100만 건 스킵, 정상 종료"합니다. 아침에 아무도 이상을 모릅니다. 하류 시스템은 빈 결과를 받습니다.
✅ 스킵 가능 예외 화이트리스트 + skipLimit. 한도 초과 시 FAILED. 한도는 "정상 데이터의 오류율 × 여유"(예: 평소 0.5% → 한도 2%).
❌ 실수 3: 스킵만 하고 기록하지 않음
catch (IllegalArgumentException e) { skipCount++; } // 어떤 건이 왜 스킵됐는지 없음"스킵 988건"이라는 숫자만 남습니다. 어느 행이, 왜 스킵됐는지 모르니 수정도 재처리도 불가능합니다. 이것은 조용한 데이터 유실입니다.
✅ 데드 레터에 식별자 + 사유 + 원본 행을 기록합니다. 데드 레터 기록 실패는 배치 실패로 취급합니다.
❌ 실수 4: 지터 없는 지수 백오프로 워커 100개 운용
BackoffPolicy.exponential(100, 2, 5000); // 모든 워커가 같은 시각에 재시도외부 API 가 잠깐 흔들리면 워커 100개가 정확히 100ms, 200ms, 400ms 후에 동시에 재시도해 API 를 다시 쓰러뜨립니다.
✅ exponentialWithJitter. 워커가 하나뿐이라도 지터는 손해가 없습니다.
❌ 실수 5: 멱등하지 않은 호출을 재시도
retry.execute(() -> paymentApi.charge(order)); // 타임아웃 = 응답 유실일 수 있음 → 이중 결제✅ 멱등 키를 붙이거나(charge(order, key)), 멱등 키를 지원하지 않는 API 는 재시도 대신 스킵 + 수동 확인 목록으로.
❌ 실수 6: 재시도 소진을 스킵과 구분하지 않고 삼킴
catch (RetryExhaustedException e) { /* 조용히 */ }재시도 소진은 "외부 시스템이 불안정하다"는 신호입니다. 소진 건수가 리포트에 없으면 외부 장애를 데이터 오류로 오인합니다.
✅ 소진은 스킵으로 처리하되 원인(cause)을 데드 레터에 남기고, 소진 건수를 리포트에 별도 표기합니다. 소진이 연속으로 N건이면 서킷을 열어 배치를 중단합니다.