Node.js에서 Bulkhead Pattern 구현하기

발행: (2025년 12월 16일 오후 08:41 GMT+9)
4 min read
원문: Dev.to

Source: Dev.to

시스템 복원력 소개

분산 시스템이나 마이크로서비스 아키텍처에서는 하나의 실패한 컴포넌트가 파급 효과를 일으켜 전체 애플리케이션을 다운시킬 수 있습니다. 이는 데이터베이스나 하위 서비스가 느려질 때 흔히 나타납니다. 들어오는 요청이 계속 쌓이면서 메모리와 CPU 사이클을 소모하고, 결국 Node.js 이벤트 루프가 고갈됩니다.

Bulkhead 패턴은 중요한 리소스를 격리하기 위해 사용하는 구조적 설계입니다. 특정 리소스(예: 데이터베이스)에 도달할 수 있는 동시 작업 수를 제한함으로써 트래픽 급증이나 데이터베이스 지연이 모든 서버 자원을 소모하지 않도록 보장합니다.

세마포어는 Bulkhead를 구현하는 동기화 원시 객체입니다. 한 번에 하나의 작업만 진행하도록 하는 뮤텍스와 달리, 세마포어는 정의된 수(N)만큼의 동시 작업을 허용합니다.

Node.js 환경에서 우리는 세마포어를 사용해 다음을 관리합니다:

  • Counter: 현재 진행 중인 비동기 작업 수를 추적합니다.
  • Queue: 동시성 제한에 도달한 뒤 도착한 작업들의 resolve 함수를 저장합니다.
  • Wait (P) / Signal (V): 각각 슬롯을 요청하거나 해제하는 연산입니다.

클래스는 최대 동시 작업 수를 정의하는 concurrencyLimit와 무한 대기열로 인한 메모리 고갈을 방지하기 위한 queueLimit를 필요로 합니다.

Bulkhead 구현

class Bulkhead {
  constructor(concurrencyLimit, queueLimit = 100) {
    this.concurrencyLimit = concurrencyLimit;
    this.queueLimit = queueLimit;
    this.activeCount = 0;
    this.queue = [];
  }

  async run(task) {
    // Admission Control and Wait Logic
    if (this.activeCount >= this.concurrencyLimit) {
      if (this.queue.length >= this.queueLimit) {
        throw new Error("Bulkhead capacity exceeded: Server Busy");
      }

      await new Promise((resolve) => {
        this.queue.push(resolve);
      });
    }

    this.activeCount++;

    try {
      // Execution of the asynchronous task
      return await task();
    } finally {
      // Release Logic
      this.activeCount--;
      if (this.queue.length > 0) {
        const nextInLine = this.queue.shift();
        nextInLine();
      }
    }
  }
}

Mongoose와 함께 Bulkhead 사용하기

const dbBulkhead = new Bulkhead(5, 10);

app.get('/data', async (req, res) => {
  try {
    const result = await dbBulkhead.run(() => User.find().lean());
    res.json(result);
  } catch (error) {
    res.status(503).json({ message: error.message });
  }
});

데이터베이스 호출을 run 메서드로 감싸면, 동시에 1,000개의 요청이 API 엔드포인트에 도달하더라도 실제로 데이터베이스에 접근하는 요청 수는 제어된 수만큼만 이루어집니다.

핵심 기술 요점

  • Fail‑Fast (Admission Control): queue.length를 확인해 요청을 즉시 거부(HTTP 503)함으로써 요청이 오래 대기하며 RAM을 소모하는 상황을 방지합니다.
  • Error Isolation: finally 블록은 데이터베이스 쿼리가 실패하더라도 activeCount가 감소하도록 보장하여 다음 대기 작업이 진행될 수 있게 합니다.
  • Resource Management: 마이크로서비스 환경에서는 동시성 제한을 전체 DB 연결 수 / 서비스 인스턴스 수 로 계산해야 합니다.

💡 질문이 있나요? 댓글로 남겨 주세요!

Back to Blog

관련 글

더 보기 »