API 서버를 운영하다 보면 특정 외부 API 하나가 느려졌을 뿐인데 전체 서비스 응답이 같이 무너지는 순간이 있습니다. 결제 연동이 막히자 주문 조회까지 느려지고, 추천 시스템이 지연되자 로그인 요청까지 대기열에 갇히는 식입니다. 문제의 핵심은 개별 기능 장애보다 공유 자원이 한꺼번에 잠식되는 구조에 있습니다. 이때 유용한 설계가 bulkhead pattern입니다. 배의 격벽처럼 리소스를 구획으로 나눠 한 영역의 침수가 전체 침몰로 번지지 않도록 만드는 방식입니다. 이 글에서는 Node.js 서비스에서 bulkhead pattern이 왜 필요한지, 어디를 먼저 분리해야 하는지, circuit breaker·timeout·queue와는 어떻게 조합하면 좋은지 실무 관점에서 정리합니다.

Node.js bulkhead pattern은 왜 필요한가

H3. 장애보다 더 무서운 것은 느린 의존성이 공용 풀을 잠식하는 상황이다

백엔드 장애는 꼭 프로세스 크래시 형태로만 오지 않습니다. 오히려 더 자주 보는 것은 느려진 외부 시스템이 애플리케이션 내부 자원을 갉아먹는 패턴입니다. 예를 들어 아래 같은 상황입니다.

  • 외부 결제 API 응답 지연으로 worker 스레드나 DB connection 사용 시간이 길어짐
  • 이미지 처리 작업이 CPU를 오래 점유해 API 응답 시간이 전반적으로 악화됨
  • 추천 시스템 호출이 쌓이면서 event loop 지연과 메모리 사용량이 커짐
  • 특정 테넌트의 급격한 트래픽이 공용 큐를 채워 다른 고객 요청까지 밀어냄

이때 문제는 “그 기능만 느리다”에서 끝나지 않습니다. 공유하던 connection pool, thread pool, rate limit budget, in-memory queue까지 같이 잠식되면 멀쩡한 기능도 연쇄적으로 느려지거나 실패합니다. bulkhead pattern은 바로 이 전파를 줄이기 위한 장치입니다.

H3. 회복탄력성은 실패를 막는 것이 아니라 실패 범위를 작게 만드는 데서 시작된다

실무에서는 모든 장애를 예방할 수 없습니다. 외부 API 장애, 일시적 네트워크 지연, 예상 밖의 burst traffic은 언제든 옵니다. 그래서 복원력 설계에서는 “실패를 0으로 만들기”보다 실패해도 피해 반경을 제한하는 구조가 중요합니다.

bulkhead pattern의 핵심 질문은 단순합니다.

  • 이 기능이 느려져도 다른 핵심 기능은 살아남는가
  • 한 고객군의 폭주가 전체 고객 경험을 무너뜨리지 않는가
  • 배치 작업이 실시간 요청 자원을 같이 먹고 있지 않은가

이 질문에 불안한 지점이 있다면 bulkhead가 필요한 신호일 가능성이 큽니다.

bulkhead pattern은 어떻게 동작하나

H3. 공유 자원을 기능별·우선순위별로 분리해 한 구획의 문제를 다른 구획에 퍼뜨리지 않는다

bulkhead pattern은 리소스를 아예 따로 운영하거나, 최소한 논리적으로라도 분리하는 접근입니다. 분리 대상은 생각보다 다양합니다.

  • HTTP client connection pool
  • DB connection pool
  • worker queue와 concurrency limit
  • thread pool 또는 별도 프로세스
  • rate limiter budget
  • 캐시 namespace와 fallback 경로

예를 들어 주문 API와 추천 API가 같은 외부 HTTP agent 설정과 같은 작업 큐를 사용하고 있다고 가정해보겠습니다. 추천 API가 느려지면 주문 API도 연결 대기와 타임아웃 영향을 같이 받을 수 있습니다. 반대로 두 기능이 서로 다른 concurrency limit와 timeout budget을 갖고 있으면 추천 쪽 문제가 주문 흐름 전체로 퍼질 가능성이 크게 줄어듭니다.

H3. 완전 분리만 답은 아니고, 먼저 제한선과 격리 경계를 세우는 것부터 시작하면 된다

많은 팀이 bulkhead를 너무 거창하게 생각합니다. 하지만 처음부터 서비스를 물리적으로 완전히 쪼갤 필요는 없습니다. 실무에서는 아래 순서만으로도 효과가 큽니다.

  1. 느린 의존성 호출 구간 파악
  2. 해당 구간별 timeout과 concurrency 상한 지정
  3. 핵심 경로와 비핵심 경로 큐 분리
  4. fallback 가능한 기능은 degrade 허용
  5. 모니터링으로 어떤 구획이 먼저 포화되는지 추적

즉 bulkhead는 마이크로서비스로 독립 배포하는 이야기만이 아니라, 같은 Node.js 서비스 안에서도 자원 사용 경계를 명확히 만드는 습관에 가깝습니다.

Node.js에서 어디를 먼저 격리해야 할까

H3. 외부 API 호출, 비동기 작업, CPU 집약 로직이 1차 후보군이다

Node.js 서비스에서 bulkhead를 우선 적용할 만한 구간은 대체로 비슷합니다.

  • 결제, 메시징, 서드파티 SaaS 같은 외부 API 호출
  • 이메일 발송, 썸네일 생성, 리포트 집계 같은 비동기 작업
  • 이미지 변환, 압축, 암호화처럼 CPU 사용량이 큰 처리
  • 검색 인덱싱, 추천, 분석 적재처럼 실패 허용도가 상대적으로 높은 부가 기능

이 구간들의 공통점은 느려질 가능성이 있고, 핵심 사용자 플로우와 분리 가치가 높다는 점입니다. 예를 들어 로그인, 결제 승인, 주문 생성 같은 핵심 요청과 추천 위젯 갱신이 같은 자원 제한을 공유하는 구조는 피하는 편이 좋습니다.

H3. 배치와 실시간 요청이 같은 풀을 쓰고 있다면 거의 항상 개선 여지가 있다

운영 중인 시스템에서 자주 보이는 문제는 배치 작업과 온라인 트래픽이 같은 자원을 먹는 구조입니다. 예를 들면 아래와 같습니다.

  • 새벽 정산 배치가 DB pool을 오래 점유해 아침 API latency가 튐
  • 대량 webhook 재처리 job이 실시간 주문 처리 queue를 밀어냄
  • 관리자용 export 요청이 일반 사용자 API worker를 같이 잠식함

이 경우 bulkhead를 적용하면 효과가 선명합니다. 배치 전용 queue, 별도 worker concurrency, 독립적인 DB connection budget만 분리해도 실시간 사용자 경험이 안정되는 경우가 많습니다.

Node.js bulkhead pattern 구현 예시

H3. 기능별 concurrency limit를 분리하면 가장 빠르게 체감 효과를 볼 수 있다

아래 예시는 외부 API 호출을 기능별로 서로 다른 제한선 안에서 실행하는 단순한 방식입니다.

import pLimit from 'p-limit';

const paymentLimit = pLimit(5);
const recommendationLimit = pLimit(20);

async function callPaymentApi(payload) {
  return paymentLimit(async () => {
    return fetch('https://payment.example.com/charge', {
      method: 'POST',
      body: JSON.stringify(payload),
      headers: { 'Content-Type': 'application/json' },
      signal: AbortSignal.timeout(3000)
    });
  });
}

async function callRecommendationApi(userId) {
  return recommendationLimit(async () => {
    return fetch(`https://recommend.example.com/users/${userId}`, {
      signal: AbortSignal.timeout(800)
    });
  });
}

핵심은 두 호출이 같은 애플리케이션 안에 있어도 동일한 동시성 예산을 공유하지 않는다는 점입니다. 결제 API가 느려져도 recommendation limit 전체를 같이 고갈시키지 않고, 반대로 추천 호출이 폭주해도 payment 경로가 완전히 잠식될 가능성을 줄일 수 있습니다.

H3. BullMQ나 작업 큐를 쓴다면 queue와 worker 자체를 목적별로 나누는 편이 안전하다

비동기 작업에서는 queue 분리가 bulkhead의 핵심이 되는 경우가 많습니다.

import { Queue, Worker } from 'bullmq';

const criticalQueue = new Queue('critical-jobs');
const nonCriticalQueue = new Queue('non-critical-jobs');

new Worker('critical-jobs', handleCriticalJob, {
  concurrency: 10
});

new Worker('non-critical-jobs', handleNonCriticalJob, {
  concurrency: 3
});

이 구조라면 영수증 발행, 주문 후속 처리처럼 중요한 작업과 추천 캐시 워밍, 통계 집계 같은 부가 작업을 분리할 수 있습니다. 부가 작업 backlog가 길어져도 핵심 후속 처리가 동일 큐에서 발목 잡히는 상황을 줄일 수 있습니다.

H3. CPU 집약 작업은 worker_threads나 별도 프로세스로 빼야 event loop 보호가 된다

Node.js는 event loop 기반이기 때문에 CPU 작업이 길어지면 전체 응답성에 악영향을 주기 쉽습니다. 이미지 리사이즈, 대용량 JSON 가공, 암호화 연산이 대표적입니다. 이 경우 bulkhead는 단순 limit를 넘어서 실행 공간 분리까지 고려해야 합니다.

  • worker_threads로 CPU 작업 이동
  • 별도 job worker 프로세스 운영
  • API 서버와 배치 서버 역할 분리

즉 CPU heavy workload는 같은 프로세스 안에서 “조심해서 같이 돌리자”보다, 아예 event loop와 떨어뜨리는 것이 더 확실한 bulkhead가 됩니다.

bulkhead pattern을 circuit breaker, timeout과 어떻게 같이 써야 할까

H3. bulkhead는 범위 제한, timeout은 대기 제한, circuit breaker는 실패 확산 차단 역할에 가깝다

이 패턴들은 경쟁 관계가 아니라 서로 보완 관계입니다.

  • Timeout: 오래 기다리지 않게 잘라낸다
  • Circuit breaker: 반복 실패 구간을 잠시 우회하거나 빠르게 실패시킨다
  • Bulkhead: 그 실패가 다른 자원 구획으로 퍼지지 않게 막는다

예를 들어 외부 결제 연동이 느릴 때 아래 조합이 실무적으로 잘 맞습니다.

  1. 짧은 timeout으로 무한 대기 방지
  2. 실패율이 높아지면 circuit breaker open
  3. 결제 호출 전용 concurrency limit로 다른 기능 보호

이 셋을 함께 써야 “느린 의존성 하나 때문에 전체 서비스가 질식하는” 상황을 꽤 잘 줄일 수 있습니다. 관련해서 재시도와 실패 격리를 함께 보려면 Node.js Circuit Breaker 가이드도 같이 보면 흐름이 더 잘 잡힙니다.

H3. 재시도는 bulkhead 밖에서 무한히 늘리면 오히려 격리를 무너뜨릴 수 있다

많이 놓치는 함정도 있습니다. retry를 공격적으로 붙이면 bulkhead가 있어도 효과가 줄어듭니다. 실패한 요청이 다시 몰려들면서 각 구획의 큐와 limit를 계속 두드리기 때문입니다. 그래서 재시도는 아래 원칙과 함께 가는 편이 좋습니다.

  • exponential backoff 적용
  • 전체 retry 횟수 제한
  • 사용자 요청 경로와 background retry 경로 분리
  • idempotency와 중복 방지 장치 마련

중복 실행 제어가 필요한 쓰기 API라면 Node.js Idempotency Key 가이드를 같이 참고하는 편이 좋습니다.

운영하면서 꼭 봐야 할 지표

H3. 평균 latency보다 queue depth, saturation, timeout 비율이 bulkhead 문제를 더 빨리 보여준다

bulkhead를 적용했다면 단순 평균 응답 시간만 보면 부족합니다. 실제로는 “어느 구획이 먼저 포화되는가”를 보여주는 지표가 더 중요합니다.

  • 기능별 queue depth
  • concurrency limit 사용률
  • timeout 발생 비율
  • 외부 API별 p95, p99 latency
  • worker backlog 증가 속도
  • event loop lag
  • DB pool wait time

bulkhead가 잘 설계됐다면 특정 구획 지표는 악화돼도 전체 시스템 핵심 경로 지표는 상대적으로 안정적으로 남아야 합니다. 즉 지표에서도 부분 장애와 전체 장애가 구분되어 보이는가를 확인해야 합니다.

H3. 격리 후에는 어떤 기능을 희생할지 미리 정해야 실제 장애 때 덜 흔들린다

bulkhead는 결국 우선순위의 문제이기도 합니다. 자원이 부족할 때 모든 기능을 똑같이 지키는 것은 보통 불가능합니다. 그래서 미리 정해야 합니다.

  • 추천 영역은 실패해도 주문은 살아야 하는가
  • 관리자 export는 지연돼도 일반 사용자 API는 유지해야 하는가
  • 실시간 알림은 늦어져도 결제 승인 결과는 즉시 돌아가야 하는가

이 우선순위가 명확해야 limit 값, queue 분리, fallback 정책도 일관되게 나옵니다. 메시지 발행과 DB 정합성까지 함께 보는 흐름이라면 Node.js Outbox Pattern 가이드도 연결해서 읽어두면 운영 설계가 더 단단해집니다.

Node.js bulkhead pattern 적용 체크리스트

H3. 아래 질문에 많이 걸리면 지금이 분리 시점이다

배포 전이나 장애 회고 때 아래 체크리스트를 써볼 수 있습니다.

  • 핵심 API와 부가 기능이 같은 큐나 같은 concurrency limit를 쓰는가
  • 외부 API 하나가 느려지면 unrelated endpoint도 같이 느려지는가
  • 배치 작업이 실시간 사용자 요청 자원을 같이 먹는가
  • CPU heavy 작업이 API 프로세스 안에서 직접 실행되는가
  • 장애 시 어떤 기능을 먼저 포기할지 기준이 없는가

두세 개 이상 해당되면 bulkhead pattern을 설계 우선순위에 올릴 만합니다.

자주 묻는 질문

H3. bulkhead pattern과 rate limiting은 같은 개념인가요?

비슷해 보이지만 초점이 다릅니다. rate limiting은 요청 유입량을 제어하는 데 가깝고, bulkhead는 내부 자원 구획을 분리해 장애 전파를 막는 데 더 가깝습니다. 실무에서는 둘을 함께 쓰는 경우가 많습니다.

H3. 작은 서비스에도 bulkhead가 필요한가요?

트래픽이 아주 작고 의존성도 단순하면 과할 수 있습니다. 하지만 외부 API, 배치, CPU 작업이 한 프로세스에 뒤섞이기 시작하면 작은 서비스라도 기본적인 격리선은 빨리 세워두는 편이 낫습니다.

H3. 마이크로서비스로 나누면 bulkhead 문제는 끝나나요?

아닙니다. 서비스를 나눠도 각 서비스 내부에서 queue, worker, pool, tenant isolation을 다시 설계해야 합니다. bulkhead는 배포 단위 분리보다 더 세밀한 자원 경계 설계에 가깝습니다.

마무리

bulkhead pattern은 화려한 패턴이라기보다, “같이 죽지 않게 하자”는 운영 감각에 가깝습니다. Node.js에서는 특히 외부 API 지연, 배치 적체, CPU 작업 때문에 공유 자원이 예상보다 쉽게 잠식됩니다. 그래서 완벽한 분리보다 먼저 핵심 경로와 비핵심 경로의 자원 경계부터 명확히 세우는 것이 중요합니다.

장애를 완전히 없애기는 어렵습니다. 대신 장애가 왔을 때 어디까지만 망가질지 설계할 수는 있습니다. 그 선을 그어주는 패턴이 bulkhead입니다.