외부 이슈 상태 동기화에서 영구 실패와 재시도를 나누고 단계 역행을 막은 설계

외부 작업 관리 시스템의 이슈 상태를 내부 파이프라인 DB로 가져오는 동기화 작업이 있었는데, 외부 시스템을 옮기는 과정에서 사라진 이슈를 끝없이 다시 조회하는 문제가 생겼다. 이를 고치면서 "어떤 실패는 다시 시도하고 어떤 실패는 끝낼지", "상태가 뒤로 가지 않게 하려면 어떻게 할지"를 정리했다. 외부 API 상태를 내부로 맞추는 reconcile 작업을 만드는 개발자에게 쓸모 있는 기준이라 기록해 둔다.
배경
개인 프로젝트인 수집·자동화 서비스는 자체 호스팅한 에이전트 오케스트레이터(Paperclip 계열)에 작업을 이슈로 넘기고, 주기적으로 그 이슈 상태를 읽어 내부 파이프라인 단계를 갱신한다. 내부 단계는 설계, 구현(BUILDING), 검토(REVIEWING), 실패 같은 값으로 나뉜다.
오케스트레이터 인스턴스를 새로 옮기면서 일부 이슈가 사라졌다. 내부 DB에는 그 이슈 ID가 남아 있었고, reconcile 작업은 조회 실패를 일시 오류로 보고 계속 재시도했다. 또 외부에서 이슈가 진행 중·검토 중으로 바뀌어도 내부 단계에는 반영되지 않았고, 작업을 받을 에이전트 역할 설정도 옛 이름이 섞여 있었다.
선택지와 결정
먼저 실패를 성격별로 나눴다.
- 404(이슈 없음)는 HTTP 규약상 일시적일 수도 있는 응답이다. 다만 이번에는 인스턴스 이전으로 이슈가 확실히 사라진 상황이라 영구 실패로 취급했다. 재시도해도 의미가 없으므로 실패 단계로 종결하고, 왜 끝났는지 알 수 있게 reconcile 오류를 명시적으로 남긴다. 이후에는 다시 조회하지 않는다.
- 네트워크 오류와 5xx는 상대 서버 사정이다. 기존 지수 백오프 재시도를 그대로 둔다.
- 에이전트 설정 오류는 사람이 설정을 고치면 풀린다. 영구 실패로 끝내고 싶었지만 DB 상태 enum에 영구 실패 값이 없었다. 스키마를 바꾸는 대신 "재시도 가능 실패"에 명확한 오류를 담고 다음 시도를 1시간 뒤로 미루는 방식으로 정했다. 설정을 고치면 다음 주기에 자연스럽게 복구된다.
상태 반영은 단조(monotonic)로 정했다. 외부의 진행 중은 BUILDING, 검토 중은 REVIEWING으로 옮기되, 이미 종결된 작업이나 더 뒤 단계에 있는 작업은 앞 단계로 되돌리지 않는다. 외부 시스템은 상태를 되돌릴 수 있고 조회 순서도 보장되지 않기 때문에, 내부 파이프라인이 앞뒤로 흔들리지 않게 하려는 결정이다.
구현 요점
단계에 순서를 매기고, 새 단계가 지금보다 뒤일 때만 갱신한다. 종결 상태는 어떤 입력에도 바뀌지 않는다.
```ts
const ORDER = { DESIGNING: 0, BUILDING: 1, REVIEWING: 2 }
const TERMINAL = new Set(['DONE', 'DESIGN_FAILED'])
function nextStage(current, incoming) {
if (TERMINAL.has(current)) return current
if (!(incoming in ORDER) || !(current in ORDER)) return current
return ORDER[incoming] > ORDER[current] ? incoming : current
}
```
조회 오류는 상태 코드로 분기한다. 404 전용 오류 타입을 따로 만들어, 호출하는 쪽이 문자열 비교 없이 "끝낼 실패"를 구분하게 했다.
에이전트 역할은 네 가지(총괄, 개발, 운영, 기획)만 환경 변수로 읽고, 옛 역할 이름은 이 넷 중 하나로 매핑했다. 작업을 넘기기 전에는 오케스트레이터의 에이전트 목록으로 총괄 에이전트가 실제로 있는지 확인한다. 설정 오류를 로그에 남길 때는 에이전트 ID 앞 8자만 기록해, 로그만 보고도 어떤 설정인지 찾을 수 있으면서 전체 식별자는 남기지 않게 했다.
운영자 해설
reconcile 작업에서 가장 흔한 실수는 모든 실패를 같은 재시도 정책으로 처리하는 것이다. 그러면 영구 실패가 큐를 계속 차지하고, 정말 일시적인 오류가 묻힌다. 실패를 "바뀌지 않는 것", "상대 사정", "사람이 고칠 것" 세 갈래로 나누는 것만으로 운영 부담이 크게 줄었다.
스키마에 알맞은 상태 값이 없을 때 바로 마이그레이션하는 대신 긴 백오프와 명확한 오류 메시지로 버틴 것은 타협이다. 대신 이 타협을 문서에 남겨, 나중에 영구 실패 상태를 추가할 때 어디를 바꿔야 하는지 알 수 있게 했다.
운영하면서 알게 된 것
- 외부 시스템 이전 뒤에는 내부에 남은 외부 ID가 유효한지 먼저 점검한다.
- 404를 영구 실패로 볼지는 상대 시스템 사정을 확인하고 정한다. 영구로 판단했다면 종결하고 이유를 기록한다.
- 외부 상태를 내부 단계로 옮길 때는 뒤로 가지 않게 단조 갱신한다.
- 사람이 고쳐야 하는 설정 오류는 짧은 주기로 재시도하지 않는다.
- 작업을 넘기기 전에 받을 대상이 실제로 있는지 확인한다.
참고 자료
- 404 Not Found — MDN Web Docs
- 503 Service Unavailable — MDN Web Docs