[TIL] Celery + Redis 태스크 큐 — 도입 배경과 동작 방식

FastAPI BackgroundTasks의 한계를 경험하고 Celery + Redis 태스크 큐로 전환한 배경, Producer-Broker-Worker 구조, BackgroundTasks와의 비교, 운영 시 고려할 설정을 정리한 글이다.

무거운 비동기 작업을 FastAPI BackgroundTasks로 처리하다 보면, 서버 재시작 시 작업이 유실되고 재시도도 어렵다. Celery + Redis로 워커를 분리하면 태스크를 큐에 보관하고, 실패 시 재시도·수평 확장까지 운영 요구에 맞게 대응할 수 있다.


들어가며

API 서버에서 분석·변환처럼 시간이 오래 걸리는 작업을 처리할 때, 처음에는 FastAPI의 BackgroundTasks로 충분해 보인다. 요청을 받고 바로 응답을 돌려준 뒤, 같은 프로세스 안에서 나머지를 비동기로 돌리면 되니까.

하지만 트래픽이 늘고 작업이 무거워질수록 이 방식의 한계가 드러난다. 서버가 재시작되면 진행 중이던 작업이 사라지고, 실패한 작업을 다시 돌릴 방법도 없다. 이번에는 그 문제를 Celery + Redis 태스크 큐로 어떻게 풀었는지 정리한다.


1. BackgroundTasks의 한계

BackgroundTasks는 FastAPI 프로세스 내부에서 태스크를 실행한다.

클라이언트 → API 서버
              └─ BackgroundTasks.add_task(...)
                 └─ 같은 프로세스 안에서 비동기 실행

이 구조에는 세 가지 문제가 있다.

문제결과
서버 재시작 시 진행 중이던 태스크 유실작업이 끝나지 않아도 호출한 쪽은 알 방법이 없음
대기 중이던 태스크도 함께 사라짐트래픽 증가 시 태스크 드롭
실패 시 재시도 불가오류가 나면 한 번에 끝

무거운 AI 파이프라인처럼 실행 시간이 길고 실패 가능성도 있는 작업에는, API 프로세스와 완전히 분리된 태스크 큐가 더 적합하다.


2. Celery란

Celery는 Python 분산 태스크 큐 라이브러리다. 핵심 개념은 세 가지다.

[Producer]      [Broker]      [Consumer]
  API 서버   →   Redis 큐   →   Celery Worker
  (태스크 등록)  (메시지 저장)   (태스크 실행)
  • Producer: 태스크를 큐에 넣는 쪽. 여기서는 FastAPI 라우터.
  • Broker: 태스크 메시지를 보관하는 중간 저장소. 여기서는 Redis.
  • Worker: 브로커에서 태스크를 꺼내 실제로 실행하는 별도 프로세스.

API가 task.delay(...)를 호출하면 태스크 정보가 Redis에 저장된다. Worker가 이를 꺼내 파이프라인을 실행하고, 결과는 웹훅 등 별도 경로로 전달한다.


3. 전환 후 동작 흐름

Celery 도입 후 흐름은 아래와 같다.

클라이언트
    │
    │  분석 요청
    ▼
API 서버
    │
    │  task.delay(...) → Redis에 태스크 등록
    │  즉시 202 Accepted 반환
    ▼
Redis (브로커)
    │  태스크 메시지 보관
    ▼
Celery Worker
    │
    │  태스크 실행 (다운로드 → 분석 → 결과 전달)
    │  실패 시 재시도
    ▼
결과 수신 서버 (DB 저장 등)

API는 태스크 등록만 담당하고, 실제 무거운 작업은 Worker가 처리한다. 호출한 쪽은 빠른 응답을 받고, Worker는 독립적으로 스케일할 수 있다.


4. BackgroundTasks와 비교

 FastAPI BackgroundTasksCelery + Redis
실행 위치API 프로세스 내부별도 Worker 프로세스
서버 재시작 시태스크 유실브로커에 남아 재처리 가능
재시도없음max_retries 설정 가능
수평 확장불가Worker 프로세스 추가로 확장
API 응답 영향무거운 태스크가 쌓이면 영향 있음완전 분리

5. 운영 시 고려할 설정

Celery를 운영에 쓸 때 자주 조정하는 설정이다.

설정권장 방향이유
task_acks_lateTrueWorker가 태스크를 완료한 뒤 ack. 처리 중 크래시 시 재처리 가능
worker_prefetch_multiplier1AI 모델처럼 무거운 작업은 한 번에 하나만 수신
max_retries작업 성격에 맞게일시적 오류(네트워크, 외부 API)에 대비
result_expires짧게 유지결과를 웹훅 등으로 전달한다면 브로커에 오래 보관할 필요 없음

Redis를 브로커로 쓸 때는 비밀번호 인증과 AOF 영속화를 함께 두면, 컨테이너 재시작 후에도 아직 처리되지 않은 태스크가 사라지지 않는다.

태스크 인자는 JSON으로 직렬화되므로, date 같은 객체는 문자열로 변환해 전달하고 Worker 쪽에서 복원하는 패턴이 일반적이다.


마무리

BackgroundTasks는 가벼운 후처리에는 충분하지만, 무거운 비동기 작업에는 한계가 분명하다.

정리하면 아래 세 가지면 충분하다.

  • API는 태스크를 큐에 넣고 빠르게 응답한다.
  • Worker는 별도 프로세스로 분리해 실행·재시도·확장을 담당한다.
  • 브로커(Redis)에 영속화와 ack 정책을 맞춰, 재시작과 실패에도 태스크가 유실되지 않게 한다.

이 구조로 API 응답 지연과 작업 유실 문제를 분리해 해결할 수 있다.