트래픽이 갑자기 몰릴 때 많은 팀이 먼저 떠올리는 건 스케일 업이나 캐시 증설입니다. 물론 중요합니다. 하지만 실제 장애 현장에서는 자원이 조금 부족한 순간보다, 모든 요청을 끝까지 받으려다가 시스템 전체가 같이 무너지는 순간이 더 치명적입니다. 이때 필요한 발상이 바로 Node.js load shedding 입니다. 감당 불가능한 요청을 무작정 붙잡지 말고, 일부 요청을 의도적으로 빨리 거절해서 핵심 경로를 살리는 전략입니다. 이 글에서는 load shedding이 왜 필요한지, queue·timeout·rate limiting과 무엇이 다른지, 그리고 Node.js API 서버에서 어떤 기준으로 적용해야 하는지 실무 관점에서 정리합니다.

Node.js load shedding은 왜 필요한가

H3. 과부하 구간에서는 처리량보다 생존성이 먼저다

서비스가 정상일 때는 가능한 많은 요청을 성공시키는 것이 중요합니다. 하지만 CPU, 메모리, DB 커넥션 풀, 외부 API 동시성 한계를 넘기기 시작하면 이야기가 달라집니다. 이때 모든 요청을 끝까지 시도하면 아래 현상이 함께 나타납니다.

  • 이벤트 루프 지연 증가
  • 대기열 적체
  • timeout 폭증
  • 재시도 트래픽 추가 유입
  • 핵심 요청까지 같이 실패

즉 과부하 구간에서는 “최대한 많이 처리”보다 “무너지지 않게 적게 받기” 가 더 좋은 전략이 됩니다. load shedding은 바로 이 시점에 불필요하거나 우선순위가 낮은 요청을 조기에 탈락시켜 시스템 전체 생존성을 높입니다.

H3. 느리게 실패하는 것보다 빨리 거절하는 편이 사용자와 시스템 모두에게 낫다

과부하 상황에서 12초 뒤 timeout으로 실패하는 API는 사용자 입장에서도 나쁘고, 서버 입장에서도 더 나쁩니다. 이미 자원을 오래 점유한 뒤 결국 실패했기 때문입니다. 반면 100ms 안에 “지금은 처리 여력이 부족하다”고 응답하면 클라이언트는 fallback을 보여주거나, 잠시 후 다시 시도하거나, 다른 경로로 우회할 수 있습니다.

이 차이는 매우 큽니다. 늦은 실패는 자원을 태우고, 빠른 실패는 자원을 보존합니다. load shedding의 핵심은 성공률을 무조건 높이는 게 아니라, 실패가 불가피할 때 실패 비용을 낮추는 것입니다.

Load shedding과 rate limiting, queue, timeout은 무엇이 다를까

H3. rate limiting은 입구 제어, load shedding은 실시간 생존 전략에 가깝다

둘 다 요청을 막는다는 점에서 비슷해 보이지만 목적이 다릅니다.

  • rate limiting: 사용자·IP·토큰 단위로 과도한 사용량을 제한
  • load shedding: 서버 내부 상태가 위험할 때 실시간으로 요청 일부를 탈락
  • timeout: 오래 기다리지 않도록 상한선 부여
  • queue: 잠깐의 버스트를 흡수하기 위한 대기 공간

예를 들어 rate limiting은 악성 트래픽이나 과도한 사용을 장기적으로 제어하는 정책이고, load shedding은 정상 트래픽이 몰려도 서버가 스스로 살아남기 위한 방어선에 가깝습니다. 관련해서 API 남용 방어가 궁금하다면 Node.js Rate Limiting 가이드를 같이 보는 편이 좋습니다.

H3. queue는 버스트를 흡수하지만, 무한 대기열은 장애를 키운다

짧은 순간의 트래픽 급증은 queue가 잘 흡수해줄 수 있습니다. 하지만 처리 속도보다 유입 속도가 오래 높으면 queue는 해결책이 아니라 지연 저장소가 됩니다. 대기열이 길어질수록 응답 시간은 악화되고, 결국 timeout과 재시도가 겹치면서 더 큰 압박이 생깁니다.

그래서 실무에서는 보통 이렇게 생각합니다.

  1. 짧은 버스트는 queue로 흡수한다
  2. 일정 한도를 넘으면 load shedding으로 탈락시킨다
  3. 핵심 경로는 별도 자원으로 보호한다

즉 queue와 load shedding은 경쟁 관계가 아니라, 버스트 흡수 이후의 안전장치 관계입니다.

어떤 상황에서 load shedding을 도입해야 할까

H3. p99 지연과 대기열 폭증이 같이 보이면 도입 신호다

아래 징후가 반복되면 load shedding을 진지하게 검토할 시점입니다.

  • 평소엔 빠르지만 피크 시간에 p99가 급격히 치솟는다
  • CPU보다 먼저 DB 커넥션 풀이나 worker 슬롯이 바닥난다
  • timeout 이후 재시도 때문에 같은 장애가 증폭된다
  • 추천·검색·통계 같은 부가 기능이 핵심 API까지 끌어내린다
  • autoscaling이 따라오기 전에 순간 과부하가 자주 발생한다

이런 상황에서는 용량만 늘리기보다, 어떤 요청을 포기해도 되는지 먼저 정의하는 것이 더 중요합니다.

H3. 모든 요청을 똑같이 취급하면 중요한 기능도 같이 죽는다

예를 들어 커머스 서비스라면 아래 요청은 우선순위가 다릅니다.

  • 결제 승인
  • 주문 조회
  • 홈 추천 위젯
  • 비로그인 사용자 개인화 배너
  • 실시간 인기 검색어 집계

과부하 순간에 위 요청을 모두 동일하게 받으면, 매출과 직결된 경로가 부가 기능과 같은 경쟁 풀에서 싸우게 됩니다. 이럴 때 load shedding은 “무엇을 먼저 포기할지”를 결정해주는 정책입니다. 예를 들면 추천 위젯은 503 + fallback으로 빠르게 내리고, 결제·주문 조회는 남기는 식입니다.

Node.js에서 load shedding은 어떻게 구현할까

H3. 동시 처리 수와 대기열 길이에 상한을 두는 방식이 가장 단순하고 효과적이다

Node.js에서는 이벤트 루프 하나가 병목이 되기 쉽기 때문에, 무제한으로 작업을 받아두는 설계가 특히 위험합니다. 실무적으로는 아래 두 값부터 정하는 것이 좋습니다.

  • 최대 동시 처리 수
  • 최대 대기열 길이

간단한 개념 예시는 아래와 같습니다.

class OverloadGate {
  private inFlight = 0;
  constructor(
    private readonly maxInFlight: number,
    private readonly maxQueueDepth: number,
  ) {}

  canAccept(currentQueueDepth: number) {
    if (this.inFlight >= this.maxInFlight) return false;
    if (currentQueueDepth >= this.maxQueueDepth) return false;
    return true;
  }

  async run<T>(currentQueueDepth: number, task: () => Promise<T>): Promise<T> {
    if (!this.canAccept(currentQueueDepth)) {
      const error = new Error('SERVER_OVERLOADED');
      (error as any).statusCode = 503;
      throw error;
    }

    this.inFlight += 1;
    try {
      return await task();
    } finally {
      this.inFlight -= 1;
    }
  }
}

핵심은 복잡한 알고리즘보다, 감당 가능한 한도를 먼저 숫자로 고정하는 것입니다. 처음엔 거칠어 보여도 무한 수용보다는 훨씬 안전합니다.

H3. 이벤트 루프 지연, 커넥션 풀 포화, 에러율을 함께 봐야 정확하다

단순히 요청 수만 기준으로 삼으면 실제 과부하를 놓칠 수 있습니다. 예를 들어 요청 수는 많지 않아도 외부 API가 느려져 커넥션 풀이 묶이면 서비스는 이미 위험 구간일 수 있습니다. 그래서 load shedding 조건은 한 가지보다 여러 지표 조합이 좋습니다.

대표적인 신호는 아래와 같습니다.

  • in-flight request 수
  • queue depth
  • event loop lag
  • DB pool 사용률
  • upstream timeout 비율
  • 5xx 증가율

이 지표를 임계치 기반으로 묶어서, 일정 기준을 넘으면 우선순위가 낮은 엔드포인트부터 shed 하도록 설계할 수 있습니다.

어떤 요청을 먼저 shed 해야 할까

H3. 읽기이면서 부가 가치가 낮은 기능부터 탈락시키는 편이 안전하다

보통 아래 순서가 현실적입니다.

  1. 실시간 통계, 실험 배너, 추천 위젯
  2. 무거운 검색/집계 API
  3. 비로그인 사용자의 선택 기능
  4. 핵심 거래와 직접 연결되지 않은 부가 조회

반대로 아래 요청은 최대한 보호해야 합니다.

  • 인증
  • 결제
  • 주문 생성/조회
  • 관리자 장애 대응 기능
  • 내부 health check와 운영 경로

이 구조는 Graceful Degradation 가이드와 같이 봐야 더 잘 설계됩니다. 무조건 막는 게 아니라, 막아도 되는 기능은 빠르게 내려놓고 핵심 기능은 끝까지 살리는 것이 목적이기 때문입니다.

H3. 자원 격리가 없으면 shed 정책이 있어도 핵심 요청이 휘말릴 수 있다

load shedding만 있고 자원 격리가 없으면 정책은 있어도 효과가 약합니다. 추천 API와 결제 API가 같은 커넥션 풀, 같은 worker, 같은 외부 의존성 풀을 공유하면 저우선순위 요청이 이미 자원을 잠식한 뒤일 수 있기 때문입니다.

이럴 때는 Bulkhead Pattern 가이드처럼 기능별 리소스 분리를 함께 도입해야 합니다. load shedding은 탈락 정책이고, bulkhead는 침범 방지 정책입니다. 둘을 같이 써야 보호 효과가 커집니다.

응답 코드는 어떻게 설계해야 할까

H3. 503과 Retry-After를 일관되게 쓰면 클라이언트가 더 똑똑하게 대응할 수 있다

과부하로 인한 의도적 거절이라면 보통 503 Service Unavailable이 자연스럽습니다. 상황에 따라 Retry-After 헤더를 함께 보내면 클라이언트가 재시도 타이밍을 더 잘 조절할 수 있습니다.

예를 들면 아래처럼 응답할 수 있습니다.

HTTP/1.1 503 Service Unavailable
Retry-After: 5
Content-Type: application/json

{
  "code": "SERVER_OVERLOADED",
  "message": "일시적으로 요청이 많아 일부 기능을 제한하고 있습니다. 잠시 후 다시 시도해주세요."
}

중요한 건 사용자 메시지와 내부 에러 코드를 구분하는 것입니다. 외부에는 과도한 공포감을 주지 않되, 내부 관측 시스템에서는 “정책에 의한 shed”인지 “예상 못 한 장애”인지 명확히 구분할 수 있어야 합니다.

H3. 무분별한 자동 재시도는 shed 정책을 무력화할 수 있다

클라이언트가 503을 받자마자 즉시 재시도하면, 서버는 살기 위해 요청을 줄였는데 오히려 더 많은 요청이 들어오게 됩니다. 그래서 load shedding을 도입했다면 재시도 정책도 함께 조정해야 합니다.

  • 즉시 재시도 금지
  • exponential backoff 적용
  • jitter 추가
  • 최대 재시도 횟수 제한
  • 사용자 액션 기반 재시도 우선

이 부분은 Exponential Backoff + Jitter 가이드와 연결해서 보면 훨씬 실용적입니다. 서버가 살아남으려는 순간, 클라이언트도 같이 협조해야 합니다.

운영에서 자주 하는 실수

H3. 임계치를 너무 높게 잡아 shed가 시작될 때는 이미 늦는 경우

많이 하는 실수는 “정말 위험해졌을 때만 막자”입니다. 하지만 실제로는 위험 신호가 보일 때는 이미 대기열이 쌓이고 응답 시간이 나빠지기 시작한 뒤인 경우가 많습니다. load shedding은 마지막 순간의 스위치가 아니라, 완전 붕괴 전에 작동하는 완충 장치여야 합니다.

처음에는 보수적으로 설정해도 괜찮습니다. 중요한 건 아래 데이터를 보며 조정하는 것입니다.

  • shed 비율
  • shed 이후 p95/p99 변화
  • 핵심 엔드포인트 성공률
  • 업스트림 timeout 감소폭
  • 사용자 이탈 또는 전환 영향

H3. 모든 엔드포인트에 같은 기준을 적용하는 경우

/checkout/recommendations 는 중요도와 비용이 다릅니다. 그런데 같은 in-flight 상한, 같은 timeout, 같은 shed 기준을 적용하면 정책이 지나치게 단순해집니다. 실무에서는 엔드포인트 그룹별로 다르게 가져가는 편이 더 낫습니다.

  • 핵심 거래 경로: 높은 보호, 낮은 허용 실패율
  • 부가 기능 경로: 빠른 shed, fallback 우선
  • 내부 배치/백오피스: 사용자 트래픽과 분리

이렇게 나누면 시스템 전체를 하나의 거대한 큐로 취급하는 실수를 줄일 수 있습니다.

실무 체크리스트

H3. 도입 전에 이 항목부터 확인하면 시행착오를 줄일 수 있다

  • 어떤 요청이 핵심 경로인지 정의했는가
  • 어떤 기능은 fallback으로 대체 가능한가
  • 동시 처리 수와 queue 상한을 숫자로 정했는가
  • shed 조건을 요청 수 외 지표와 함께 볼 수 있는가
  • 503 응답과 Retry-After를 클라이언트가 올바르게 처리하는가
  • retry 정책이 과부하 순간을 더 악화시키지 않는가
  • shed 이벤트를 별도 메트릭으로 관측하는가

마무리

Node.js load shedding은 “요청을 거절하는 기술”이라서 처음엔 방어적으로 들릴 수 있습니다. 하지만 실제로는 전체 서비스를 살리기 위한 매우 현실적인 선택입니다. 감당할 수 없을 때 모두를 붙잡으려는 시스템보다, 덜 중요한 요청을 먼저 내려놓고 중요한 요청을 지키는 시스템이 더 건강합니다.

트래픽 급증이나 외부 의존성 지연이 반복되는 서비스라면, 스케일링만 고민하기 전에 load shedding 정책부터 정의해보는 편이 좋습니다. 특히 rate limiting, bulkhead, graceful degradation, retry 전략과 함께 설계하면 과부하 순간의 장애 반경을 훨씬 작게 만들 수 있습니다.