Job Scheduling: BullMQ vs Cloud Scheduler
dha-sleep-api에서 Push 알림 스케줄링에 BullMQ를 선택한 이유와 Cloud Scheduler와의 비교
개요
작업 스케줄링에는 크게 두 가지 접근 방식이 있습니다:
- In-Process Queue (BullMQ): 애플리케이션 내부에서 Redis 기반 큐로 처리
- External Scheduler (Cloud Scheduler): 외부 서비스가 HTTP 호출로 트리거
dha-sleep-api의 Push 스케줄링에는 BullMQ를 선택했습니다.
아키텍처 비교
BullMQ 방식
┌─────────────────────────────────────────────────────────┐
│ Redis │
│ delayed queue: [job1: 08:00, job2: 09:30, ...] │
└─────────────────────────────────────────────────────────┘
▲
│ polling (매 초)
▼
┌─────────────────────────────────────────────────────────┐
│ dha-sleep-api 서버 │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Producer │───▶│ Redis │ │
│ │ (Job 등록) │ │ Queue │ │
│ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Worker │ │
│ │ (Job 처리) │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
특징:
- 서버가 Redis를 polling하여 시간 도달 시 자동 실행
- 같은 프로세스 내에서 함수 호출로 처리
- 외부 네트워크 호출 없음
Cloud Scheduler 방식
┌─────────────────────────────────────────────────────────┐
│ Google Cloud Scheduler │
│ [job1: 08:00 → POST /api/push/send] │
│ [job2: 09:30 → POST /api/push/send] │
└─────────────────────────────────────────────────────────┘
│
│ HTTP POST (시간 도달 시)
▼
┌─────────────────────────────────────────────────────────┐
│ dha-sleep-api 서버 │
│ │
│ ┌──────────────┐ │
│ │ Controller │ ◀── HTTP 요청 수신 │
│ │ /api/push │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Service │ │
│ │ (Push 발송) │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
특징:
- 외부 서비스가 HTTP로 API 호출
- 각 Job마다 별도 스케줄러 엔트리 필요
- 네트워크 통신 필수
상세 비교
1. Job 생성 방식
| 항목 | BullMQ | Cloud Scheduler |
|---|---|---|
| 동적 생성 | ✅ 코드에서 즉시 생성 | ⚠️ API 호출로 생성 (지연 있음) |
| 개별 사용자 스케줄 | ✅ 사용자별 Job 쉽게 생성 | ⚠️ 수천 개 스케줄 관리 어려움 |
| 시간 정밀도 | ms 단위 | 분 단위 (cron) |
예시 - 사용자별 Push 예약:
// BullMQ: 간단함
await queue.add('send-push', { userId, title }, {
delay: 3600000, // 1시간 후
jobId: `push-${pushId}`
});
// Cloud Scheduler: 복잡함
await cloudScheduler.createJob({
name: `push-${pushId}`,
schedule: '0 20 * * *', // cron 표현식
httpTarget: {
uri: 'https://api.example.com/push/send',
body: JSON.stringify({ userId, title })
}
});
2. Job 취소
| 항목 | BullMQ | Cloud Scheduler |
|---|---|---|
| 취소 속도 | ✅ 즉시 (Redis 명령) | ⚠️ API 호출 필요 |
| 조건부 취소 | ✅ 데이터 기반 필터링 | ❌ Job 이름으로만 가능 |
dha-sleep-api 사용 사례:
// 사용자가 대화 시작하면 대기 중인 Push 취소
await pushQueueService.cancelPush(pushId); // 즉시 취소
3. 재시도 및 에러 처리
| 항목 | BullMQ | Cloud Scheduler |
|---|---|---|
| 자동 재시도 | ✅ 설정 가능 (attempts, backoff) | ⚠️ 제한적 |
| 실패 Job 조회 | ✅ 큐에서 직접 조회 | ⚠️ 로그에서 확인 |
| 수동 재실행 | ✅ job.retry() | ❌ 새 스케줄 생성 필요 |
// BullMQ 재시도 설정
{
attempts: 3,
backoff: {
type: 'exponential',
delay: 5000 // 5초 → 10초 → 20초
}
}
4. 모니터링 및 상태 관리
| 항목 | BullMQ | Cloud Scheduler |
|---|---|---|
| 실시간 상태 | ✅ waiting/active/delayed/completed/failed | ⚠️ 실행 기록만 |
| Job 데이터 조회 | ✅ 전체 payload 조회 가능 | ❌ 제한적 |
| 대시보드 | Bull Board, Arena | Cloud Console |
5. 비용
| 항목 | BullMQ | Cloud Scheduler |
|---|---|---|
| 기본 비용 | Redis 비용만 | 월 $0.10/job + 실행당 비용 |
| 대량 Job | 추가 비용 없음 | Job 수에 비례 |
| 예시 (1만 Job/일) | ~$50/월 (Redis) | ~$300/월 이상 |
6. 인프라 의존성
| 항목 | BullMQ | Cloud Scheduler |
|---|---|---|
| 필수 인프라 | Redis | GCP 프로젝트 |
| 서버 다운 시 | Job 유지 (Redis에 저장) | Job 유지 (GCP에 저장) |
| 네트워크 장애 | ✅ 내부 처리로 무관 | ❌ HTTP 호출 실패 가능 |
dha-sleep-api에 BullMQ가 적합한 이유
1. 사용자별 개인화된 스케줄링
Push Planner Agent가 각 사용자의 대화 컨텍스트를 분석하여 개인화된 시간에 Push를 예약합니다.
// 사용자 A: 오후 8시 30분에 취침 리마인더
// 사용자 B: 오후 10시에 취침 리마인더
// 사용자 C: Push 불필요 (Agent 판단)
- Cloud Scheduler: 수천 명의 사용자별로 개별 스케줄 생성/관리 필요 → 비현실적
- BullMQ: 코드 한 줄로 delay 지정하여 즉시 등록 → 간단
2. 조건부 취소 요구사항
"사용자가 먼저 대화를 시작하면 예약된 Push 취소" 기능이 필수입니다.
// push-scheduler.service.ts
async cancelPendingPushes(userId: string): Promise<number> {
// cancelIfUserInitiatesFirst가 true인 Push만 취소
const result = await this.prisma.scheduledPush.updateMany({
where: {
userId,
cancelIfUserInitiatesFirst: true,
status: 'SCHEDULED'
},
data: { status: 'CANCELLED' }
});
// BullMQ Job도 취소
await this.pushQueueService.cancelPush(pushId);
}
- Cloud Scheduler: 조건부 필터링 불가, 모든 Job 이름을 알아야 취소 가능
- BullMQ: 데이터 기반 필터링으로 즉시 취소
3. 밀리초 단위 정밀도
Push Planner Agent가 "지금부터 2시간 30분 15초 후"와 같이 정확한 시간을 지정할 수 있습니다.
- Cloud Scheduler: cron 표현식 (분 단위가 최소)
- BullMQ:
delay: 9015000(ms 단위)
4. 이미 Redis 사용 중
dha-sleep-api는 세션 관리, 캐싱 등에 이미 Redis를 사용하고 있어 추가 인프라 비용이 없습니다.
5. 실시간 상태 추적
Push 발송 상태를 실시간으로 추적하고 실패 시 즉시 재시도가 가능합니다.
// 큐 상태 조회
const status = await pushQueueService.getQueueStatus();
// { waiting: 5, active: 1, delayed: 23, completed: 1000, failed: 2 }
Cloud Scheduler가 적합한 경우
다음 상황에서는 Cloud Scheduler가 더 적합합니다:
| 상황 | 이유 |
|---|---|
| 고정 시간 배치 작업 | 매일 오전 3시 DB 백업 등 |
| 전체 사용자 대상 알림 | 모든 사용자에게 동일 시간에 발송 |
| 서버리스 환경 | Cloud Run 인스턴스가 항상 실행되지 않음 |
| 장기 스케줄 (월/년) | Redis 메모리 부담 없음 |
결론
dha-sleep-api Push 스케줄링 요구사항
| 요구사항 | BullMQ | Cloud Scheduler |
|---|---|---|
| 사용자별 개인화 시간 | ✅ | ⚠️ |
| 조건부 취소 | ✅ | ❌ |
| ms 단위 정밀도 | ✅ | ❌ |
| 대량 Job 비용 효율 | ✅ | ⚠️ |
| 실시간 상태 추적 | ✅ | ⚠️ |
| 재시도 전략 커스터마이징 | ✅ | ⚠️ |
결론: dha-sleep-api의 Push 스케줄링은 동적이고 개인화된 스케줄링이 핵심이므로 BullMQ가 최적의 선택입니다.
참고: 하이브리드 접근
dha-sleep-api에서는 두 방식을 병행 사용합니다:
| 용도 | 기술 | 이유 |
|---|---|---|
| Push 알림 스케줄링 | BullMQ | 동적, 개인화, 조건부 취소 |
| 일일 리포트 생성 | Cloud Scheduler | 고정 시간, 전체 대상 |
| TimeMachine 이벤트 | Cloud Scheduler | 시스템 전체 시간 동기화 |
이렇게 각 기술의 장점을 활용하는 하이브리드 접근이 가장 효과적입니다.