웹 애플리케이션의 비동기 작업 관리
Min-jun Kim
Dev Intern · Leapcell

소개
현대 웹 애플리케이션 개발에서는 응답성과 확장성이 무엇보다 중요합니다. 모든 요청을 동기식으로 처리하는 것은 특히 이미지 처리, 대량 이메일 전송, 보고서 생성 또는 복잡한 데이터 계산과 같이 오래 걸리는 작업을 다룰 때 사용자 경험 저하로 이어지기 쉽습니다. 이때 비동기 작업 큐가 필수적이 됩니다. 이러한 시간이 많이 소요되는 작업을 별도의 프로세스로 오프로드하면 메인 웹 애플리케이션 스레드는 들어오는 사용자 요청을 신속하게 처리할 수 있어 인식된 성능과 전반적인 시스템 효율성을 크게 향상시킵니다. 이 글에서는 Python용 Celery, Node.js용 BullMQ, .NET용 Hangfire라는 세 가지 주요 비동기 작업 큐 라이브러리를 살펴보고 각 웹 프레임워크와의 효과적인 통합 전략을 논의합니다. 이러한 도구를 활용하는 방법을 이해하는 것은 고성능의 내결함성 웹 서비스를 구축하는 데 중요합니다.
시작하기 전 핵심 개념
각 라이브러리의 구체적인 내용으로 들어가기 전에 비동기 작업 처리를 뒷받침하는 핵심 개념에 대한 공통된 이해를 확립해 보겠습니다.
- 작업 큐(Task Queue): 비동기 실행을 위해 작업을 큐에 추가할 수 있는 시스템입니다. 웹 애플리케이션(생산자)과 백그라운드 작업자(소비자) 간의 중개자 역할을 합니다.
- 생산자(Producer): 작업을 생성하고 작업 큐로 디스패치하는 구성 요소(일반적으로 웹 애플리케이션)입니다.
- 소비자/작업자(Consumer/Worker): 작업 큐를 모니터링하고 작업을 검색하여 백그라운드에서 실행하는 별도의 프로세스 또는 애플리케이션입니다.
- 브로커(Broker): 생산자와 소비자 간의 통신을 용이하게 하는 메시지 큐 시스템(예: Redis, RabbitMQ)입니다. 작업자가 처리할 준비가 될 때까지 작업을 저장합니다.
- 결과 백엔드(Result Backend): 실행된 작업의 결과 또는 상태를 저장하여 생산자가 완료 상태를 조회할 수 있도록 하는 선택적 구성 요소입니다.
- 멱등성(Idempotency): 여러 번 실행해도 한 번 실행한 것과 동일한 결과를 생성하는 연산의 속성입니다. 이는 오류로 인해 재시도될 수 있는 작업에 중요합니다.
- 동시성(Concurrency): 여러 작업 또는 요청을 동시에 처리하는 능력입니다. 작업 큐는 병렬로 많은 작업을 처리하기 위해 동시성을 사용합니다.
Celery (Python): Django 및 Flask를 위한 견고한 백그라운드 처리
Celery는 Python 애플리케이션을 위한 강력하고 분산된 작업 큐입니다. 매우 유연하며 소규모 프로젝트부터 대규모 시스템까지 모든 용도로 널리 사용됩니다.
원칙 및 구현
Celery는 Redis 또는 RabbitMQ와 같은 메시지 브로커를 사용하여 작업 배포를 용이하게 하는 생산자-소비자 모델로 작동합니다. 작업이 호출되면 브로커로 전송되고, 브로커는 이를 사용 가능한 Celery 작업자에게 전달합니다.
웹 프레임워크(Django/Flask)와의 통합
Python 웹 프레임워크와 Celery를 통합하는 것은 일반적으로 프로젝트 내에서 Celery를 구성한 다음 뷰 또는 비즈니스 로직에서 작업을 디스패치하는 것을 포함합니다.
예시: Django 통합
-
설치:
pip install celery redis
-
celery.py
(예: Django 프로젝트의 메인 앱 디렉토리에):import os from celery import Celery # 'celery' 프로그램에 대한 기본 Django 설정 모듈을 설정합니다. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project_name.settings') app = Celery('your_project_name') # 여기에 문자열을 사용하면 Windows를 사용할 때 작업자가 객체를 pickle할 필요가 없습니다. app.config_from_object('django.conf:settings', namespace='CELERY') # 등록된 모든 Django 앱 구성에서 작업 모듈을 로드합니다. app.autodiscover_tasks() @app.task(bind=True) def debug_task(self): print(f'Request: {self.request!r}')
-
settings.py
(Django 프로젝트 설정):# ... (기타 Django 설정) ... CELERY_BROKER_URL = 'redis://localhost:6379/0' CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = 'UTC' # 또는 현지 시간대
-
**
tasks.py
(Django 앱 내, 예:myapp/tasks.py
):from celery import shared_task import time @shared_task def send_confirmation_email(user_email, order_id): print(f"Sending email for order {order_id} to {user_email}...") time.sleep(5) # 오래 걸리는 이메일 전송 작업 시뮬레이션 print(f"Email sent for order {order_id}.") return {"status": "success", "order_id": order_id} @shared_task def generate_report(user_id): print(f"Generating report for user {user_id}...") time.sleep(10) report_data = {"user_id": user_id, "data": "complex report content"} print(f"Report generated for user {user_id}.") return report_data
-
Django 뷰에서 디스패치 (
myapp/views.py
):from django.http import HttpResponse from .tasks import send_confirmation_email, generate_report def create_order_view(request): if request.method == 'POST': user_email = request.POST.get('email') order_id = "12345" # 예시 주문 ID # 사용자에게 즉시 응답 반환 # 이메일은 백그라운드에서 전송됩니다. send_confirmation_email.delay(user_email, order_id) # 나중에 결과를 얻으려면: # result = send_confirmation_email.apply_async(args=[user_email, order_id]) # task_id = result.id # 나중에 상태를 확인하기 위해 저장 return HttpResponse(f"Order created. Confirmation email will be sent to {user_email}.", status=202) return HttpResponse("Please POST to create an order.") def request_report_view(request, user_id): # 보고서를 비동기적으로 생성 result = generate_report.delay(user_id) return HttpResponse(f"Report generation started for user {user_id}. Task ID: {result.id}", status=202) def check_report_status_view(request, task_id): from celery.result import AsyncResult res = AsyncResult(task_id) if res.ready(): return HttpResponse(f"Report status: Completed. Result: {res.get()}", status=200) else: return HttpResponse(f"Report status: Pending/Running. State: {res.state}", status=202)
애플리케이션 시나리오
- 이메일 전송: 거래 및 마케팅 이메일 오프로드.
- 이미지/비디오 처리: 미디어 파일 크기 조정, 워터마킹, 인코딩.
- 데이터 가져오기/내보내기: 대규모 CSV/Excel 파일 작업 처리.
- 보고서 생성: 복잡한 PDF 또는 데이터 요약 생성.
- API 통합: 느릴 수 있는 외부 서비스에 대한 호출.
- 예약된 작업 : 주기적인 작업 실행을 위해 Celery Beat 사용.
BullMQ (Node.js): Express 및 NestJS를 위한 고성능 큐
BullMQ는 Redis를 기반으로 구축된 빠르고 견고한 큐 시스템으로, Node.js용으로 특별히 설계되었습니다. 성능, 안정성 및 사용 편의성에 중점을 둡니다.
원칙 및 구현
BullMQ는 Redis 스트림과 원자적 연산을 활용하여 매우 효율적이고 내구성 있는 메시지 큐를 제공합니다. 작업 우선순위, 지연된 작업, 반복 작업 및 지수 백오프를 포함한 작업 재시도와 같은 기능을 지원합니다.
웹 프레임워크(Express/NestJS)와의 통합
BullMQ를 통합하는 것은 일반적으로 큐 인스턴스를 생성하고, 작업에 대한 프로세서를 정의하고, 웹 애플리케이션 라우트 또는 서비스에서 작업을 추가하는 것을 포함합니다.
예시: Express.js 통합
-
설치:
npm install bullmq ioredis
-
queue.js
(큐 정의 및 프로세서를 위한 별도 파일):const { Queue, Worker } = require('bullmq'); const IORedis = require('ioredis'); const connection = new IORedis({ maxRetriesPerRequest: null, enableReadyCheck: false }); const emailQueue = new Queue('emailQueue', { connection }); const reportQueue = new Queue('reportQueue', { connection }); // 이메일 작업을 위한 작업자 const emailWorker = new Worker('emailQueue', async job => { console.log(`Processing email job ${job.id}: Sending email to ${job.data.userEmail} for order ${job.data.orderId}...`); await new Promise(resolve => setTimeout(resolve, 5000)); // 지연 시뮬레이션 console.log(`Email job ${job.id} completed.`); return { status: 'sent', orderId: job.data.orderId }; }, { connection }); emailWorker.on('completed', job => { console.log(`Job ${job.id} has completed! Result:`, job.returnvalue); }); emailWorker.on('failed', (job, err) => { console.log(`Job ${job.id} has failed with error ${err.message}`); }); // 보고서 작업을 위한 작업자 const reportWorker = new Worker('reportQueue', async job => { console.log(`Processing report job ${job.id}: Generating report for user ${job.data.userId}...`); await new Promise(resolve => setTimeout(resolve, 10000)); // 지연 시뮬레이션 const reportData = { userId: job.data.userId, data: "complex report content"}; console.log(`Report job ${job.id} completed.`); return reportData; }, { connection }); module.exports = { emailQueue, reportQueue, connection };
-
app.js
(Express 애플리케이션):const express = require('express'); const { emailQueue, reportQueue } = require('./queue'); // 큐 가져오기 const app = express(); app.use(express.json()); app.post('/create-order', async (req, res) => { const { userEmail, orderId } = req.body; if (!userEmail || !orderId) { return res.status(400).send('User email and order ID are required.'); } // 이메일 전송 작업을 큐에 추가 const job = await emailQueue.add('sendConfirmationEmail', { userEmail, orderId }, { removeOnComplete: true, // 완료된 작업 정리 removeOnFail: false, // 검사를 위해 실패한 작업 유지 attempts: 3 // 최대 3회 재시도 }); res.status(202).json({ message: `Order created. Confirmation email will be sent. Job ID: ${job.id}`, jobId: job.id }); }); app.get('/generate-user-report/:userId', async (req, res) => { const userId = req.params.userId; const job = await reportQueue.add('generateUserReport', { userId }, { removeOnComplete: true, removeOnFail: false, attempts: 1 // 이 예시에서는 보고서 생성 시 재시도 없음 }); res.status(202).json({ message: `Report generation started for user ${userId}. Job ID: ${job.id}`, jobId: job.id }); }); app.get('/job-status/:queueName/:jobId', async (req, res) => { const { queueName, jobId } = req.params; let queue; if (queueName === 'emailQueue') { queue = emailQueue; } else if (queueName === 'reportQueue') { queue = reportQueue; } else { return res.status(400).send('Invalid queue name.'); } const job = await queue.getJob(jobId); if (!job) { return res.status(404).send('Job not found.'); } const state = await job.getState(); const result = await job.returnvalue; // 완료된 경우 반환 값 가져오기 res.json({ jobId: job.id, name: job.name, state: state, data: job.data, result: result, failedReason: job.failedReason, attemptsMade: job.attemptsMade }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
이것을 실행하려면 Express 앱을 시작하고 별도의 Node.js 프로세스에서 queue.js
파일을 실행해야 합니다(또는 원하는 경우 단일 프로세스 아키텍처에 통합할 수 있지만 프로덕션의 경우 별도의 작업자 프로세스가 권장됩니다).
애플리케이션 시나리오
- 실시간 알림: 백그라운드 프로세스가 완료된 후 웹 소켓을 통해 사용자에게 알림 푸시.
- 데이터 동기화: 타사 API와의 데이터 동기화.
- 백그라운드 API 호출: 즉각적인 응답이 필요하지 않은 중요하지 않은 API 호출.
- 배치 처리: 대규모 데이터 세트를 청크 단위로 처리.
- 예약된 작업: 일별, 주별 또는 시간별 작업을 위한 반복 작업 기능 사용.
Hangfire (.NET): ASP.NET Core를 위한 인프로세스/아웃프로세스 백그라운드 작업
Hangfire는 ASP.NET Core 애플리케이션, 콘솔 애플리케이션 또는 Windows 서비스 내에서 즉시, 지연 및 반복 작업을 수행할 수 있도록 하는 매우 다재다능한 .NET 라이브러리입니다. SQL Server, Redis 및 PostgreSQL과 같은 다양한 저장소 메커니즘을 지원합니다.
원칙 및 구현
Hangfire는 작업 정의를 저장소 백엔드(예: 데이터베이스)에 영구 저장하고 Hangfire 서버(작업자)가 이 저장소를 폴링하여 작업을 선택하고 처리하는 방식으로 작동합니다. 웹 애플리케이션과 동일한 프로세스에서 실행되거나 별도의 전용 작업자 프로세스에서 실행될 수 있습니다.
웹 프레임워크(ASP.NET Core)와의 통합
Hangfire를 통합하는 것은 Startup.cs
(또는 .NET 6+의 Program.cs
)에서 구성하고 컨트롤러 또는 서비스에서 작업을 enqueuing하는 것을 포함합니다.
예시: ASP.NET Core 통합
-
설치:
dotnet add package Hangfire.AspNetCore dotnet add package Hangfire.SqlServer # 또는 Hangfire.Redis 등
-
**
Startup.cs
(또는 .NET 6+의Program.cs
):.NET 5 이하 (Startup.cs)의 경우:
using Hangfire; using Hangfire.SqlServer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Threading.Tasks; public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // Hangfire 서비스 추가. services.AddHangfire(configuration => configuration .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() .UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"), new SqlServerStorageOptions { CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), QueuePollInterval = TimeSpan.FromSeconds(15), // 새 작업을 얼마나 자주 확인할지 UseRecommendedIsolationLevel = true, DisableGlobalLocks = true })); // 처리 서버를 IHostedService로 추가 services.AddHangfireServer(); // 작업에서 필요할 수 있는 사용자 지정 서비스 추가 services.AddTransient<IEmailService, EmailService>(); services.AddTransient<IReportService, ReportService>(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IBackgroundJobClient backgroundJobClient, IRecurringJobManager recurringJobManager) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); app.UseAuthorization(); // Hangfire 대시보드 활성화 app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = new[] { new HangfireDashboardNoAuthFilter() } // 프로덕션용 실제 인증 대신 데모용 }); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapHangfireDashboard(); // 대시보드를 "/hangfire"로 매핑 }); // 주기적 작업 예시 추가 recurringJobManager.AddOrUpdate( "DailyCleanupJob", () => Console.WriteLine("Performing daily cleanup..."), Cron.Daily); // 시작 시 또는 테스트를 위해 작업을 enqueuing할 수도 있습니다. backgroundJobClient.Enqueue(() => Console.WriteLine("Hello Hangfire from startup!")); } } // Hangfire 대시보드의 인증을 우회하는 간단한 필터 (프로덕션에서는 주의해서 사용) public class HangfireDashboardNoAuthFilter : IDashboardAuthorizationFilter { public bool Authorize(DashboardContext context) { return true; // 데모 목적으로 모든 액세스 허용. 프로덕션에서는 적절한 인증을 구현하십시오. } } // Hangfire 작업이 호출할 예시 서비스 public interface IEmailService { Task SendOrderConfirmation(string userEmail, string orderId); } public class EmailService : IEmailService { public async Task SendOrderConfirmation(string userEmail, string orderId) { Console.WriteLine($"Sending email for order {orderId} to {userEmail}..."); await Task.Delay(5000); // 네트워크 지연 시뮬레이션 Console.WriteLine($"Email sent for order {orderId}."); } } public interface IReportService { Task<string> GenerateUserReport(int userId); } public class ReportService : IReportService { public async Task<string> GenerateUserReport(int userId) { Console.WriteLine($"Generating report for user {userId}..."); await Task.Delay(10000); var reportContent = $"Report for User {userId}: Generated successfully."; Console.WriteLine($"Report generated for user {userId}."); return reportContent; } }
.NET 6+ (Program.cs)의 경우:
using Hangfire; using Hangfire.SqlServer; using Hangfire.Dashboard; // IDashboardAuthorizationFilter에 필요 using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Configuration; // 포함되지 않은 경우 추가 using System; using System.Threading.Tasks; var builder = WebApplication.CreateBuilder(args); // 컨테이너에 서비스 추가. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Hangfire 서비스 추가. builder.Services.AddHangfire(configuration => configuration .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() .UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection"), new SqlServerStorageOptions { CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), QueuePollInterval = TimeSpan.FromSeconds(15), UseRecommendedIsolationLevel = true, DisableGlobalLocks = true })); // 처리 서버 추가 builder.Services.AddHangfireServer(); // 사용자 지정 서비스 추가 builder.Services.AddTransient<IEmailService, EmailService>(); builder.Services.AddTransient<IReportService, ReportService>(); var app = builder.Build(); // HTTP 요청 파이프라인 구성. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); // Hangfire 대시보드 활성화 app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = new[] { new HangfireDashboardNoAuthFilter() } // 비프로덕션 데모용 }); app.MapControllers(); app.MapHangfireDashboard(); // 대시보드 매핑 // 시작 시 주기적 작업 예시 등록 app.Services.GetService<IRecurringJobManager>()?.AddOrUpdate( "DailyCleanupJob", () => Console.WriteLine("Performing daily cleanup with Hangfire..."), Cron.Daily); app.Run(); // --- 서비스 및 Authorization Filter 정의 (위와 동일) --- public class HangfireDashboardNoAuthFilter : IDashboardAuthorizationFilter { public bool Authorize(DashboardContext context) => true; // 위험! 데모 전용. } public interface IEmailService { Task SendOrderConfirmation(string userEmail, string orderId); } public class EmailService : IEmailService { public async Task SendOrderConfirmation(string userEmail, string orderId) { Console.WriteLine($"Sending email for order {orderId} to {userEmail}..."); await Task.Delay(5000); Console.WriteLine($"Email sent for order {orderId}."); } } public interface IReportService { Task<string> GenerateUserReport(int userId); } public class ReportService : IReportService { public async Task<string> GenerateUserReport(int userId) { Console.WriteLine($"Generating report for user {userId}..."); await Task.Delay(10000); var reportContent = $"Report for User {userId}: Generated successfully."; Console.WriteLine($"Report generated for user {userId}."); return reportContent; } }
-
appsettings.json
:{ "ConnectionStrings": { "HangfireConnection": "Server=(localdb)\mssqllocaldb;Database=HangfireDB;Trusted_Connection=True;MultipleActiveResultSets=true" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }
HangfireDB
를 생성하거나 기존 데이터베이스에 대한 연결 문자열을 업데이트하십시오. Hangfire는 필요한 테이블을 생성합니다. -
컨트롤러 예시 (
Controllers/OrderController.cs
):using Hangfire; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; [ApiController] [Route("[controller]")] public class OrderController : ControllerBase { private readonly IBackgroundJobClient _backgroundJobClient; private readonly IBackgroundJobClientFactory _jobClientFactory; private readonly IEmailService _emailService; // 필요한 경우 직접 사용하기 위해 주입됨 public OrderController(IBackgroundJobClient backgroundJobClient, IBackgroundJobClientFactory jobClientFactory, IEmailService emailService) { _backgroundJobClient = backgroundJobClient; _jobClientFactory = jobClientFactory; _emailService = emailService; } [HttpPost("create")] public IActionResult CreateOrder([FromBody] OrderRequest request) { // 이메일 전송을 위한 즉시 실행 작업 enqueuing var emailJobId = _backgroundJobClient.Enqueue<IEmailService>(x => x.SendOrderConfirmation(request.UserEmail, request.OrderId)); return Accepted(new { Message = $"Order created. Confirmation email will be sent. Email Job ID: {emailJobId}", EmailJobId = emailJobId }); } [HttpGet("generate-report/{userId}")] public IActionResult GenerateReport(int userId) { // 보고서 생성을 위한 지연 작업 enqueuing (예: 1분 후 실행) var reportJobId = _backgroundJobClient.Schedule<IReportService>(x => x.GenerateUserReport(userId), TimeSpan.FromMinutes(1)); // 1분 후 실행 // 또는 즉시 즉시 실행 작업: // var reportJobId = _backgroundJobClient.Enqueue<IReportService>(x => x.GenerateUserReport(userId)); return Accepted(new { Message = $"Report generation scheduled for user {userId}. Report Job ID: {reportJobId}", ReportJobId = reportJobId }); } [HttpGet("job-status/{jobId}")] public IActionResult GetJobStatus(string jobId) { var jobState = JobStorage.Current.GetConnection().GetStateData(jobId); return Ok(new { JobId = jobId, State = jobState?.Name, Reason = jobState?.Reason, CreatedAt = jobState?.CreatedAt }); } } public class OrderRequest { public string OrderId { get; set; } public string UserEmail { get; set; } }
애플리케이션 시나리오
- 감사 로깅: 사용자 흐름에 영향을 주지 않고 백그라운드에서 작업 기록.
- 데이터베이스 유지 관리: 데이터 정리 또는 동기화 스크립트 실행.
- 시스템 통합: 외부 시스템으로 데이터 전송.
- 모든 오래 걸리는 프로세스: Celery 및 BullMQ와 마찬가지로 수백 밀리초 이상 걸리는 모든 것은 백그라운드 작업으로 고려해야 합니다.
- 내장 대시보드: 작업을 모니터링하고 관리할 수 있는 편리한 웹 UI 제공.
결론
Celery, BullMQ 및 Hangfire는 각 생태계 내에서 비동기 작업 처리를 위한 강력한 솔루션을 제공합니다. 성숙한 Python 생태계를 갖춘 Celery는 Django 및 Flask에 대한 다재다능한 선택입니다. Node.js용으로 구축되고 Redis에 의해 구동되는 BullMQ는 고성능, 실시간 시나리오에서 탁월합니다. Hangfire는 뛰어난 영구성과 내장된 대시보드를 통해 .NET 애플리케이션을 위한 엔터프라이즈급 통합 솔루션을 제공합니다.
이들 중에서 선택하는 것은 기술 스택과 특정 프로젝트 요구 사항에 따라 크게 달라집니다. 이 세 가지 모두 개발자가 백그라운드 작업을 효율적으로 관리하여 응답성이 뛰어나고 확장 가능하며 고가용성 웹 애플리케이션을 구축할 수 있도록 합니다. 이러한 비동기 작업 관리 시스템을 전략적으로 통합함으로써 개발자는 웹 애플리케이션의 사용자 경험과 리소스 활용도를 크게 향상시킬 수 있습니다.