Files
2nd/10_Wiki/Topics/Coding/Backpressure_Patterns.md
T
2026-05-09 21:08:02 +09:00

7.0 KiB

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, created_at, updated_at, last_reinforced, review_reason, merge_history, tags, raw_sources, tech_stack, applied_in
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status created_at updated_at last_reinforced review_reason merge_history tags raw_sources tech_stack applied_in
backpressure-patterns 백프레셔 패턴 (Backpressure Patterns) Coding draft backpressure-patterns
backpressure
flow control
rate limiting
throttling
queue overflow
백프레셔
null B 0.85 conceptual 2026-05-09 2026-05-09 2026-05-09
coding
async
streaming
queues
throttling
vibe-coding
P-Reinforce session 2026-05-09 — bulk Coding seed batch 1
language applicable_to
TypeScript / Node.js / Reactive frameworks
Backend
Stream processing
Worker

백프레셔 패턴

Producer 가 Consumer 보다 빠르면 메모리가 무한정 늘어난다. 답은 producer 를 늦추거나(slow), 큐 한계에서 떨어뜨리거나(drop), 사용자에게 거부하기(reject). 절대 "그냥 큐가 알아서 하겠지" 가 아니다.

📖 핵심 개념

비동기 시스템에서 producer 와 consumer 의 처리 속도 비대칭이 발생하면 buffer 가 무한정 자라거나(OOM), 가장 늦은 부분에서 latency 가 폭발한다.

해결책 4종:

  1. Slow producer (pull 모델): consumer 가 "가져갈 준비됐다" 를 신호. producer 는 그때만 push.
  2. Bounded queue + block: 큐 가득 차면 producer 가 enqueue 시 block.
  3. Bounded queue + drop: 가득 차면 새 메시지 drop (또는 가장 오래된 것 drop = "head drop").
  4. Reject at the door: 입구에서 처리량 한도 초과시 client 에 4xx 즉시 반환. 큐에 넣지 않음.

언어/런타임마다 메커니즘이 다름:

  • Node.js streams: pipe() + stream.write() 의 boolean return + 'drain' 이벤트
  • Async iterators: for await 가 자연스러운 pull
  • RxJS / Reactive: throttleTime, sample, audit, buffer, windowTime
  • Goroutines: bounded channel make(chan T, N) 가 자동 block

💻 코드 패턴

1. Bounded queue with reject (TypeScript / 의사코드)

class BoundedQueue<T> {
  private q: T[] = [];
  constructor(private readonly capacity: number) {}

  tryEnqueue(item: T): boolean {
    if (this.q.length >= this.capacity) return false; // reject
    this.q.push(item);
    return true;
  }
  dequeue(): T | undefined { return this.q.shift(); }
}

// 사용
if (!queue.tryEnqueue(req)) {
  res.status(503).json({ error: 'overloaded, retry later' });
  return;
}

2. Node.js stream — drain 이벤트 활용

function writeAll(writable: NodeJS.WritableStream, chunks: Buffer[]) {
  return new Promise<void>((resolve, reject) => {
    let i = 0;
    function next() {
      while (i < chunks.length) {
        const ok = writable.write(chunks[i++]);
        if (!ok) {
          writable.once('drain', next); // backpressure 감지 → drain 까지 대기
          return;
        }
      }
      resolve();
    }
    writable.on('error', reject);
    next();
  });
}

writable.write()false 를 반환하면 buffer 가 watermark 초과. 'drain' 이벤트 후 재개. 무시하면 메모리 폭주.

3. Async iterator — 자연스러운 pull

async function* batchedFetch(urls: string[], concurrency = 10): AsyncGenerator<Response> {
  const queue = [...urls];
  const inFlight: Promise<Response>[] = [];

  while (queue.length || inFlight.length) {
    while (inFlight.length < concurrency && queue.length) {
      inFlight.push(fetch(queue.shift()!));
    }
    const result = await Promise.race(inFlight.map((p, idx) => p.then(r => ({ r, idx }))));
    inFlight.splice(result.idx, 1);
    yield result.r;
  }
}

// consumer 가 처리한 만큼만 fetch — 자동 backpressure
for await (const res of batchedFetch(urls)) {
  await processSlowly(res);
}

4. Token bucket — rate limit 입구 차단

class TokenBucket {
  private tokens: number;
  private lastRefill = Date.now();
  constructor(private readonly capacity: number, private readonly refillPerSec: number) {
    this.tokens = capacity;
  }
  tryTake(): boolean {
    this.refill();
    if (this.tokens < 1) return false;
    this.tokens -= 1;
    return true;
  }
  private refill() {
    const now = Date.now();
    const delta = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(this.capacity, this.tokens + delta * this.refillPerSec);
    this.lastRefill = now;
  }
}

const bucket = new TokenBucket(100, 10); // 초당 10건, 버스트 100
app.use((req, res, next) => {
  if (!bucket.tryTake()) return res.status(429).json({ error: 'rate limit' });
  next();
});

5. Drop 정책 — 최신 우선 (head drop)

function pushOldestDrop<T>(q: T[], item: T, capacity: number): T | undefined {
  let dropped: T | undefined;
  if (q.length >= capacity) dropped = q.shift(); // 가장 오래된 것 버림
  q.push(item);
  return dropped;
}

실시간 메트릭 / 센서 데이터처럼 "오래된 값은 가치 없음" 일 때.

🤔 의사결정 기준

데이터 성격 권장 전략
결제·거래 — 절대 손실 불가 bounded queue + block (또는 외부 큐)
실시간 메트릭 — 최신만 중요 head drop
HTTP 요청 — 사용자가 기다림 reject at the door (429)
스트림 처리 — 가능한 모두 처리 pull 기반 async iterator
분산 시스템 Kafka / SQS / RabbitMQ — 외부 큐가 backpressure 담당

안티패턴

  • 무한 in-memory 큐: const q = []; producer 만 push. 시간 문제로 OOM.
  • Promise.all(huge_array.map(...)): 동시성 제한 없음. 1만 개 요청 동시 발사 → external API 죽거나 우리 메모리 폭발.
  • 드랍하면서 로그 안 남김: 사일런트 데이터 손실. 최소 metric 카운터.
  • block 방식이 web request handler 안에서: 사용자 응답 lateny 폭발. reject 쪽이 적합.
  • rate limit 을 사용자 단위가 아닌 글로벌만: 한 악성 클라이언트가 모든 사용자 차단.
  • stream 의 write() boolean return 무시: 가장 흔한 Node.js 메모리 누수 원인.
  • timer 기반 polling 으로 backpressure 흉내: setInterval(consume, 100) — consumer 가 1초당 10번만 처리. queue 폭발 가능.
  • 외부 API 호출에 타임아웃 없음: 외부가 멈추면 우리 큐 무한정 누적.

🤖 LLM 활용 힌트

  • LLM에게 worker 코드 작성: "bounded queue 사용. capacity 초과 시 reject 또는 head drop. drop 시 metric 카운터 증가" 명시.
  • HTTP API: "입구에 rate limit, 큐 가득 차면 429 즉시 반환" 패턴 요청.
  • 스트림 파이프라인: "async iterator pull 기반으로 작성" 명시 → 자연스러운 backpressure.
  • 외부 API 호출: "concurrency limit + AbortSignal timeout 필수" 강조.

🧪 검증 상태

  • verification_status: conceptual
  • Reactive Streams 명세, Node.js streams docs, Kafka consumer group 메커니즘 등 표준 패턴.
  • 적용 사례 발견 시 applied_in 추가.

🔗 관련 문서