Celery 대 ARQ Python 애플리케이션을 위한 올바른 작업 큐 선택
Grace Collins
Solutions Engineer · Leapcell

소개
현대 웹 개발, 특히 Python에서는 애플리케이션이 종종 시간이 오래 걸리거나, 리소스 집약적이거나, 네트워크 지연에 취약한 작업을 처리해야 하는 과제에 직면합니다. 예로는 이메일 보내기, 이미지 처리, 보고서 생성 또는 외부 API 호출 등이 있습니다. 이러한 작업을 동기적으로 수행하면 응답 없는 사용자 인터페이스와 낮은 사용자 경험으로 이어질 수 있으며, 단순히 별도의 스레드로 오프로드하는 것만으로는 효과적으로 확장되지 않을 수 있습니다. 바로 여기서 작업 큐가 메인 애플리케이션 흐름에서 이러한 작업을 분리하는 중요한 중개자 역할을 합니다. 이를 통해 백그라운드 처리가 가능해지고, 인식되는 성능이 향상되며, 시스템의 전반적인 복원력이 강화됩니다. 그러나 다양한 강력한 도구를 사용할 수 있으므로 올바른 작업 큐를 선택하는 것은 어려운 작업이 될 수 있습니다. 이 글에서는 두 가지 주요 Python 작업 큐 솔루션인 Celery와 ARQ를 자세히 비교하고, 동기 및 비동기 기능을 중점적으로 다루어 프로젝트에 대한 정보에 입각한 결정을 내릴 수 있도록 돕습니다.
작업 큐 및 비동기 개념 해독
Celery와 ARQ의 구체적인 내용을 살펴보기 전에 몇 가지 기본 개념을 명확히 이해해 봅시다.
작업 큐: 핵심적으로 작업 큐는 나중에 또는 별도의 프로세스에서 작업을 실행하도록 연기할 수 있는 메커니즘입니다. 일반적으로 프로듀서가 큐(Redis 또는 RabbitMQ와 같은 메시지 브로커를 사용하는 경우가 많음)에 작업을 보내고, 컨슈머(워커)가 큐에서 이러한 작업을 가져와 실행하는 것을 포함합니다. 이 분산 아키텍처는 확장성과 내결함성을 제공합니다.
동기 처리: 동기 모델에서는 작업이 순차적으로 실행됩니다. 작업을 시작하면 프로그램은 다음 작업으로 이동하기 전에 완료될 때까지 기다립니다. 이는 간단하지만 메인 실행 스레드를 차단하여 응답 없음으로 이어질 수 있습니다.
비동기 처리: 반대로 비동기 처리를 사용하면 프로그램이 작업을 시작한 다음 해당 작업이 완료될 때까지 기다리지 않고 즉시 다른 작업을 계속할 수 있습니다. 비동기 작업이 완료되면 메인 프로그램에 알릴 수 있습니다. Python에서는 async/await
구문을 사용하여 동시 코드를 작성하기 위한 표준 라이브러리가 asyncio
입니다. 이 논블로킹 I/O 모델은 I/O 바운드 작업에 특히 적합합니다.
작업 상태 및 결과: 강력한 작업 큐 시스템은 작업 상태(예: 보류 중, 시작됨, 성공, 실패)를 추적하고 완료된 후 결과를 검색할 수 있는 방법도 필요합니다. 이는 종종 결과 백엔드를 포함합니다.
Celery: 베테랑과 동기식 근원
Celery는 수년간 많은 Python 애플리케이션의 초석이었던 강력하고 프로덕션 준비가 된 분산 작업 큐입니다. 본질적으로 비동기 작업 실행을 위해 설계되었지만, 내부 메커니즘과 일반적인 사용 패턴은 asyncio
의 이벤트 루프가 아닌, 전통적인 멀티프로세싱 또는 스레딩 모델을 사용하는 작업자 동시성에 더 의존합니다.
Celery 작동 방식:
- 프로듀서(클라이언트): 애플리케이션은
delay()
또는apply_async()
메서드를 사용하여 Celery에 작업을 디스패치합니다. - 메시지 브로커: 작업은 직렬화되어 메시지 브로커(예: RabbitMQ, Redis, Amazon SQS)로 전송됩니다. 이 브로커는 안정적인 중개자 역할을 합니다.
- 워커: Celery 워커는 지속적으로 메시지 브로커에서 새 작업을 폴링합니다. 작업을 받으면 워커는 별도의 프로세스 또는 스레드에서 해당 작업을 실행합니다.
- 결과 백엔드(선택 사항): 작업이 완료된 후 결과(및 상태)는 클라이언트가 나중에 검색할 수 있도록 결과 백엔드(예: Redis, 데이터베이스)에 저장될 수 있습니다.
예제 Celery 구현:
먼저 Celery와 메시지 브로커(예: Redis)를 설치합니다.
pip install celery redis
다음으로 Celery 애플리케이션과 작업을 정의합니다.
# tasks.py from celery import Celery app = Celery('my_app', broker='redis://localhost:6379/0', backend='redis://localhost:6379/1') @app.task def add(x, y): print(f"Executing add task: {x} + {y}") return x + y @app.task(bind=True) def long_running_task(self, duration): import time print(f"Starting long running task for {duration} seconds...") time.sleep(duration) print("Long running task finished.") return f"Slept for {duration} seconds"
Celery 워커를 실행하려면:
celery -A tasks worker --loglevel=info
애플리케이션에서 작업을 디스패치하려면:
# client.py from tasks import add, long_running_task import time print("Dispatching tasks...") result_add = add.delay(4, 5) result_long = long_running_task.delay(10) print(f"Task add dispatched with ID: {result_add.id}") print(f"Task long dispatched with ID: {result_long.id}") # 나중에 상태와 결과를 확인할 수 있습니다. print("\nPolling for results (Celery)...") for _ in range(15): if result_add.ready() and result_long.ready(): break print(f"Add status: {result_add.status}, Long status: {result_long.status}") time.sleep(1) print(f"Result of add task: {result_add.get()}") print(f"Result of long running task: {result_long.get()}") # 동기 호출 예시 (백그라운드 작업에는 이상적이지 않음) # 이것은 작업이 완료될 때까지 클라이언트를 차단합니다. # sync_result = add.apply(args=[10, 20]) # print(f"Synchronous add result: {sync_result.get()}")
Celery는 재시도, 스케줄링(Celery Beat), 속도 제한, 다양한 작업자 동시성 모델(prefork, eventlet, gevent, 스레드)과 같은 기능 제공으로 매우 구성 가능합니다. 강점은 성숙도, 방대한 기능 세트, CPU 바운드 또는 I/O 바운드 작업이든 일반적인 백그라운드 작업 처리를 위한 검증된 안정성입니다(I/O 바운드인 경우 eventlet
또는 gevent
풀은 "prefork"보다 효율적인 동시성을 제공함).
ARQ: asyncio
네이티브 경쟁자
ARQ(Asynchronous Redis Queue의 약자)는 asyncio
를 염두에 두고 특별히 구축된 더 새롭고 가벼운 작업 큐입니다. Redis를 유일한 메시지 브로커 및 상태 백엔드로 활용하므로 최신 asyncio
네이티브 Python 애플리케이션에 강력한 선택이 됩니다. ARQ는 논블로킹 I/O 및 동시 실행이 가장 중요한 비동기 생태계에 원활하게 통합되도록 설계되었습니다.
ARQ 작동 방식:
- 프로듀서(클라이언트):
asyncio
애플리케이션은enqueue_job()
을 사용하여 ARQ에 작업을 디스패치합니다. - Redis: ARQ는 Redis 목록(큐용)과 해시/정렬된 세트(작업 상태 및 결과용)를 유일한 저장소로 사용합니다.
- 워커: ARQ 워커는 Redis에서 새 작업을 폴링하는
asyncio
이벤트 루프입니다. 작업을 받으면 워커는 해당 비동기 함수를 실행합니다. - 결과 저장: 작업 결과 및 상태는 Redis에 직접 저장됩니다.
예제 ARQ 구현:
먼저 ARQ를 설치합니다.
pip install arq redis
다음으로 ARQ 설정 및 작업을 정의합니다.
# worker.py from arq import ArqRedis, create_pool from arq.connections import RedisSettings async def add(ctx, x, y): print(f"Executing async add task: {x} + {y}") await asyncio.sleep(0.1) # 약간의 비동기 작업 시뮬레이션 return x + y async def long_running_async_task(ctx, duration): import asyncio print(f"Starting long running async task for {duration} seconds...") await asyncio.sleep(duration) print("Long running async task finished.") return f"Slept for {duration} seconds" class WorkerSettings: functions = [add, long_running_async_task] redis_settings = RedisSettings() # 기본값: host='localhost', port=6379, db=0 max_jobs = 10 # 최대 10개의 작업을 동시에 처리 (asyncio 작업)
ARQ 워커를 실행하려면 (worker.py
와 같은 디렉토리에서):
arq worker.WorkerSettings
asyncio
애플리케이션에서 작업을 디스패치하려면:
# client.py import asyncio from arq import ArqRedis, create_pool from arq.connections import RedisSettings async def main(): redis = await create_pool(RedisSettings()) print("Dispatching ARQ tasks...") job_add = await redis.enqueue_job('add', 4, 5) job_long = await redis.enqueue_job('long_running_async_task', 10) print(f"Job add dispatched with ID: {job_add.job_id}") print(f"Job long dispatched with ID: {job_long.job_id}") # 나중에 상태와 결과를 확인할 수 있습니다. print("\nPolling for results (ARQ)...") for _ in range(15): status_add = await job_add.status() status_long = await job_long.status() if status_add.is_finished and status_long.is_finished: break print(f"Add status: {status_add}, Long status: {status_long}") await asyncio.sleep(1) result_add = await job_add.result() result_long = await job_long.result() print(f"Result of add job: {result_add}") print(f"Result of long running job: {result_long}") await redis.close() if __name__ == '__main__': asyncio.run(main())
ARQ는 asyncio
가 이미 기반인 환경에서 탁월합니다. 가벼운 특성, 최소한의 종속성, 네이티브 비동기 지원은 I/O 바운드 작업에 매우 효율적입니다. 단일 프로세스 내에서 asyncio
작업을 통해 동시성을 본질적으로 처리하여 I/O 바운드 작업에 대한 리소스 사용량이 매우 효율적입니다. 또한 재시도 로직, 스케줄링 및 지연된 실행도 지원합니다.
작업 큐 선택: 동기 대 비동기 패러다임
Celery와 ARQ의 핵심적인 차이점은 종종 작업의 특성과 애플리케이션의 아키텍처에 따라 달라집니다.
Celery를 선택해야 하는 경우:
- 혼합 워크로드: CPU 바운드 작업(예: 복잡한 계산, 데이터 처리)과 I/O 바운드 작업이 혼합된 백그라운드 작업이 있는 경우, Celery는 다양한 작업자 풀(
prefork
for CPU-bound,eventlet
/gevent
for I/O-bound)로 구성할 수 있어 강력한 선택 사항입니다. - 레거시 또는 비-asyncio 애플리케이션: 기존 애플리케이션이 처음부터
asyncio
로 구축되지 않은 경우, Celery를 통합하는 것이 더 간단할 것입니다. 왜냐하면 주 애플리케이션에asyncio
요구 사항을 강요하지 않기 때문입니다. - 광범위한 기능 및 생태계: Celery는 더 성숙하고 광범위한 생태계를 보유하고 있으며, 고급 라우팅, 전용 스케줄링(Celery Beat), 더 풍부한 모니터링 인터페이스, 더 넓은 범위의 메시지 브로커 지원과 같은 기능을 제공합니다. 이러한 기능이 바로 필요하다면 Celery가 좋은 선택입니다.
- 높은 안정성 요구 사항: Celery는 수많은 프로덕션 환경에서 검증되었으며, 작업 재시도, 오류 처리 및 워커 안정성을 위한 강력한 메커니즘을 제공합니다.
ARQ를 선택해야 하는 경우:
asyncio
네이티브 애플리케이션: 애플리케이션이 이미 전적으로asyncio
네이티브인 경우(예: FastAPI, Sanic 또는 순수asyncio
로 구축된 경우), ARQ는 자연스러운 확장처럼 느껴질 것입니다. 기존 이벤트 루프에 원활하게 통합됩니다.- 주로 I/O 바운드 작업: ARQ의
asyncio
기본 기능은 I/O 작업(네트워크 요청, 데이터베이스 쿼리, 파일 읽기)을 기다리는 작업에 매우 효율적입니다. 최소한의 오버헤드로 많은 동시 I/O 작업을 처리할 수 있습니다. - 경량 및 단순성: ARQ는 종속성 및 코드베이스 측면에서 훨씬 더 가볍습니다. Celery의 광범위한 기능 세트 없이 간단하고 집중적이며 성능이 뛰어난 Redis 기반 작업 큐를 찾고 있다면 ARQ가 훌륭한 옵션입니다.
- I/O에 대한 리소스 효율성:
asyncio
의 협력적 멀티태스킹 덕분에 ARQ 워커는 단일 프로세스 내에서 많은 비동기 작업을 관리할 수 있으며, 이는 종종 I/O 바운드 작업에 대한 Celery의 프로세스 기반 동시성보다 동시 작업당 메모리 및 CPU 사용량이 낮습니다. - Redis 전용 요구 사항: Redis를 유일한 메시지 브로커 및 상태 백엔드로 사용할 의향이 있다면 ARQ는 추가 구성 요소(예: RabbitMQ) 없이 인프라를 단순화합니다.
결론
Celery와 ARQ 중에서 선택하는 것은 프로젝트의 특정 요구 사항, 백그라운드 작업의 특성 및 기존 아키텍처 패러다임에 따라 달라지는 결정입니다. Celery는 동기 유연성과 풍부한 기능 세트로 다양한 워크로드와 기존 애플리케이션에 대한 강력한 선택으로 남아 있습니다. 반면 ARQ는 최신 asyncio
생태계에서 빛을 발하며, 가볍고 asyncio
네이티브 디자인으로 I/O 바운드 작업에 대한 비교할 수 없는 효율성을 제공합니다. 궁극적으로 Celery는 다재다능한 작업마차이고, ARQ는 날렵하고 강력한 asyncio
머신입니다.