자체 방문 통계는 항상 수집됩니다. 사용자가 공개 페이지를 어떻게 이용하는지 파악하는 데 쓰이는 Google Analytics(로그인한 워크스페이스는 제외)는 동의한 경우에만 사용됩니다. 자세한 내용은 개인정보 처리방침을 참고하세요.

API 오류 처리: 401, 422부터 제한적 자가 복구까지

인증, 인가, 기능, 파라미터, 알 수 없는 결과를 구분하고, 현재 카탈로그로 요청을 바로잡으며, 네트워크 요청을 보내지 않는 판단 엔진으로 재시도를 제한해 오류 처리가 무단 권한 상승이나 중복 호출로 이어지지 않도록 합니다.

요청이 실패했을 때 클라이언트가 먼저 물어야 할 것은 "몇 번 더 시도할 수 있는가?"가 아니라 "이 요청이 실행되었는가, 그리고 재시도가 의미 있으려면 무엇을 바꿔야 하는가?"입니다. 키 누락, 잘못된 플랫폼 이름, 지원되지 않는 파라미터, 응답 스트리밍 중 연결 끊김은 모두 똑같은 catch 블록으로 들어올 수 있습니다. 이를 한 가지 방식으로 재시도하면 수정 가능한 입력이 반복 실패로 바뀌고, 이미 생성된 작업이 중복 생성될 수도 있습니다.

이 글에서는 EveryInfra REST 인터페이스를 예로 들어 오류 계층화, 카탈로그 확인, 제한적 자가 복구 방법을 정리합니다. 예시 판단 엔진은 다음 단계 권고만 반환하며, 요청을 보내거나 자격 증명을 바꾸지 않고, 데이터 범위를 자동으로 넓히지도 않습니다. 현재 소스 코드, 무료 공개 프로브, 과거 비즈니스 샘플은 각각 따로 사용하며, 400 응답 하나로 인증된 상태의 모든 실패 동작을 추론하려 하지 않습니다.

오류를 분석하기 전에 시도 기록부터 보존하기

호출 전에 비즈니스 작업 ID를 생성해 같은 의도에 속한 여러 시도를 연결합니다. 시도마다 시작·종료 시각, 요청 버전, 대상 기능, HTTP 상태 코드, 제공되는 서비스 요청 ID를 저장합니다. 비즈니스 ID는 서버 멱등성 키가 아닙니다. 서비스가 지원을 명시하지 않았다면 같은 ID로 다시 보내도 중복 제거가 될 것이라고 가정할 수 없습니다.

응답 본문은 HTML, 일반 텍스트, 빈 내용, 파싱할 수 없는 JSON일 수 있습니다. 오류를 파싱하기 전에 Content-Type과 상태를 확인합니다. 파싱에 실패하면 최소한의 진단 요약만 보존하고, 원본 오류 페이지를 사용자 프롬프트에 그대로 넣거나 에이전트에 지시로 전달하지 않습니다. 기본적으로 로그에서는 키, 서명된 전체 URL, 원본 본문, 기타 불필요한 개인정보를 제외합니다.

현재 EveryInfra 공개 오류 엔벨로프에는 error.code, error.message, error.request_id가 포함됩니다. 처리 로직은 코드와 HTTP 상태를 중심으로 구성하고, 메시지는 사람이 읽는 텍스트로 취급합니다. 영어 메시지가 바뀌지 않을 것이라고 가정하지 말고, 문자열에서 처음 발견한 URL로 자격 증명을 실은 채 자동 이동해서도 안 됩니다.

진단 허용 목록은 OWASP Logging Cheat Sheet를 참고할 수 있습니다. 상호작용 식별자로 이벤트를 연결하고, 토큰, 세션 값, 민감한 정보는 마스킹합니다. 오류 로그는 시도를 찾을 만큼의 컨텍스트를 제공하되, 자격 증명이나 고객 데이터 사본의 또 다른 출처가 되어서는 안 됩니다.

401과 403: 인증과 인가를 따로 처리하기

HTTP 401은 유효한 인증 자격 증명이 없음을, 403은 서버가 요청을 거부했음을 나타냅니다. 프로토콜 상태는 일반적인 의미를 제공하며, 구체적인 원인은 제품의 오류 코드에 따라 달라집니다. 현재 EveryInfra 구현에서 키 누락, 유효하지 않은 키, 만료된 키는 인증 문제이고, 제품, 세분화된 기능, 소스 IP에 대한 제한은 인가 문제입니다.

먼저 요청이 올바른 서비스에 도달했는지, Authorization 헤더가 서비스 요구 사항과 일치하는지, 실행 중인 프로세스가 예상한 환경을 읽고 있는지 확인합니다. 비밀 값은 출력하지 말고 변수 존재 여부와 설정 플래그만 확인합니다. 브라우저에서 콘솔에 로그인한 세션이 있다고 해서 서버 측 프로세스가 올바른 API 키를 가져왔다는 증거가 되지는 않습니다.

인증 실패가 연속되면 영향을 받는 작업을 일시 중지하고 담당자에게 알립니다. 복구하려고 다른 키를 돌려 쓰거나, 계정을 바꾸거나, 권한 제한을 해제해서는 안 됩니다. 자동 갱신은 제품이 지원하고 명시적으로 승인된 흐름에만 적용되며, 장기 API 키를 스스로 갱신되는 토큰으로 취급할 수 없습니다.

잘못된 기능 이름: 후보를 먼저 읽고 대상을 검증하기

키 없이 볼 수 있는 공개 오류는 다음과 같이 관찰할 수 있습니다. 여기서는 더 이상 쓰이지 않는 google_maps 식별자를 일부러 전달해 카탈로그만 조회하고, 장소 리뷰는 수집하지 않습니다.

알 수 없는 플랫폼의 상태와 오류 본문 확인
curl --silent --show-error --include --max-time 30 \
  'https://api.everyinfra.com/api/v1/social/catalog?platform=google_maps'

2026-09-04 20:46(베이징 시간) 기준 결과는 HTTP 400이며, error.code는 unknown_capability이고, 메시지에는 google_maps_reviews와 또 다른 유사 후보가 포함됩니다. 알 수 없는 기능을 일률적으로 404에 대응시킬 수 없으며, 후보가 첫 번째로 나왔다는 이유만으로 원래 작업을 자동으로 전환해서도 안 됩니다.

원래 작업이 장소 리뷰를 대상으로 한다면, google_maps_reviews가 리뷰를 제공하는지, 어떤 URL 구조를 요구하는지, 출력이 원래 요구 사항을 충족하는지 검증합니다. 후보 매칭 알고리즘은 이름 유사성만 판단할 뿐 비즈니스 의도를 이해하지 못합니다. 앱 스토어와 장소 리뷰는 서로 바꿔 쓸 수 있는 데이터 출처가 아닙니다.

422: 현재 파라미터에 맞게 입력 수정하기

422는 일반적으로 요청 내용은 이해했지만 그 안의 지시를 처리할 수 없음을 나타냅니다. 애플리케이션은 필드 누락, 알 수 없는 필드, 유효하지 않은 열거값, 크기 제한을 더 세분해 구분해야 합니다. 해결 방법은 각각 다릅니다. URL이 없으면 실제 대상이 필요하고, 알 수 없는 정렬 옵션은 파라미터 검증이 필요하며, 입력이 너무 크면 원본 자료의 절반을 조용히 버리는 대신 확인된 분할 옵션이 필요합니다.

특정 플랫폼으로 좁혀 파라미터 확인
curl -fsS --max-time 30 \
  'https://api.everyinfra.com/api/v1/social/catalog?platform=douyin' \
  | jq '[.capabilities[] | {
      action, required_params, optional_params, param_meanings, max_limit
    }]'

현재 구현은 알 수 없는 파라미터에 unknown_param을, 지원되지 않는 열거값에 invalid_value를 반환하며, 조치 가능한 힌트를 함께 제공합니다. 그러나 같은 이름의 파라미터라도 액션마다 다를 수 있으므로, 전역 파라미터 표가 각 액션의 파라미터 레퍼런스를 대신할 수 없습니다. required_params, optional_params, 허용 값은 함께 평가해야 하며, 일반적인 목록 개수 제한은 페이지네이션 약속이 아닙니다.

수정한 뒤에는 새 요청 버전을 생성하고, 원래 오류와 수정 근거를 보존합니다. 원래 의도가 명확히 유지되는 경우에만 매핑을 자동 적용하며, 후보가 여러 개이거나, 실제 대상이 없거나, 시간 범위를 넓히거나, 모델을 바꾸는 경우에는 사람이 결정해야 합니다. 누락된 필드를 모델의 추측으로 채우는 것은 안전한 자가 복구가 아닙니다.

402와 429: 할당량 문제를 요청 속도 제한으로 오인하지 않기

EveryInfra는 현재 할당량 부족을 나타내기 위해 402 quota_exhausted를 사용합니다. 이는 제품 고유의 규칙이며, 모든 서비스에서 402가 갖는 보편적인 비즈니스 의미가 아닙니다. 할당량 문제는 계정 소유자가 개입해야 합니다. 동시 요청 수를 낮춘다고 잔액이 자동으로 늘어나지는 않으며, 클라이언트가 자동으로 충전하거나 자격 증명을 옮겨서는 안 됩니다.

429는 요청 속도 제한 신호입니다. 응답에 유효한 Retry-After 헤더가 있으면 그 지시를 따릅니다. 헤더가 없다고 해서 즉시 짧은 간격으로 반복 재시도해도 된다는 뜻은 아니며, 서비스 복구 시각을 지어내도 된다는 뜻도 아닙니다. 재시도 간격, 지터, 최대 시도 횟수, 마감 시한은 애플리케이션 정책에서 명시적으로 정의해야 합니다.

요청 속도 제한 전략은 할당량을 공유하는 모든 작업 프로세스를 포괄해야 합니다. 워커 하나만 백오프하고 나머지는 계속 요청을 보내면 혼잡은 해소되지 않습니다. 이 지침은 애플리케이션 스케줄링에 관한 것이며, API가 여러 클라이언트 프로세스를 대신 조율해 준다는 뜻이 아닙니다.

5xx 오류, 연결 끊김, 제출된 작업: 실행 상태부터 확인하기

5xx 오류를 "아무 일도 일어나지 않았다"로 단순하게 해석할 수는 없습니다. 클라이언트가 전체 응답을 받지 못한 사이에 서비스가 요청을 수락했거나, 부분 결과를 생성했거나, 과금 단계를 완료했을 수 있습니다. 명시적인 멱등성 보장이나 실행 증거가 없다면, POST를 자동으로 다시 보내지 말고 결과를 알 수 없음으로 기록한 뒤 요청 식별자로 확인합니다.

job_id나 서비스가 제공하는 조회 엔드포인트가 있으면 원래 작업을 우선 조회합니다. 실행 중, 마무리 중, 성공, 실패, 조회 실패는 각각 따로 처리해야 합니다. 조회 결과가 404라면 신원이나 범위 문제일 수 있으므로, 이를 이유로 대체 작업을 생성해서는 안 됩니다. 조회할 때 원래 요청에 대응하는 신원을 사용하는지 확인합니다.

HTTP 200을 받은 뒤에도 비즈니스 내용은 확인해야 합니다. MCP에는 JSON-RPC와 도구 오류 계층이 추가되므로 REST 전용 전략에 의존할 수 없습니다. 빈 집합, 부분 결과, 비정상 응답은 각각 고유한 상태를 유지하며 일률적으로 성공으로 표시되지 않습니다. 최종 과금액도 클라이언트 측 상태만으로 도출할 수 없습니다.

Microsoft의 재시도 패턴은 중요한 점을 짚습니다. 복구 가능한 장애만 재시도하고, 작업의 멱등성과 중첩된 재시도 계층을 고려해야 한다는 것입니다. SDK, 큐, 비즈니스 계층이 각자 재시도하면 전체 시도 횟수가 예산을 넘을 수 있습니다. 모든 곳에서 기본적으로 "다시 시도"하는 대신, 전체 컨텍스트를 이해하는 하나의 계층이 결정을 맡아야 합니다.

재시도를 실행하지 않고 라우팅만 하는 의사 결정 엔진

다음은 오프라인 애플리케이션 계층 예시이며 EveryInfra SDK가 아닙니다. 입력은 프로토콜 파싱을 마친 REST 시도 기록에서 가져와야 합니다. retry_after_policy_check는 실제 인터페이스 계약이나 실행되지 않았다는 명시적 증거를 근거로 설정해야 하며, 모델의 판단이나 오류 텍스트만으로 임의 활성화해서는 안 됩니다. attempts는 이전 시도 횟수를 추적합니다.

오프라인 JavaScript: 권한 게이트와 재시도 예산 유지
function nextStep({status, hasJob = false, replaySafe = false,
                   attempts = 1, maxAttempts = 3}) {
  if (status !== null && (!Number.isInteger(status) || status < 100 || status > 599)) {
    throw new Error("invalid HTTP status");
  }
  if (typeof hasJob !== "boolean" || typeof replaySafe !== "boolean"
      || !Number.isSafeInteger(attempts) || attempts < 1
      || !Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {
    throw new Error("invalid decision inputs");
  }
  if (status === 401 || status === 403) return "authorization_review";
  if (status === 402) return "account_review";
  if (hasJob) return "inspect_existing_job";
  if (status === 400 || status === 422) return "revise_request";
  if (status === 429 || status === null || (status >= 500 && status <= 599)) {
    return replaySafe && attempts < maxAttempts
      ? "retry_after_policy_check"
      : "inspect_attempt";
  }
  if (status >= 200 && status < 300) return "validate_delivery";
  return "manual_review";
}

// synthetic attempt; contains no network calls.
console.log(nextStep({status: 503}));
console.log(nextStep({status: 429, replaySafe: true, attempts: 3}));
console.log(nextStep({status: 202, hasJob: true}));

예시의 세 출력은 각각 현재 시도 확인, 예산 소진 후 시도 확인, 원래 작업 조회를 요구합니다. retry_after_policy_check는 즉시 재시도하라는 명령이 아닙니다. 바깥 계층에서 대기 시간, 전역 요청 속도 제한, 사용자가 허용한 작업 범위도 확인해야 합니다. 파라미터를 조정한 뒤 새로 만든 시도도 전체 비즈니스 예산에 포함되며, 카운터를 초기화해 무한 반복해서는 안 됩니다.

시스템이 멈추는지 검증하는 것은 복구를 검증하는 것만큼 중요하다

  • 유효하지 않은 인증: 대체 자격 증명에 접근하지 않으며 비즈니스 재시도를 일으키지 않습니다.
  • 모호한 기능 후보: 원래 대상을 유지하고 검증을 요구하며, 제품을 자동으로 전환하지 않습니다.
  • 수락되었지만 응답이 중단된 요청: 원래 시도와 작업을 보존하며 중복 생성하지 않습니다.
  • 파라미터 수정 후에도 계속 실패: 새 버전을 기록하되 전체 시도 예산을 지킵니다.
  • JSON이 아닌 오류, 요청 ID 누락, 알 수 없는 코드: 비정상 종료하거나 성공을 지어내지 않고 보수적 처리로 넘어갑니다.
  • 사용 가능한 데이터는 반환되었지만 비즈니스 목표 미달성: 전달 상태와 후속 문제를 보존하며, 잔액 복원을 임의로 선언하지 않습니다.

소규모 오류 샘플 세트를 유지하고 SDK를 업그레이드할 때, API 파라미터가 바뀔 때, 인증 전략을 변경할 때 다시 실행합니다. 소스 테스트는 예상 순서를 검증하지만, 프로덕션 배포에는 범위를 제한한 실제 입력의 증거가 필요하며 각각의 날짜도 따로 기록해야 합니다. 오래된 스크린샷이나 무료 디렉터리에서 발생한 오류로 현재 인증 환경의 비즈니스 인수 검증을 대신해서는 안 됩니다.

안전한 자가 복구의 목표는 명확히 고칠 수 있는 문제로 사람을 방해하는 일을 줄이고, 실제 판단이 필요할 때는 즉시 실행을 멈추는 것입니다. 각 단계에서 오류가 어디서 발생했는지, 무엇이 바뀌었는지, 실행이 이루어졌는지, 다음 단계가 왜 허용되는지가 분명하다면, 오류 메시지는 끝없는 재시도의 계기가 아니라 실질적인 연동 도구가 됩니다.

참고 LF-12