평균 응답 시간만 보면 괜찮은데, 가끔씩 3초·5초짜리 느린 요청이 튀어나와 사용자 경험을 망치는 서비스가 있습니다. 이런 문제는 전체가 다 느린 것보다 더 까다롭습니다. 대부분의 요청은 정상이라 모니터링 대시보드 평균값으로는 잘 안 드러나는데, 실제 사용자는 그 느린 몇 번의 경험을 더 강하게 기억하기 때문입니다. 이 글에서는 Node.js request hedging 으로 tail latency를 줄이는 방법, 어떤 요청에 적용해야 하는지, 그리고 timeout·circuit breaker·bulkhead와 어떻게 함께 설계해야 하는지를 실무 관점에서 정리합니다.

Node.js request hedging이 왜 필요한가

H3. 평균이 아니라 p95·p99가 사용자 체감을 좌우하는 경우가 많다

서비스 운영에서 자주 보는 착시는 평균 응답 시간만 보고 안심하는 것입니다. 예를 들어 90% 요청이 150ms 안에 끝나더라도, 10%가 3초 이상 걸리면 사용자는 서비스가 느리다고 느낄 수 있습니다. 특히 검색 자동완성, 상품 상세, 추천 API, 내부 마이크로서비스 연쇄 호출 같은 경로에서는 tail latency 가 UX를 무너뜨리는 주범이 됩니다.

request hedging은 이런 “가끔 비정상적으로 느린 요청”에 대응하는 전략입니다. 첫 요청이 너무 늦어질 때 동일한 요청을 다른 복제 경로로 한 번 더 보내고, 더 빨리 끝난 응답만 채택하는 방식입니다. 핵심은 평균 성능 최적화가 아니라, 꼬리처럼 길게 늘어지는 느린 응답을 잘라내는 데 있습니다.

H3. 느린 요청 하나가 전체 연쇄 지연으로 번질 수 있다

Node.js 백엔드에서는 요청 하나가 끝나기 전에 여러 의존성을 순서대로 호출하는 일이 흔합니다. 예를 들어 API Gateway → 사용자 프로필 서비스 → 추천 서비스 → 캐시/DB 조회처럼 이어지면, 중간에 하나만 느려져도 최종 응답이 크게 밀립니다.

이때 첫 시도가 비정상적으로 오래 걸릴 때 대체 시도를 만들어두면, 전체 연쇄 지연을 줄일 수 있습니다. 물론 아무 요청이나 두 번 보내면 비용과 부하가 늘어나기 때문에, hedging은 선별적으로 써야 합니다.

Request hedging은 정확히 어떻게 동작할까

H3. 첫 요청이 일정 시간 이상 지연되면 두 번째 요청을 시작한다

가장 단순한 흐름은 이렇습니다.

  1. 원본 요청을 먼저 보낸다
  2. 짧은 hedge delay 동안 기다린다
  3. 아직 응답이 없으면 동일 요청을 한 번 더 보낸다
  4. 먼저 끝난 응답을 채택한다
  5. 늦게 끝난 요청은 취소하거나 결과를 버린다

이 방식의 핵심은 “처음부터 두 번 보내는 것”이 아니라 지연이 감지될 때만 복제 요청을 추가한다는 점입니다. 그래야 평소 비용 증가를 줄이면서도 tail latency만 겨냥할 수 있습니다.

H3. hedging은 retry와 비슷해 보여도 목적이 다르다

retry는 실패 후 다시 시도하는 전략입니다. 반면 hedging은 아직 실패가 확정되지 않았더라도, 너무 느린 요청을 실패에 준하는 위험 신호로 보고 병렬 대체 경로를 여는 전략입니다.

차이를 간단히 정리하면 아래와 같습니다.

  • retry: 에러나 timeout 이후 재시도
  • hedging: 느림 자체를 기준으로 추가 시도
  • timeout: 기다릴 상한선 정의
  • circuit breaker: 연속 실패 구간 빠르게 차단

그래서 hedging은 retry를 대체하기보다는, 느린 꼬리 구간을 줄이고 싶은 특정 호출에 별도로 붙는 패턴에 가깝습니다.

어떤 요청에 request hedging을 적용해야 할까

H3. 읽기 요청과 멱등성이 보장된 호출에 더 잘 맞는다

hedging은 같은 요청을 둘 이상 보낼 수 있으므로, 부작용 없는 읽기 요청에 가장 잘 맞습니다. 대표적으로 아래 같은 경우가 실전 적용 후보입니다.

  • 캐시/리드 레플리카 조회
  • 검색 인덱스 조회
  • 추천 결과 조회
  • 프로필/설정 읽기 API
  • 멱등성이 보장된 내부 GET 호출

반대로 아래처럼 상태를 바꾸는 쓰기 요청에는 매우 신중해야 합니다.

  • 결제 승인
  • 재고 차감
  • 쿠폰 발급
  • 포인트 적립
  • 외부 파트너에 중복 부과가 가능한 요청

쓰기 요청에 hedging을 걸면 중복 처리 위험이 생길 수 있습니다. 이런 구간은 idempotency key 가이드 같은 중복 방지 장치가 먼저 있어야 합니다.

H3. 모든 느린 API에 적용하면 오히려 시스템 비용이 커진다

hedging은 tail latency를 줄이는 대신, 추가 요청이라는 비용을 지불합니다. 그래서 아래 조건을 만족할수록 더 적합합니다.

  • p95는 괜찮지만 p99 꼬리가 유독 긴 API
  • 요청 단가가 낮고 읽기 위주인 API
  • 늦은 응답이 UX에 직접 악영향을 주는 경로
  • 복제 요청이 전체 시스템 부하를 감당 가능한 수준으로만 늘리는 경우
  • 취소 신호 전파가 가능한 클라이언트/서버 구조

즉 hedging은 “느린 API니까 다 두 번 보내자”가 아니라, 느린 꼬리 때문에 손해 보는 가치가 추가 비용보다 큰 곳에만 적용해야 합니다.

Node.js에서 request hedging은 어떻게 구현할까

H3. AbortController로 패자 요청을 빨리 정리하는 것이 중요하다

Node.js에서 hedging을 구현할 때 중요한 점은 두 번째 요청을 보내는 것보다, 이긴 요청 외 나머지를 빨리 취소하는 것입니다. 취소가 안 되면 tail latency는 줄어도 전체 리소스 낭비가 커집니다.

아래는 개념을 단순화한 예시입니다.

async function hedgedFetch(url: string, hedgeDelayMs = 120) {
  const primaryController = new AbortController();
  const hedgeController = new AbortController();

  const primary = fetch(url, { signal: primaryController.signal });

  let hedgeStarted = false;
  const hedgeTimer = setTimeout(() => {
    hedgeStarted = true;
  }, hedgeDelayMs);

  await new Promise((resolve) => setTimeout(resolve, hedgeDelayMs));

  const candidates = [
    primary.then(async (res) => ({ winner: 'primary', res })),
  ];

  if (hedgeStarted) {
    const hedge = fetch(url, { signal: hedgeController.signal })
      .then(async (res) => ({ winner: 'hedge', res }));
    candidates.push(hedge);
  }

  try {
    const result = await Promise.race(candidates);

    if (result.winner === 'primary') {
      hedgeController.abort();
    } else {
      primaryController.abort();
    }

    return result.res;
  } finally {
    clearTimeout(hedgeTimer);
  }
}

실서비스에서는 이보다 더 세밀한 예외 처리, metrics, hedge 시작 조건, 최대 동시 hedge 수 제한이 필요합니다. 그래도 본질은 같습니다. 느린 요청을 하나 더 복제하되, 이긴 요청만 남기고 나머지는 빨리 정리해야 합니다.

H3. hedge delay는 p95 근처에서 실험적으로 잡는 편이 현실적이다

hedge delay를 너무 짧게 잡으면 거의 모든 요청이 복제되어 부하가 급증합니다. 반대로 너무 길면 실제 tail latency 개선 효과가 약합니다.

실무에서는 보통 아래 기준으로 시작합니다.

  • 해당 API의 p95 또는 p97 지연 시간 근처
  • 사용자 체감 임계치를 넘기기 직전 시점
  • 인프라 비용 상승 폭을 감당 가능한 구간

예를 들어 p95가 120ms, p99가 1.8초인 API라면 100~150ms 구간부터 실험할 수 있습니다. 중요한 건 한 번에 정답을 찾는 게 아니라, hedge 비율·추가 부하·tail latency 감소폭을 함께 보며 조정하는 것입니다.

timeout, circuit breaker, bulkhead와는 어떻게 연결될까

H3. timeout이 없으면 hedge 요청도 같이 오래 매달릴 수 있다

hedging을 붙였다고 timeout이 필요 없어지는 건 아닙니다. 원본 요청과 hedge 요청 모두 오래 붙잡혀 있으면 오히려 시스템 자원만 더 먹게 됩니다. 그래서 Node.js AbortController timeout 가이드에서 다룬 것처럼 각 시도마다 명확한 timeout 예산이 있어야 합니다.

정리하면 아래 순서가 좋습니다.

  • 전체 요청 deadline 정의
  • 각 시도별 timeout 정의
  • 일정 지연 이상이면 hedge 시작
  • 승자 외 요청 즉시 취소

즉 hedging은 timeout 위에 얹히는 최적화이지, timeout을 대체하는 안전장치가 아닙니다.

H3. circuit breaker 없이 장애 구간에 hedge를 남발하면 더 아플 수 있다

업스트림이 실제로 고장 난 상태라면 hedging은 실패 요청을 두 배로 만들 수 있습니다. 그래서 circuit breaker 패턴 가이드처럼, 실패율이 높아지는 구간에서는 빠르게 차단하는 보호막이 필요합니다.

좋은 조합은 보통 이렇습니다.

  • 평소 tail latency만 길 때: 제한적 hedging
  • 실제 장애/에러율 급증 시: circuit breaker 우선 차단
  • 느리지만 optional인 기능: fallback 또는 graceful degradation

즉 “느림 완화”와 “장애 차단”은 같은 문제가 아닙니다. hedging은 전자에 강하고, circuit breaker는 후자에 강합니다.

H3. bulkhead가 없으면 hedge 트래픽이 핵심 자원까지 침범할 수 있다

hedging은 의도적으로 요청 수를 늘리는 패턴이라서 자원 격리가 더 중요해집니다. 관련해서는 bulkhead pattern 가이드와 이어서 보는 편이 좋습니다.

예를 들어 추천 API hedging을 도입했는데 같은 커넥션 풀·같은 워커 슬롯을 결제 API와 공유하면, tail latency를 줄이려다 핵심 경로 자원을 더 압박할 수 있습니다. 그래서 hedging 대상 기능은 아래를 함께 가져가야 합니다.

  • 기능별 동시성 상한
  • hedge 가능 요청 수 상한
  • 대기열 상한
  • 패자 요청 취소율 관측

Request hedging을 쓸 때 자주 하는 실수

H3. 성공률이 아니라 비용과 부하를 같이 보지 않는 실수

hedging을 적용하면 p99가 예쁘게 내려갈 수 있습니다. 하지만 동시에 호출 수, 네트워크 비용, 업스트림 QPS가 올라갈 수 있습니다. 이걸 안 보고 지연 시간만 보면 나중에 비용 폭탄이나 rate limit 문제를 만날 수 있습니다.

최소한 아래 지표는 함께 봐야 합니다.

  • hedge 시작 비율
  • hedge 승리 비율
  • 패자 요청 취소 성공률
  • 전체 QPS 증가율
  • p95/p99 개선폭
  • 업스트림 429/5xx 변화

이 수치를 보면 “정말 의미 있는 꼬리 절감인지” 아니면 “부하를 늘려서 겨우 조금 빨라진 것인지” 구분하기 쉬워집니다.

H3. 멱등성 검증 없이 POST 요청에 붙이는 실수

이건 가장 위험한 실수입니다. 같은 요청을 둘 이상 보내도 안전한지 검증되지 않은 상태에서 hedging을 쓰면, 중복 주문·중복 과금·중복 이벤트 발행 같은 사고로 이어질 수 있습니다.

상태 변경 요청이라면 먼저 아래를 확인해야 합니다.

  • idempotency key 지원 여부
  • 서버 측 중복 감지 로직 존재 여부
  • 외부 공급자도 중복 보호를 지원하는지
  • late duplicate가 와도 안전한지

이게 불명확하면 hedging보다 timeout·retry 축소·비동기 분리가 우선입니다.

실무 체크리스트: 도입 전에 무엇을 확인해야 할까

H3. 이 질문들에 답할 수 있으면 절반은 끝난다

실서비스 도입 전에는 아래를 먼저 점검하는 편이 좋습니다.

  • 이 호출은 읽기 요청인가, 멱등성이 충분한가
  • p95 대비 p99 꼬리가 실제로 심한가
  • hedge delay는 어떤 데이터로 정할 것인가
  • 패자 요청을 정말 취소할 수 있는가
  • hedge 트래픽 상한은 어떻게 둘 것인가
  • 업스트림 비용이나 rate limit는 감당 가능한가
  • circuit breaker, timeout, fallback과 충돌하지 않는가

이 질문들에 답이 흐리면, hedging은 멋있어 보여도 운영 부담만 늘릴 수 있습니다.

마무리

Node.js request hedging의 핵심은 모든 요청을 더 많이 보내는 데 있지 않습니다. 느린 꼬리 구간만 정밀하게 겨냥해, 사용자에게 체감되는 지연을 줄이는 것에 있습니다.

잘 쓰면 p99를 크게 낮춰 UX를 개선할 수 있지만, 잘못 쓰면 비용·부하·중복 요청 위험만 늘어납니다. 그래서 읽기 위주, 멱등성이 분명한 경로부터 작게 실험하고, hedge 비율과 취소율, 업스트림 부하까지 같이 봐야 합니다.

오늘 바로 점검해볼 질문은 이것입니다. 내 Node.js 서비스에서 평균은 괜찮은데 p99만 유독 긴 API가 있고, 그 요청이 정말 hedging으로 해결할 가치가 있는가?