오류를 500으로 감싸는 순간 재시도 판단도 바뀐다

외부 API 오류를 제품의 공통 예외로 바꾸면 메시지와 오류 코드가 일정해진다. 하지만 작업 큐가 그 예외의 HTTP 상태로 재시도를 결정한다면 변환은 표시 형식 이상의 의미를 가진다. Commons가 403을 반환했는데 내보내기 워커가 500만 보면, 권한이 없는 같은 요청을 다시 보낸다.

원래 403이 cause와 로그에 남아 있어도 스케줄러가 그 값을 읽지 않으면 소용없다. AI Commons에서는 오류를 받는 계층이 실제로 무엇을 검사하는지에 맞춰 전달 계약을 바꿨다. 내보내기와 보이스 클론의 최종 실패 판정에 각각 적용한 계약이다.

원래 상태를 남기는 방식도 공급자별로 다르다

Commons 내보내기 워커는 getStatus()를 본다. toCommonsExportError는 SDK 예외 또는 바로 아래 cause의 SDK 예외를 찾아, 영구 오류로 분류한 4xx는 원래 상태를 유지한다. 408·409·429와 5xx 등 일시 오류는 503으로 변환한다. 본문에는 기존 오류 코드·메시지와 upstreamStatus를 남기고 원인 예외도 보존한다.

경로재시도 분류의 입력4xx 중 일시 오류로 남기는 상태
Commons 내보내기변환된 예외의 getStatus()408 · 409 · 429
보이스 클론원본에서 복사한 statusCode와 오류 코드408 · 429

변환 범위는 exportFile의 등록·업로드·상태 확인 단계다. 공개 createContent와 uploadFile 자체의 기존 500·cause 계약은 유지한다. 다른 호출처까지 한꺼번에 바꾸는 대신 재시도 판단이 필요한 경로에 변환을 둔 것이다. SDK 원인이 없는 파일 I/O 오류와 기존 변환 실패·시간 초과를 모두 같은 새 503 정책으로 바꾸지는 않는다.

보이스 워커는 ServiceUnavailableException에 원본 statusCode를 복사한다. 보이스 분류기가 읽는 것은 getStatus()가 아니라 이 필드다. Commons의 409 예외를 보이스에도 자동 적용할 수 없는 이유다. HTTP 번호만으로 모든 공급자의 재시도 규칙을 통일하면 서비스별 의미를 잃는다.

첫 시도의 실패를 작업의 종료로 쓰면 다음 성공을 버린다

보이스 클론에는 별도의 상태 문제가 있었다. 첫 외부 호출이 일시 오류로 실패하면 워커가 Voice를 ERROR, VoiceJob을 FAILED로 저장했다. 처리기는 오류를 던져 BullMQ의 다음 시도를 허용한다. 다음 시도에서 외부 생성이 성공해도 완료 저장은 이미 종료된 작업을 갱신하지 않는 조건에 걸린다.

  1. 일시 오류 발생현재 시도는 실패했지만 큐에는 재시도가 남아 있다.
  2. 기존 워커가 FAILED를 기록DB에서는 작업 전체가 끝난 것으로 보인다.
  3. 큐의 재시도에서 외부 생성 성공새 공급자 보이스 ID를 받는다.
  4. 완료 조건이 성공 저장을 거부이미 terminal 상태인 작업을 COMPLETED로 바꾸지 못한다.

완료 조건을 완화하면 삭제나 취소된 작업도 되살아날 수 있다. 수정할 곳은 다음 성공의 저장 조건이 아니라 첫 실패의 의미였다. 재시도가 남은 복구 가능한 오류에는 최종 상태를 쓰지 않고 경고와 오류만 전달한다. 시도 로그의 실패와 제품 작업의 최종 실패가 다른 상태로 남는다.

재시도를 끝내는 곳과 최종 상태를 쓰는 곳이 같은 규칙을 쓴다

처리기는 attemptsMade와 설정된 시도 수를 워커에 전달한다. 현재 시도는 attemptsMade + 1이다. 마지막 시도이거나 복구 불가 오류일 때만 워커가 ERROR·FAILED를 기록한다. 핵심 조건을 줄이면 다음과 같다.

function shouldRecordFinalVoiceFailure(error, attemptInfo) {
  return isFinalJobAttempt(attemptInfo)
    || isUnrecoverableVoiceError(error);
}

처리기도 같은 isUnrecoverableVoiceError를 사용해 BullMQ의 UnrecoverableError로 끝낼지 결정한다. 워커는 재시도를 기다리는데 처리기는 재시도를 끊으면 DB 작업이 PROCESSING에 남을 수 있다. 공통 분류는 두 결정의 불일치를 막는다. 시도 정보가 없는 호출은 마지막 시도로 취급한다.

보이스의 복구 불가 목록에는 한도 초과, 업로드 경로 없음, 보이스 없음과 외부 클론 성공 후 DB 반영 실패가 포함된다. 마지막 경우에 다시 클론하면 공급자 보이스를 중복 생성할 수 있다. 상태가 없는 네트워크 오류는 재시도로 남긴다. 이 선택은 응답이 사라진 외부 성공까지 구분한다는 뜻이 아니다.

같은 오류가 변환과 종료 판단을 통과하는지 확인한다

Commons 테스트는 SDK 403을 실제 등록 래퍼를 통해 내보내기까지 전달하고 getStatus()가 403인지 확인한다. 영구 상태와 일시 상태의 변환, 공개 래퍼의 기존 500 계약, SDK 원인이 없는 오류의 동일 인스턴스 유지도 다룬다. 업로드·상태 확인 실패에는 알려진 자산을 먼저 삭제하려고 하며, 삭제 실패가 원래 오류를 덮지 않는지 확인한다.

보이스 테스트는 첫 5xx·429·상태 불명 오류에는 최종 쓰기가 없고, 마지막 5xx나 첫 영구 4xx에는 최종 쓰기가 있는지 확인한다. 첫 5xx 뒤 두 번째 시도가 READY·COMPLETED로 저장되는 흐름도 모킹한 공급자와 DB 상태로 검증한다. 실제 공급자 요청이나 살아 있는 BullMQ의 재시도 소진을 검증하는 범위는 아니다.

재시도를 정확히 분류해도 외부 부작용이 한 번만 일어난다는 보장은 남지 않는다. 보이스 생성 성공 뒤 응답을 잃으면 상태 불명 오류의 재시도가 중복 생성할 수 있다. 이미 생성 ID를 아는 경우의 보상 삭제와 결과를 모르는 호출의 정책은 별도로 설계해야 한다.