effidevFlutter · Cloudflare 엣지 · 클라우드 비용 최적화
한국어

Cloudflare Queues로 비동기 작업 처리 파이프라인 구축하여 API 응답 지연 없애기

Cloudflare Queues로 구축하는 서버리스 비동기 작업 파이프라인

유저가 회원가입 버튼을 누르거나 주문을 결제할 때 환영 이메일 발송, 이미지 썸네일 리사이징, 외부 CRM 데이터 동기화 작업이 API 응답 흐름(HTTP Request-Response Loop) 안에서 동기(Synchronous)로 실행되면 API 지연 시간(Latency)은 3초, 5초로 치솟게 된다. 이는 유저 이탈과 타임아웃 에러로 직결된다.

이 문제를 해결하는 정석은 무거운 작업을 백그라운드 **비동기 큐(Asynchronous Queue)**로 밀어 넣고, 유저에게는 즉시 202 Accepted 응답을 돌려주는 것이다. 과거에는 AWS SQS나 RabbitMQ 같은 큐 메시징 시스템과 동기화 관리용 백엔드 워커를 별도로 띄워야 했지만, Cloudflare Queues는 Cloudflare Workers 환경과 100% 통합된 서버리스 메시지 큐를 제공한다.

이 글에서는 Cloudflare Queues의 동기 방식 대비 아키텍처적 이점, Producer-Consumer 파이프라인 구축, 데드 레터 큐(DLQ, Dead Letter Queue) 설정과 지수 백오프(Exponential Backoff) 재시도 정책, 배치 처리(Batching) 최적화, 그리고 실전 이메일/이미지 비동기 처리 풀 코드를 다룬다.

핵심 요약

  • 서버리스 비동기 디커플링: 유저 API 요청 시 무거운 작업(이메일, AI 처리, 웹훅)을 Queues로 env.MY_QUEUE.send() 처리하고 API 응답 시간(TTFB)을 500ms -> 10ms로 98% 감축.
  • Producer-Consumer 아키텍처: 메시지를 발행하는 Producer Worker와 큐를 배치 단위로 소비하는 Consumer Worker가 완전히 분리되어 독립적으로 스케일링.
  • 안전망 - 데드 레터 큐(DLQ): 외부 API 장애로 메시지 처리가 반복 실패할 경우 지수 백오프 재시도 후 DLQ로 메시지를 안전하게 격리하여 데이터 손실(Data Loss) 0% 달성.
  • 배치 최적화(Batching): max_batch_size(최대 100개)와 max_batch_timeout(최대 30초) 설정을 조절하여 Worker 호출 횟수 및 엣지 CPU 청구 비용 최적화.

동기식 API vs Cloudflare Queues 비동기 파이프라인 비교

기존 동기식(Synchronous) 처리 방식

[User Request] ──(HTTP POST)──> [Worker API] ──(500ms)──> [Resend Email API]
                                             ──(1200ms)──> [Image Resizing]
                                             ──(300ms)──> [D1 Log Write]
               <──(HTTP 200, 2000ms Delay)─── [Response Sent to User]

Cloudflare Queues 비동기(Asynchronous) 처리 방식

[User Request] ──(HTTP POST)──> [Producer Worker] ──(env.QUEUE.send())──> [Cloudflare Queues]
               <──(HTTP 202, 12ms Delay)───────── [Instant Response]

                                                        ▼ (Batch Processing)
                                              [Consumer Worker]
                                                        ├──> [Resend Email API]
                                                        ├──> [Image Resizing]
                                                        └──> [D1 Log Write]

비동기 방식에서는 유저가 12ms 만에 응답을 받으며, 무거운 3개 작업은 백그라운드 Consumer Worker에서 안전하게 배치로 처리된다.

Cloudflare Queues 주요 파라미터 및 한계치 (Limits)

항목 한계치 / 추천 설정 설명
메시지 최대 크기 128 KB 단일 메시지 당 최대 JSON/바이너리 패이로드 크기
Max Batch Size 1개 ~ 100개 (기본 10개) Consumer Worker 호출 시 전달되는 최대 메시지 수
Max Batch Timeout 0초 ~ 30초 (기본 5초) 큐에 메시지가 덜 차더라도 지정 시간 후 Consumer 실행
Retry Delay 지수 백오프 지원 retry({ delaySeconds: 10 }) 옵션으로 재시도 조절
Max Delivery Attempts 1회 ~ 100회 실패 시 최대 재시도 횟수 지정 (초과 시 DLQ 전송)

wrangler.toml 설정 (Producer & Consumer & DLQ)

Queues 파이프라인을 구축하려면 wrangler.toml에 Producer 바인딩과 Consumer 구성을 명시한다.

name = "async-job-processor"
main = "src/index.ts"
compatibility_date = "2026-07-01"

# 1. Producer 바인딩 (메시지 발행용)
[[queues.producers]]
  queue = "user-job-queue"
  binding = "JOB_QUEUE"

# 2. Consumer 구성 (메시지 소비 및 DLQ 설정)
[[queues.consumers]]
  queue = "user-job-queue"
  max_batch_size = 25
  max_batch_timeout = 5
  max_retries = 3
  dead_letter_queue = "user-job-dlq" # 3회 실패 시 DLQ로 이동

# 3. DLQ 모니터링용 Consumer
[[queues.consumers]]
  queue = "user-job-dlq"
  max_batch_size = 10

비동기 작업 처리 풀 TypeScript 구현

다음은 HTTP API 요청을 받는 Producer Worker와 큐 메시지를 비동기로 실행하는 Consumer Worker 통합 코드다.

export interface Env {
  JOB_QUEUE: Queue;
  DB: D1Database;
}

interface EmailJobPayload {
  type: "SEND_EMAIL";
  userId: string;
  email: string;
  subject: string;
}

interface ImageJobPayload {
  type: "PROCESS_IMAGE";
  imageId: string;
  imageUrl: string;
}

type JobPayload = EmailJobPayload | ImageJobPayload;

export default {
  // =================================================================
  // 1. Producer: HTTP API 요청 수신 및 즉시 Queue 전송 (HTTP 202)
  // =================================================================
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }

    const body = await request.json<JobPayload>();

    // 큐에 메시지 비동기 발행 (지연 처리 0ms)
    await env.JOB_QUEUE.send(body, {
      contentType: "json",
    });

    // 유저에게 10ms 만에 즉시 Accepted 응답 반환
    return Response.json(
      { success: true, message: "작업이 성공적으로 큐에 등록되었습니다." },
      { status: 202 }
    );
  },

  // =================================================================
  // 2. Consumer: Queue 메시지 배치 단위 수신 및 비동기 처리
  // =================================================================
  async queue(batch: MessageBatch<JobPayload>, env: Env): Promise<void> {
    console.log(`[Queue Consumer] 배치 메시지 수신: 총 ${batch.messages.length}건`);

    for (const message of batch.messages) {
      const job = message.body;

      try {
        if (job.type === "SEND_EMAIL") {
          // 외부 이메일 API (Resend / SendGrid) 호출 연동
          console.log(`[이메일 발송 중] To: ${job.email}, Subject: ${job.subject}`);
          
          // 가상의 외부 API 실패 테스트
          if (job.email.includes("fail")) {
            throw new Error("외부 이메일 발송 서버 응답 없음 (503)");
          }

        } else if (job.type === "PROCESS_IMAGE") {
          console.log(`[이미지 리사이징 중] Image ID: ${job.imageId}`);
        }

        // 성공 시 메시지 승인 (Ack - 큐에서 삭제)
        message.ack();
      } catch (error) {
        console.error(`[작업 실패] Message ID: ${message.id}, 사유:`, error);

        // 3회 미만 실패 시 10초 후 지수 백오프 재시도 (Retry)
        if (message.attempts < 3) {
          message.retry({ delaySeconds: Math.pow(2, message.attempts) * 5 });
        } else {
          // 3회 초과 시 Nack 처리하여 Dead Letter Queue(DLQ)로 격리 이송
          message.nack();
        }
      }
    }
  },
};

데드 레터 큐(DLQ) 모니터링 및 알림 파이프라인

메시지가 DLQ로 넘어왔다는 것은 복구 불가능한 장애(Poison Message)가 발생했다는 의미다. DLQ 전용 Consumer를 두어 Slack/Discord 알림을 발송하거나 D1에 로그를 보관한다.

// wrangler.toml의 user-job-dlq 바인딩 핸들러
async queue(batch: MessageBatch<any>, env: Env): Promise<void> {
  for (const msg of batch.messages) {
    console.error(`[DLQ 감지] 치명적 실패 메시지 ID: ${msg.id}`);
    
    // Slack Webhook으로 장애 알림 전송
    await fetch("https://hooks.slack.com/services/XXX/YYY/ZZZ", {
      method: "POST",
      body: JSON.stringify({
        text: `🚨 [DLQ 장애 발생] Message ID: ${msg.id}\nBody: ${JSON.stringify(msg.body)}`,
      }),
    });

    msg.ack(); // 알림 후 DLQ 정돈
  }
}

결론 및 최적화 요약

Cloudflare Queues를 도입하면 API latency 98% 단축, 서버리스 무제한 스케일링, **DLQ 기반 데이터 손실 0%**라는 최상급 백엔드 비동기 파이프라인을 구축할 수 있다.