본문으로 바로가기 본문 바로가기
WIKI 기술 지식 베이스

오래 실행되는 작업을 분산 작업 큐로 넘기기

원문 보기 위키 갱신

PDF 생성, 느린 외부 API 호출, 머신러닝 모델 실행, 뉴스레터 발송 같은 작업은 시간이 오래 걸리기도 해요. 이렇게 오래 걸리는 작업을 HTTP 핸들러 안에서 처리하면 앱이 타임아웃나고 사용자를 혼란스럽게 만들 수 있어요. 대신 백그라운드 워커로 작업을 넘기는 방법을 알려드릴게요.

출처: 문서

본문

개요

어떤 일들은 시간이 오래 걸려요. PDF 생성. 느린 서드파티 API 호출. 머신러닝 모델 실행. 이메일 뉴스레터 발송, 그리고 메일 서버가 말썽을 부리면 재시도하기.

이유가 무엇이든, 웹 앱이 그 작업이 끝나기를 가만히 기다리고 있다면 잘못하고 있는 거예요. 단지 "비효율적"이라는 의미가 아니라, "앱이 타임아웃 나고 사용자를 혼란스럽게 만들고 디버깅을 끔찍하게 만든다"는 의미로요.

대신 그 작업을 백그라운드 워커로 넘겨야 해요.

이것은 새로운 아이디어가 아니에요. 이 패턴은 오래전부터 있어 왔고, 보통 워크 큐(work queue), 잡 큐(job queue), **태스크 큐(task queue)**라고 불러요. 핵심은 이래요:

  • 웹 앱이 요청을 받아요.
  • 모든 것을 즉시 처리하는 대신, 잡을 큐에 넣어요.
  • 워커라고 불리는 별도의 프로세스가 그 큐에서 잡을 꺼내 백그라운드에서 처리해요.

이렇게 하면 웹 핸들러가 즉시 응답할 수 있어요. 또한 백그라운드 작업은 다른 머신에서 실행되고, 실패 시 재시도되며, 프론트엔드와 독립적으로 확장될 수 있어요.

아마 이미 이런 것을 깨닫지 못한 채 사용해 본 적이 있을 거예요. 태스크 큐로 이메일을 보낸 적 있나요? 그게 워커였어요. 리포트가 생성되는 동안 대시보드에서 스피너를 본 적 있나요? 그 뒤에 잡 큐가 있을 거예요.

이 가이드에서는 이를 설정하는 방법을 보여드릴게요. Python이라면 Celery, Node라면 BullMQ를, 유휴 컨테이너를 실행하기 싫다면 Fly Machines를 사용해요.

큐에 이미 익숙하다면 건너뛰어도 돼요. 하지만 비동기 작업을 HTTP 핸들러에 우겨 넣고 잘 되기를 바랐다면, 이 글이 탈출구예요. 정리해 봐요.


왜 워크 큐를 사용할까?

  • 응답성(Responsiveness): HTTP 응답을 즉시 반환해요. 워커는 시간을 들여 처리하게 두세요.
  • 확장성(Scalability): 웹 서버와 워커를 독립적으로 확장해요. 유휴 비동기 호출에 컴퓨팅을 낭비할 필요가 없어요.
  • 재시도(Retries): 잡은 요청을 막거나 사용자를 귀찮게 하지 않고 깨끗하게 실패하고 재시도될 수 있어요.
  • 비용 효율(Cost efficiency): Fly Machines를 사용하면 잡이 실행되는 동안의 워커 시간에 대해서만 비용을 내요.
  • 지속성(Persistence): 잡은 사용자가 브라우저 탭을 닫아도 신경 쓰지 않아요. 작업은 여전히 완료돼요.

Redis와 Valkey

이 가이드의 대부분 도구는 메시지 브로커로 Redis를 사용해요. 수명이 짧은 메시지와 태스크 큐에 좋은 빠른 인메모리 데이터 저장소죠. Fly.io에 배포한다면, 더 활발한 커뮤니티와 거버넌스 모델을 가진 Redis의 포크인 Valkey를 시도해 보는 것을 권장해요. Enterprise 라이선스 없이도 Valkey 클러스터를 설정할 수 있어요. 같은 방식으로 동작하지만 더 많은 유연성과 장기적 안정성을 제공해요. Redis나 Valkey를 직접 배포하거나 Upstash Redis 같은 관리형 서비스를 사용할 수 있어요.


옵션 1: Celery (Python)

Celery는 Django와 Flask에서 흔히 사용하는 풀 기능의 분산 태스크 큐예요.

동작 방식

  1. 앱이 메시지 브로커(Redis 또는 RabbitMQ)에 태스크를 enqueue해요.
  2. 워커 프로세스가 큐에서 태스크를 꺼내 실행해요.
  3. 태스크 결과는 선택적으로 저장할 수 있어요(예: Redis나 데이터베이스에).

설정

기본 요소를 설치하세요:

pip install "celery[redis]" django-celery-results

celery.py를 만드세요:

from celery import Celery

app = Celery("my_app")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()

태스크 정의에는 @shared_task를 사용하세요:

from celery import shared_task

@shared_task(bind=True, autoretry_for=(Exception,), retry_backoff=3, retry_kwargs={"max_retries": 5})
def crunch_numbers(self, arg):
    ...

워커를 실행하세요:

celery -A my_app worker -l info

Fly.io에 배포하기

fly launch를 사용해 구성을 생성하고 Upstash Redis를 추가하세요.

fly.toml을 업데이트하세요:

[processes]
app = "gunicorn my_app.wsgi"
worker = "celery -A my_app worker -l info"

그런 다음:

fly deploy
fly scale count worker=2
**리전 팁**: Redis 인스턴스와 워커를 같은 리전(예: `ord`, `fra`)에 두어 불필요한 지연과 이상한 레이스 컨디션을 피하세요.

옵션 2: Fly Machines로 온디맨드 워커

항상 상주하는 큐와 워커 풀이 필요한 건 아니에요. 빈도가 낮거나 일회성 잡이라면 태스크당 Fly Machine 하나를 띄울 수 있어요.

동작 방식

  1. 앱이 잡(매개변수, 메타데이터)을 Redis/Valkey에 써요.
  2. Fly Machines API를 호출해 임시 워커를 띄워요.
  3. 워커가 Redis에서 잡 데이터를 읽고 실행한 뒤 결과를 다시 써요.
  4. 앱이 Redis를 폴링해 상태를 확인하고 결과를 가져와요.
  5. 머신을 종료하고 정리해요.

이는 자주 발생하지 않는 잡에 좋고, 인프라를 간결하게 유지해 줘요. 앱 이미지를 재사용하거나 전용 워커 이미지를 만들 수 있어요.

잡 데이터와 결과가 JSON 직렬화 가능한지 확인만 하세요.


옵션 3: BullMQ (Node.js)

BullMQ는 Node.js 앱을 위한 Redis 기반 큐 시스템이에요. Celery와 유사하게 동작해요:

  • 앱이 잡을 Redis/Valkey에 enqueue해요.
  • 워커가 잡을 꺼내 실행해요.
  • 재시도, 속도 제한, 잡 지연, 동시성 제어 같은 기능을 제공해요.

BullMQ는 TypeScript와 잘 어울리고, 문서가 좋으며, NestJS 같은 현대 백엔드 프레임워크와 잘 통합돼요.

Upstash로 Redis를 프로비저닝하거나 fly redis create를 사용하세요.

잡 부하와 머신 크기에 따라 필요에 맞게 워커를 확장하세요:

const worker = new Worker("my-queue", async job => {
  // do your work here
}, { concurrency: 4 });

다른 옵션들

위 도구 중 어느 것도 여러분의 스택이나 사고방식에 맞지 않는다면, 대안이 많아요:

  • Sidekiq (Ruby): Ruby와 Rails 앱의 백그라운드 잡을 위한 사실상의 표준이에요. Redis 기반이고, 검증됐으며, 빠르죠.
  • Oban (Phoenix) Elixir를 위한 견고한 백그라운드 잡 처리 라이브러리예요.
  • RQ (Python): Celery보다 더 간단하고 가벼운 잡 큐로, 더 작은 프로젝트나 더 단순한 요구에 좋아요.

이들 대부분은 Redis/Valkey와 통합되며, 일부는 더 이색적인 브로커(Postgres나 Kafka 같은)를 지원하고, 모두 적절한 설정으로 Fly.io에 배포할 수 있어요.


마지막 참고 사항

무엇을 하든 HTTP 요청 핸들러 안에서 오래 실행되는 작업을 처리하지 마세요. 잡 큐를 사용하세요. 여러분의 스택과 사용 사례에 맞는 도구를 선택하고, 웹 서버가 잘하는 일—빠르게 응답하고 과부하에서도 무너지지 않는 것—을 하도록 두세요.

관련 읽을거리

더 알아보기 (Learn more)