홈 › 실무 배치 › 05 / 6

Skip과 Retry 로직 구현

섹션 7진행 0 / 6

5. 자주 하는 실수 (Tip)

❌ 실수 1: 모든 예외를 재시도

java
new RetryTemplate(5, backoff, Set.of(Exception.class), null);   // 전부 재시도

NumberFormatException 을 5번 재시도하며 백오프까지 기다립니다. 100만 건 중 1만 건이 형식 오류면 5만 번의 헛된 재시도와 수십 분의 대기. 더 나쁜 것은 OutOfMemoryError... 는 Exception 이 아니라 그나마 다행이지만, DB 접속 불가(SQLException)를 건마다 5번씩 재시도하면 배치가 몇 시간 동안 "실행 중"으로 보입니다.

✅ 재시도는 일시적 오류 타입만 화이트리스트로. 나머지는 즉시 던져서 스킵 정책이 판단하게 합니다.

java
Set.of(SocketTimeoutException.class, SQLTransientException.class, TransientApiException.class)

❌ 실수 2: 스킵 한도 없이 스킵

java
catch (Exception e) { log.warn("skip", e); continue; }   // 한도 없음, 타입 구분 없음

입력 파일 인코딩이 바뀌어 전 행이 파싱 실패해도 배치는 "100만 건 스킵, 정상 종료"합니다. 아침에 아무도 이상을 모릅니다. 하류 시스템은 빈 결과를 받습니다.

✅ 스킵 가능 예외 화이트리스트 + skipLimit. 한도 초과 시 FAILED. 한도는 "정상 데이터의 오류율 × 여유"(예: 평소 0.5% → 한도 2%).

❌ 실수 3: 스킵만 하고 기록하지 않음

java
catch (IllegalArgumentException e) { skipCount++; }   // 어떤 건이 왜 스킵됐는지 없음

"스킵 988건"이라는 숫자만 남습니다. 어느 행이, 왜 스킵됐는지 모르니 수정도 재처리도 불가능합니다. 이것은 조용한 데이터 유실입니다.

✅ 데드 레터에 식별자 + 사유 + 원본 행을 기록합니다. 데드 레터 기록 실패는 배치 실패로 취급합니다.

❌ 실수 4: 지터 없는 지수 백오프로 워커 100개 운용

java
BackoffPolicy.exponential(100, 2, 5000);   // 모든 워커가 같은 시각에 재시도

외부 API 가 잠깐 흔들리면 워커 100개가 정확히 100ms, 200ms, 400ms 후에 동시에 재시도해 API 를 다시 쓰러뜨립니다.

✅ exponentialWithJitter. 워커가 하나뿐이라도 지터는 손해가 없습니다.

❌ 실수 5: 멱등하지 않은 호출을 재시도

java
retry.execute(() -> paymentApi.charge(order));   // 타임아웃 = 응답 유실일 수 있음 → 이중 결제

✅ 멱등 키를 붙이거나(charge(order, key)), 멱등 키를 지원하지 않는 API 는 재시도 대신 스킵 + 수동 확인 목록으로.

❌ 실수 6: 재시도 소진을 스킵과 구분하지 않고 삼킴

java
catch (RetryExhaustedException e) { /* 조용히 */ }

재시도 소진은 "외부 시스템이 불안정하다"는 신호입니다. 소진 건수가 리포트에 없으면 외부 장애를 데이터 오류로 오인합니다.

✅ 소진은 스킵으로 처리하되 원인(cause)을 데드 레터에 남기고, 소진 건수를 리포트에 별도 표기합니다. 소진이 연속으로 N건이면 서킷을 열어 배치를 중단합니다.