MCP가 한 번 실패한 뒤 계속 같은 오류를 낼 때: 거부된 Promise 캐시 진단

MCP 도구가 한 번 실패한 뒤 같은 오류만 반복한다면, 재요청이 실제 작업을 다시 시작하는지부터 확인한다. JavaScript에서 거부된 Promise를 변수에 계속 보관하면 다음 호출도 같은 실패를 받는다. 바깥 요청을 다시 보내는 것과 내부 조회 함수를 다시 실행하는 것은 다르다.

이 글은 Node.js로 도구 서버의 조회·초기화 로직을 만드는 개발자를 위한 진단이다. 동일한 오류 문구만으로 Promise 캐시를 원인으로 확정할 수는 없다. 원격 서비스가 계속 실패하는 경우와 이미 끝난 실패를 재사용하는 경우를 나누는 것이 출발점이다.

요청 횟수와 실제 실행 횟수를 따로 센다

테스트 환경의 읽기 전용 작업에서 핸들러 진입 횟수와 실제 로더 실행 횟수를 각각 센다. 캐시 적중 여부, 실행 시작·종료, 성공·실패도 같은 임시 식별자로 연결한다. 인증값, 고객 원문, 전체 URL의 쿼리 문자열은 로그에 남기지 않는다.

두 번 요청했는데 로더는 한 번만 실행됐고, 첫 Promise를 두 번째에도 그대로 반환했다면 이 글의 패턴과 맞는다. 반대로 로더가 매번 실행된다면 원격 오류·권한·요청 조건을 먼저 조사한다. 운영 서버를 재시작해 증상만 없애기 전에 어느 경로인지 확인해야 한다.

실패도 저장하는 짧은 코드

let cached;
function read() {
  return cached ??= load();
}

load가 Promise를 반환한다는 가정에서, 이 코드는 성공값만 기억하는 코드가 아니다. 실패한 Promise 객체도 null이나 undefined가 아니므로 계속 남는다. 같은 객체를 다시 await한다고 load가 재실행되지는 않는다. 성공한 결과를 얼마나 오래 쓸지와 실패를 언제 버릴지는 별도로 정해야 한다.

ECMAScript 2025는 Promise의 pending·fulfilled·rejected 상태와 이미 결정된 Promise의 처리 규칙을 정의한다. 캐시에 실패를 보관할지는 언어가 아니라 애플리케이션의 선택이다. Promise Objects 공식 명세

진행 중인 같은 읽기만 합치는 예제

아래 shareInFlight는 하나의 고정된 자원과 동일한 권한 범위의 읽기를 가정한 설계 예제다. 작업 중 들어온 호출은 같은 Promise를 받고, 성공하거나 실패해 끝나면 참조를 지운다. 완료 결과를 장기 보관하는 캐시나 자동 재시도기는 아니다. 다음 명시적 호출이 있어야 새 작업이 시작된다.

function shareInFlight(load) {
  let inFlight;
  return function read() {
    if (inFlight) return inFlight;
    const current = Promise.resolve()
      .then(() => load())
      .finally(() => {
        if (inFlight === current) inFlight = undefined;
      });
    inFlight = current;
    return current;
  };
}

Promise.resolve().then 안에서 load를 호출하므로 동기 throw도 호출자에게 거부된 Promise로 전달된다. finally에서 정리한 뒤 반환되는 current를 그대로 보관하고 반환한다. 호출자는 이 반환값을 await하거나 catch해야 한다. 정리 함수에는 실패할 수 있는 네트워크 요청을 넣지 않는다.

inFlight === current 검사는 정리할 대상이 아직 같은 작업인지 확인한다. 이 최소 예제에는 강제 교체 기능이 없지만, 나중에 무효화 기능을 붙였을 때 이전 작업의 정리가 새 작업의 참조까지 지우지 않도록 비교를 남겼다. 무효화·취소 구현 자체는 아래 시험 범위에 포함하지 않았다.

finally를 붙이고 원래 Promise만 반환하지 않는다

// 피해야 할 분리된 체인
const original = load();
original.finally(cleanup); // 이 반환 Promise의 거부가 방치될 수 있다
return original;

finally는 원래 Promise를 제자리에서 수정하는 방식이 아니다. 위처럼 파생된 체인을 버리면 호출자가 original의 실패를 처리해도 별도의 거부가 남을 수 있다. 정리 체인까지 반환하거나 await하는 경로를 유지한다. finally 공식 알고리즘과 Node.js v24.19.0 unhandledRejection 문서에서 확인할 수 있다.

직접 실행한 합성 검사의 결과

외부 API나 MCP 호스트 대신 성공·실패 시점을 통제하는 로더로 검사했다. 첫 구현은 두 번째 호출에도 같은 거부된 Promise를 반환했다. 개선 예제는 동시 호출을 하나로 합치고, 첫 실패 뒤 다음 호출에서 로더를 다시 실행했다. 성공 뒤의 다음 호출 역시 새로 실행됐다.

{"node":"v24.19.0","assertions":16,"result":"PASS","badLoaderCalls":1,"goodLoaderCalls":3}

16개 assert가 통과했다. badLoaderCalls는 실패를 보관한 구현의 실제 로더 실행 횟수이고, goodLoaderCalls는 실패·성공·후속 성공의 세 실행을 합친 값이다. 성능 비교나 장애 복구율이 아니다. 실행 옵션은 node --unhandled-rejections=strict였으며, 검사 코드는 모든 실패를 명시적으로 처리했다.

독자가 자신의 코드에 옮길 때는 다음 네 조건을 자동 검사로 남기는 것을 권한다. 동시 두 호출의 실제 로더 실행은 한 번인지, 실패가 두 호출자에게 모두 전달되는지, 종료 후 새 호출에서 다시 실행되는지, 성공 뒤 새 조회가 필요하다는 설계와 일치하는지다. 서버·호스트 통합 시험은 별도로 필요하다.

그대로 적용하면 안 되는 경우

여러 사용자나 검색 조건을 하나로 합치면 안 된다. 이 함수는 인자를 받지 않는 단일 작업용이다. 다른 계정·테넌트·권한·검색 조건·페이지를 처리한다면 공유 범위를 분리해야 한다. 호출마다 인가 검사를 수행하고, 검증된 동일 범위에서만 작업을 공유한다. 비밀 토큰 자체를 캐시 키나 로그로 노출하지 않는다.

영원히 pending인 작업은 정리되지 않는다. 시간초과·취소·동시 작업 수 제한은 별도 설계다. 한 호출자의 취소가 다른 대기자의 공유 작업까지 취소할 수 있는지도 정해야 한다. 이 예제는 이를 해결하지 않는다.

실패 참조를 지웠다고 즉시 반복 호출해도 되는 것은 아니다. 지속 장애에서 요청이 계속 들어오면 원격 호출도 반복될 수 있다. 오류 종류에 따른 대기·상한·백오프를 검토하고, 권한 거절은 우회하지 않는다. 명시적 만료 시간을 둔 실패 캐시가 의도된 시스템이라면 무조건 삭제하기보다 그 정책부터 확인한다.

쓰기에는 같은 재시도 논리를 적용하지 않는다. 저장·발행·결제가 실패 응답을 줬어도 실제 반영됐을 수 있다. Promise 참조를 지우는 것은 원격 변경의 취소나 중복 방지가 아니다. 결과 조회와 해당 API의 멱등성 계약을 먼저 확인한다.

적용 전 기록할 항목

핸들러 진입 횟수 / 로더 실행 횟수:
같은 Promise 참조를 재사용했는가:
공유할 자원·입력·권한 범위:
성공 후 보관 정책 / 실패 후 보관 정책:
호출자의 오류 처리 경로:
시간초과·취소 시 실제 작업 상태:
재호출 허용 조건 / 호출 상한:

확인 범위와 이어 읽기

확인일은 2026년 10월 23일 KST, 실행 환경은 Node.js v24.19.0이다. ECMAScript 2025 확정판과 같은 Node.js 버전의 공식 문서를 대조했다. MCP SDK를 설치하거나 프로토콜 통신을 시험하지 않았으므로 특정 MCP revision·SDK·제품의 호환성을 검증했다고 보지 않는다. 위 결과는 JavaScript 합성 로더의 동작에 한정된다.

기존 async forEach 완료 시점 진단이 기다리지 않은 작업을 다뤘다면, 이번 글은 이미 끝난 실패를 다시 기다리는 문제다. 서로 다른 요청의 값이 섞인다면 요청별 상태 분리를 함께 점검한다. 수정 완료의 기준은 오류 문구가 사라졌다는 것만이 아니라, 새 요청이 의도한 횟수로 실행되고 그 결과가 호출자에게 전달되는 것이다.


다른 글 보기 · 주제 탐색 · 작성자와 편집 기준 · 문의·정정 요청

RUDA DIRECTOR에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기