백그라운드 작업 오케스트레이터
API 응답을 차단하지 않고 장기 실행 작업을 처리하는 프로덕션 등급의 백그라운드 작업 시스템을 설계하고 구현하는 전문가입니다.
사용 시기
✅ 이럴 때 사용하세요:
- 장기 실행 작업(이메일 발송, 리포트 생성, 이미지 처리)
- 배치 작업(대량 가져오기, 내보내기, 데이터 마이그레이션)
- 스케줄링된 작업(일일 다이제스트, 정리 작업, 정기 보고서)
- 재시도 로직이 필요한 작업(외부 API 호출, 불안정한 작업)
- 우선순위 기반 처리(프리미엄 사용자 우선, 중요 알림)
- 속도 제한 작업(API 할당량, 타사 서비스 제한)
❌ 이럴 때 사용하지 마세요:
- 실시간 양방향 통신(WebSocket 사용)
- 1초 미만의 지연 요구 사항(인메모리 캐싱 사용)
- 단순 지연(setTimeout으로 5초 미만 처리 가능)
- 동기식 API 응답(요청 핸들러에서 로직 유지)
빠른 결정 트리
이 작업이:
├── 5초 이상 걸리나요? → 백그라운드 작업
├── 실패 시 재시도가 필요한가요? → 백그라운드 작업
├── 일정에 따라 실행되나요? → 백그라운드 작업 (cron 패턴)
├── 사용자 상호작용을 차단하나요? → 백그라운드 작업
├── 배치로 처리하나요? → 백그라운드 작업
└── 즉시 반환되나요? → 동기 처리 유지
기술 선택
Node.js: BullMQ (2024년 이후 권장)
사용 시기:
- TypeScript 프로젝트
- Redis가 이미 스택에 있음
- 고급 기능 필요(속도 제한, 우선순위, 반복 작업)
Bull 대신 BullMQ를 선택하는 이유:
- Bull(v3) → BullMQ(v4+): TypeScript로 완전 재작성
- Redis 연결 처리 개선
- 동시성 및 성능 향상
- 능동적인 유지보수(Bull은 유지보수 모드)
Python: Celery
사용 시기:
- Python/Django 프로젝트
- 분산 작업 실행 필요
- 복잡한 워크플로우(체인, 그룹, 코드)
대안:
- RQ (Redis Queue): 더 간단함, 기능 적음
- Dramatiq: 현대적, 생태계가 덜 발달
- Huey: 경량, 소규모 프로젝트에 적합
클라우드 네이티브: AWS SQS, Google Cloud Tasks
사용 시기:
- 서버리스 아키텍처
- Redis/RabbitMQ를 관리하고 싶지 않음
- 보장된 전달 및 데드 레터 큐 필요
일반적인 안티 패턴
안티 패턴 1: 데드 레터 큐 없음
초보자의 생각: "3번 재시도 후 조용히 실패 처리"
문제: 실패한 작업이 사라져 가시성이나 복구 경로가 없음.
올바른 접근법:
// BullMQ with dead letter queue
const queue = new Queue('email-queue', {
connection: redis,
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
},
removeOnComplete: 100, // 마지막 100개 성공 유지
removeOnFail: false // 모든 실패 유지하여 검사 가능
}
});
// 실패한 작업 모니터링
const failedJobs = await queue.getFailed();
타임라인:
- 2020년 이전: 재시도 후 무시
- 2020년+: 데드 레터 큐 표준화
- 2024년+: 작업 실패에 대한 관찰 가능성 필수
안티 패턴 2: 동기식 작업 처리
증상: API 엔드포인트가 작업 완료를 기다림
문제:
// ❌ 잘못됨 - API 응답 차단
app.post('/send-email', async (req, res) => {
await sendEmail(req.body.to, req.body.subject);
res.json({ success: true });
});
왜 잘못되었나: 타임아웃, 나쁜 UX, 서버 리소스 낭비
올바른 접근법:
// ✅ 올바름 - 큐에 추가하고 즉시 응답
app.post('/send-email', async (req, res) => {
const job = await emailQueue.add('send', {
to: req.body.to,
subject: req.body.subject
});
res.json({
success: true,
jobId: job.id,
status: 'queued'
});
});
// 별도 워커가 작업 처리
worker.process('send', async (job) => {
await sendEmail(job.data.to, job.data.subject);
});
안티 패턴 3: 멱등성 없음
문제: 작업이 두 번 실행됨 → 중복 청구, 이중 이메일
발생 원인:
- 처리 중 Redis 연결 끊김
- 작업 완료 전 워커 충돌
- 작업 타임아웃으로 재시도 트리거 (실행 중인 상태에서)
올바른 접근법:
// ✅ 중복 제거 키가 있는 멱등 작업
await queue.add('charge-payment', {
userId: 123,
amount: 50.00
}, {
jobId: `payment-${orderId}`, // 중복 방지
attempts: 3
});
// 워커에서: 이미 처리되었는지 확인
worker.process('charge-payment', async (job) => {
const { userId, amount } = job.data;
// 멱등성 확인
const existing = await db.payments.findOne({
jobId: job.id
});
if (existing) {
return existing; // 이미 처리됨
}
// 결제 처리
const result = await stripe.charges.create({...});
// 멱등성 레코드 저장
await db.payments.create({
jobId: job.id,
result
});
return result;
});
안티 패턴 4: 속도 제한 없음
문제: 타사 API에 과부하를 주거나 할당량을 소진
증상: Sendgrid, Stripe 등에서 "Rate limit exceeded" 오류
올바른 접근법:
// BullMQ 속도 제한
const queue = new Queue('api-calls', {
limiter: {
max: 100, // 최대 100개 작업
duration: 60000 // 60초당
}
});
// 또는: 우선순위 기반 속도 제한
await queue.add('send-email', data, {
priority: user.isPremium ? 1 : 10,
rateLimiter: {
max: user.isPremium ? 1000 : 100,
duration: 3600000 // 시간당
}
});
안티 패턴 5: 워커 확장 고려 안 함
문제: 단일 워커가 큐 깊이를 따라잡지 못함
증상: 큐가 쌓이고, 작업이 몇 시간/일 지연됨
올바른 접근법:
// 여러 워커로 수평 확장
const worker = new Worker('email-queue', async (job) => {
await processEmail(job.data);
}, {
connection: redis,
concurrency: 5 // 워커당 5개 작업 동시 처리
});
// 여러 워커 프로세스 실행 (PM2, Kubernetes 등)
// 각 워커는 concurrency * num_workers 작업 처리
모니터링:
// 큐 깊이 알림 설정
setInterval(async () => {
const waiting = await queue.getWaitingCount();
if (waiting > 1000) {
alert('큐 깊이가 1000을 초과했습니다. 워커를 확장하세요!');
}
}, 60000);
구현 패턴
패턴 1: 이메일 캠페인
// 큐 설정
const emailQueue = new Queue('email-campaign', { connection: redis });
// 배치 추가
async function sendCampaign(userIds: number[], template: string) {
const jobs = userIds.map(userId => ({
name: 'send',
data: { userId, template },
opts: {
attempts: 3,
backoff: { type: 'exponential', delay: 5000 }
}
}));
await emailQueue.addBulk(jobs);
}
// 재시도 로직이 있는 워커
const worker = new Worker('email-campaign', async (job) => {
const { userId, template } = job.data;
const user = await db.users.findById(userId);
const email = renderTemplate(template, user);
try {
await sendgrid.send({
to: user.email,
subject: email.subject,
html: email.body
});
} catch (error) {
if (error.code === 'ECONNREFUSED') {
throw error; // 재시도
}
// 잘못된 이메일, 재시도 안 함
console.error(`사용자 ${userId}의 이메일이 잘못됨`);
}
}, {
connection: redis,
concurrency: 10
});
패턴 2: 스케줄링된 보고서
// 매일 오전 9시 보고서
await queue.add('daily-report', {
type: 'sales',
recipients: ['[email protected]']
}, {
repeat: {
pattern: '0 9 * * *', // Cron 구문
tz: 'America/New_York'
}
});
// 워커가 보고서 생성 후 이메일 발송
worker.process('daily-report', async (job) => {
const { type, recipients } = job.data;
const data = await generateReport(type);
const pdf = await createPDF(data);
await emailQueue.add('send', {
to: recipients,
subject: `일일 ${type} 보고서`,
attachments: [{ filename: 'report.pdf', content: pdf }]
});
});
패턴 3: 비디오 트랜스코딩 파이프라인
// 진행률 추적이 있는 다단계 작업
await videoQueue.add('transcode', {
videoId: 123,
formats: ['720p', '1080p', '4k']
}, {
attempts: 2,
timeout: 3600000 // 1시간 타임아웃
});
worker.process('transcode', async (job) => {
const { videoId, formats } = job.data;
for (let i = 0; i < formats.length; i++) {
const format = formats[i];
// 진행률 업데이트
await job.updateProgress((i / formats.length) * 100);
// 트랜스코딩
await ffmpeg.transcode(videoId, format);
}
await job.updateProgress(100);
});
// 클라이언트가 진행률 폴링
app.get('/videos/:id/status', async (req, res) => {
const job = await queue.getJob(req.params.jobId);
res.json({
state: await job.getState(),
progress: job.progress
});
});
모니터링 및 관찰 가능성
필수 메트릭
// 큐 상태 대시보드
async function getQueueMetrics() {
const [waiting, active, completed, failed, delayed] = await Promise.all([
queue.getWaitingCount(),
queue.getActiveCount(),
queue.getCompletedCount(),
queue.getFailedCount(),
queue.getDelayedCount()
]);
return {
waiting, // 처리 대기 중인 작업
active, // 현재 처리 중인 작업
completed, // 성공적으로 완료됨
failed, // 재시도 후 실패
delayed, // 미래에 실행 예정
health: waiting < 1000 && failed < 100 ? 'healthy' : 'degraded'
};
}
BullMQ Board (UI)
// 개발: 작업을 시각적으로 모니터링
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';
const serverAdapter = new ExpressAdapter();
createBullBoard({
queues: [
new BullMQAdapter(emailQueue),
new BullMQAdapter(videoQueue)
],
serverAdapter
});
app.use('/admin/queues', serverAdapter.getRouter());
// http://localhost:3000/admin/queues 방문
프로덕션 체크리스트
□ 데드 레터 큐 설정됨
□ 지수 백오프가 있는 재시도 전략
□ 작업 타임아웃 제한 설정
□ 타사 API에 대한 속도 제한
□ 중요한 작업을 위한 멱등성 키
□ 워커 동시성 조정 (CPU 코어 * 2)
□ 수평 확장 구성 (여러 워커)
□ 큐 깊이 모니터링 및 알림
□ 실패한 작업 검사 워크플로우
□ 작업 데이터에 로그에 PII 포함 안 됨
□ Redis 지속성 활성화 (AOF 또는 RDB)
□ 정상 종료 처리 (SIGTERM)
사용 vs. 사용하지 말아야 할 시나리오
| 시나리오 | 백그라운드 작업 사용? |
|---|---|
| 가입 시 환영 이메일 보내기 | ✅ 예 - 2-5초 소요 가능 |
| 신용 카드 청구 | ⚠️ 때때로 - 결제 제공자 지연 시간에 따라 다름 |
| PDF 보고서 생성 (30초) | ✅ 예 - 확실히 백그라운드 |
| DB에서 사용자 프로필 가져오기 | ❌ 아니오 - 밀리초, 동기 처리 유지 |
| 비디오 업로드 처리 (5분) | ✅ 예 - 항상 백그라운드 |
| 양식 입력 유효성 검사 | ❌ 아니오 - 동기 유효성 검사 |
| 일일 cron 작업 | ✅ 예 - 반복 작업 사용 |
| 실시간 채팅 메시지 | ❌ 아니오 - WebSocket 사용 |
기술 비교
| 기능 | BullMQ | Celery | AWS SQS |
|---|---|---|---|
| 언어 | Node.js | Python | 모든 언어 (HTTP API) |
| 백엔드 | Redis | Redis/RabbitMQ/SQS | 관리형 |
| 우선순위 | ✅ | ✅ | ✅ |
| 속도 제한 | ✅ | ❌ | ✅ (속성 통해) |
| 반복/Cron | ✅ | ✅ (celery-beat) | ❌ (EventBridge 사용) |
| UI 대시보드 | Bull Board | Flower | CloudWatch |
| 워크플로우 | ❌ | ✅ (체인, 그룹) | ❌ |
| 학습 곡선 | 중간 | 중간 | 낮음 |
| 비용 | Redis 호스팅 | Redis 호스팅 | 백만 건당 $0.40 |
참고 자료
/references/bullmq-patterns.md- 고급 BullMQ 패턴 및 예제/references/celery-workflows.md- Celery 체인, 그룹, 코드/references/job-observability.md- 모니터링, 경고, 디버깅
스크립트
scripts/setup_bullmq.sh- Redis로 BullMQ 초기화scripts/queue_health_check.ts- 큐 메트릭 대시보드scripts/retry_failed_jobs.ts- 실패한 작업 일괄 재시도
이 스킬이 안내하는 내용: 백그라운드 작업 구현 | 큐 아키텍처 | 재시도 전략 | 워커 확장 | 작업 관찰 가능성