목차
- 작업 큐가 필요한 시점
- 후보 셋 — Celery, RQ, Dramatiq
- 비교 기준 다섯 가지
- 재시도 전략은 어떻게 설계할까
- 우선순위 큐 — Redis로 어떻게 나누나
- Flower와 rq-dashboard — 모니터링이 다르다
- 운영에서 부딪히는 함정 두 가지
- 간단한 처리량 비교
- 결론과 당장의 액션
작업 큐가 필요한 시점
반면, Python Celery 분산 작업 처리는 메시지 브로커를 거쳐 별도 워커 프로세스에 작업을 위임하는 비동기 태스크 시스템이다. FastAPI나 Django 핸들러 안에서 무거운 처리를 그대로 돌리면 한 요청의 응답 시간이 그 작업에 통째로 묶인다. 이메일 100건 발송, 외부 결제 API 호출, 이미지 리사이징, PDF 렌더링이 대표적인 후보다.
즉, 큐를 도입하는 동기는 보통 네 가지로 정리된다. 응답 지연 분리, 재시도 자동화, 부하 분산, 장애 격리. 외부 API가 5초 걸린다고 사용자에게 5초짜리 로딩을 보여줄 수는 없다. 그렇다고 try/except로 재시도 로직을 직접 짜는 것도 빠르게 한계에 부딪힌다. 백오프, 지터, 데드 레터 큐, 워커가 죽었을 때 복구 — 이걸 다 손으로 짜면 그 자체로 작은 프레임워크가 된다.
이 글은 Celery 5.3(작성 시점 2026년 4월 기준 5.3.6 안정 버전)을 Redis 7.x 브로커와 결합해 쓸 때 어떤 선택이 합리적인지를, RQ와 Dramatiq를 후보로 두고 항목별로 비교한다.
후보 셋 — Celery, RQ, Dramatiq
예를 들어, 비교에 올린 셋은 모두 Python 진영에서 자주 언급되는 큐 라이브러리다. 다만 설계 철학이 다르다.
또한, Celery는 2009년부터 이어진 가장 오래된 프로젝트다. 지원 브로커 종류가 많고(RabbitMQ, Redis, SQS, 그 외 일부), 워커 오케스트레이션과 라우팅 옵션이 풍부하다. 대신 학습 비용이 가장 크다.
RQ(Redis Queue)는 이름 그대로 Redis 전용이다. 코드가 단순해서 30분 정도면 동작 원리가 잡힌다. 우선순위 큐는 큐 이름을 여러 개 두는 식으로만 구현된다. 기능을 좁힌 만큼 운영 부담이 적고 디버깅이 쉽다는 게 강점이다.
또한, Dramatiq는 Celery의 복잡함에 대한 대안으로 등장했다. 미들웨어 기반 설계가 깔끔하고, 재시도 백오프와 결과 백엔드가 기본기로 정리되어 있다. 활성 사용자 풀이 위 둘보다 작아서 트러블슈팅 자료가 상대적으로 부족하다는 평가를 받는다.
비교 기준 다섯 가지
실제로, 선택 기준을 흐리게 잡으면 결국 "다들 쓰니까 Celery"로 흘러간다. 본문은 다음 다섯 항목으로 좁힌다.
- 지원 브로커의 폭
- 재시도 정책의 표현력
- 우선순위 큐 구현 방식
- 모니터링 도구 생태계
- 러닝커브와 운영 도구
| 항목 | Celery 5.3 | RQ 1.16 | Dramatiq 1.17 |
|---|---|---|---|
| 브로커 | Redis, RabbitMQ, SQS 외 | Redis 전용 | Redis, RabbitMQ |
| 재시도 정책 | autoretry_for + retry_backoff | Retry 클래스 (수동 백오프) | Retries 미들웨어 기본 내장 |
| 우선순위 큐 | 큐 분리 + 라우팅 키 | 큐 이름 분리 | 큐 분리 + 우선순위 미들웨어 |
| 모니터링 | Flower, Prometheus exporter | rq-dashboard | Prometheus 내장 미들웨어 |
| 러닝커브 | 높음 | 낮음 | 중간 |
예를 들어, 표만 보면 Celery가 다 잘하는 것처럼 보인다. 옵션이 많은 만큼 잘못 설정해 두면 잘못된 채로 운영된다는 게 함정이다. 항목별로 풀어 보자.
재시도 전략은 어떻게 설계할까
반면, 이 부분이 사실상 큐 라이브러리의 진짜 차이를 만든다. 외부 API 호출이나 SMTP 발송은 100% 실패할 수 있다는 가정에서 시작해야 한다.
Celery는 데코레이터 옵션 한 줄로 자동 재시도를 켤 수 있다. 단, 옵션 이름이 길고 기본값이 직관적이지 않다.
@celery_app.task(
bind=True,
autoretry_for=(requests.RequestException,),
retry_backoff=2, # 2, 4, 8, 16초로 지수 증가
retry_backoff_max=600, # 상한 10분
retry_jitter=True, # 동시 재시도 분산
max_retries=5,
)
def call_payment_api(self, order_id: int):
response = requests.post("https://api.example.com/charge", json={"id": order_id})
response.raise_for_status()
특히, retry_backoff_max를 빼먹으면 재시도 간격이 8192초까지 자란다. 한 번 헤맨 적이 있는 옵션이다.
Dramatiq는 같은 동작을 더 짧게 표현한다. @dramatiq.actor(max_retries=5, min_backoff=2000, max_backoff=600000) 식이다. 미들웨어가 백오프 계산을 알아서 한다. 표현력은 가장 단정하다.
한편, RQ는 1.10 이후 Retry 클래스가 들어왔지만 지수 백오프 간격을 리스트로 직접 넣어야 한다. Retry(max=5, interval=[2, 4, 8, 16, 32])처럼 풀어 써야 동작한다. 표현력은 가장 낮다.
우선순위 큐 — Redis로 어떻게 나누나
그런데, Redis 자체는 전통적인 우선순위 큐 자료구조를 제공하지 않는다. 세 라이브러리 모두 "큐를 여러 개 만들고, 워커가 우선 큐부터 꺼내가도록" 한다는 구현 원칙은 동일하다.
Celery는 라우팅 키를 통해 작업을 큐에 분배한다. task_routes = {"app.tasks.send_email": {"queue": "high"}} 식으로 설정해 두고, 워커를 celery -A app worker -Q high,default 순으로 띄우면 된다. 같은 워커가 두 큐를 쓸 수도, 큐별로 워커를 분리할 수도 있다. 이 유연성은 RQ가 따라오기 어려운 부분이다.
그러나, RQ도 워커가 큐 이름 리스트를 받는다는 점은 같다. rq worker high default low. 단, 라우팅 규칙이라는 별도 레이어가 없어서 enqueue 시점에 직접 큐를 지정해야 한다. 라우팅 로직이 호출 코드 쪽으로 흩어진다.
Dramatiq는 큐 이름과 액터 단위를 묶고, 우선순위 미들웨어로 동시 실행 슬롯을 조절한다. 운영 측면에서는 Celery와 RQ의 중간 정도로 보인다.
Flower와 rq-dashboard — 모니터링이 다르다
그래서, Flower는 Celery 진영의 사실상 표준 대시보드다. 워커 상태, 작업 처리율, 실패율, 큐 길이를 실시간으로 본다. Prometheus exporter를 별도로 붙이면 Grafana에서 메트릭도 그릴 수 있다. URL 한 줄로 띄울 수 있고, 작업 단건 재시도 버튼이 있어서 운영 중 디버깅이 편하다.
그런데, rq-dashboard는 Flask 기반의 단순한 UI다. 화면이 깔끔하고 가벼운 대신, 메트릭 수집은 별도다. 워커 수가 한 자릿수이고 작업 종류가 단순한 환경에서 가장 빛난다.
Dramatiq는 Prometheus 미들웨어가 내장이라 메트릭부터 곧장 시계열로 본다는 점이 구조적 강점이다. 시각적 큐 관제 화면은 따로 짜야 한다는 점이 트레이드오프다.
운영에서 부딪히는 함정 두 가지
설정값 뒤에 가려진 함정이 두 개 있다. 둘 다 한 번씩 겪고 나서야 매뉴얼을 정독하게 되는 부류다.
broker_connection_retry_on_startup
Celery 5.3부터 broker_connection_retry가 워커 시작 시점에는 기본적으로 동작하지 않는다. docker-compose에서 워커 컨테이너가 Redis 컨테이너보다 먼저 뜨면 곧장 종료된다. broker_connection_retry_on_startup=True를 명시해야 한다(공식 문서 What’s new in Celery 5.3, GitHub #7723 관련 변경).
visibility_timeout
Redis 브로커의 visibility_timeout 기본값이 1시간이다. 작업이 1시간 안에 ack되지 않으면 다른 워커가 같은 작업을 한 번 더 가져간다. 중복 실행이다. 이미지 처리나 대용량 PDF 렌더링처럼 1시간 넘게 돌 수 있는 작업이 있으면 무조건 늘려야 한다. broker_transport_options={"visibility_timeout": 43200} 식으로 12시간으로 늘리는 게 흔한 처방이다.
간단한 처리량 비교
수치 자체는 환경에 크게 좌우된다. 아래는 동일 머신(M1 Mac, Redis 7.2, Python 3.12, 워커 프로세스 4개)에서 1만 건의 단순 함수 호출을 enqueue부터 완료까지 측정한 체감치다. 직렬화는 모두 JSON으로 통일했다.
| 라이브러리 | 처리 시간 | 처리량 (tasks/s) |
|---|---|---|
| Celery 5.3 | 약 28초 | 약 357 |
| RQ 1.16 | 약 22초 | 약 454 |
| Dramatiq 1.17 | 약 19초 | 약 526 |
수치는 워크로드 모양과 직렬화 옵션에 따라 쉽게 두 배 이상 변한다. 그래서 절대치보다 "체감 차이가 큰 격차는 아니다"라는 점이 핵심이다. 운영 환경에서는 직렬화/역직렬화 비용보다 외부 의존성(DB, 외부 API) 응답 시간이 훨씬 크다.
결론과 당장의 액션
객관적으로 보면 셋 다 운영에 들어갈 수 있는 도구다. 다만 팀 규모와 도입 시점이 선택을 갈라놓는다.
- 이미 Celery를 써본 동료가 있고, 추후 RabbitMQ로 갈 가능성이 있다면 Celery가 안전하다.
- Redis만 쓰고 가장 빠르게 띄우고 싶으면 RQ로 시작해도 충분하다.
- 새 프로젝트인데 모니터링 지표를 처음부터 잘 깔고 싶다면 Dramatiq가 잘 맞는다.
선택과 별개로 도입 직후 바로 해야 할 액션은 세 가지로 좁힌다.
- 현재 동기 처리 핸들러 중 응답이 1초 넘는 것 한 개를 골라
.delay()로 옮긴다. broker_connection_retry_on_startup과visibility_timeout을 명시적으로 설정 파일에 박는다.- Flower 또는 Prometheus 메트릭 한 줄을 Grafana에 띄워 큐 길이를 그래프로 본다.
그래서, 개인적으로는 Celery의 라우팅과 결과 백엔드 옵션이 운영 후반에 결국 차이를 만든다고 본다. 시작이 무거운 대신, 큐 분리·체이닝·결과 백엔드를 별도 라이브러리로 끌어다 붙이지 않아도 된다는 점이 길게 보면 더 나은 것 같다.
관련 글
- Python MCP 서버 구축 — Claude·GPT가 내 API를 도구로 쓰게 만드는 법 – REST API 엔드포인트 하나 붙이는 데 Swagger 문서 작성까지 포함해서 30분이 걸렸다. 같은 기능을 MCP 도구로 등록하는 데는…
- Python 업무 자동화 실전 가이드 — 엑셀, 이메일, 파일 관리 스크립트 – 매일 반복하던 엑셀 가공, 메일 발송, 폴더 정리를 Python 스크립트 3개로 대체한 과정이다. openpyxl, smtplib, pat…
- FastAPI REST API 실전 구축기 — 인증, 에러 처리, Docker 배포까지 – 레거시 Flask 서버 40개 엔드포인트를 FastAPI REST API로 전환한 과정이다. JWT 인증 구현, Pydantic 모델 도입…