민프
[Nest.js] NestJS Schedule로 스케줄링을 손쉽게 처리하자(@nestjs/schedule) (feat. Redis) 본문
[Nest.js] NestJS Schedule로 스케줄링을 손쉽게 처리하자(@nestjs/schedule) (feat. Redis)
민프야 2025. 5. 14. 21:00NestJS에서 스케줄링이 필요한 이유?
백엔드를 개발하다 보면 “주기적으로 실행되어야 하는 작업”이 필요할 때가 많습니다. 예를 들어
- 매 5분마다 결제되지 않은 주문 자동 취소
- 매일 새벽 3시에 통계 집계
- 매주 월요일 오전 9시에 이메일 발송
- 정기적으로 캐시 정리
이런 스케줄 작업을 Node.js에서 처리하려면 일반적으로 cron, setInterval, node-cron, bull, Agenda.js 등을 사용해 직접 구현해야 했습니다.
하지만 NestJS는 이런 개발자들의 니즈를 해결하기 위해, @nestjs/schedule이라는 공식 모듈을 제공합니다.
NestJS Schedule이란?
NestJS Schedule은 NestJS 기반 프로젝트에서 간편하게 크론(Cron) 작업과 반복 작업을 처리할 수 있게 해주는 공식 모듈입니다.
“표준화된 NestJS 스타일로 스케줄러를 선언형으로 작성할 수 있는 유틸리티.”
- 기반 기술: 내부적으로 node-cron을 사용합니다.
- 장점:
- NestJS의 DI(의존성 주입), 모듈 시스템과 자연스럽게 통합
- 간단한 데코레이터 방식의 사용법
- 정확한 주기 실행 (ms, cron 형식 모두 지원)
- 별도 서비스 클래스에 선언만 하면 작동
설치 방법 및 적용 방법
$ npm install @nestjs/schedule
$ npm install @types/cron
AppModule에 등록하셔야 합니다.
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [ScheduleModule.forRoot()],
})
export class AppModule {}
기본 사용 예시
1. @Cron() – 크론 방식 스케줄러
import { Cron, CronExpression } from '@nestjs/schedule';
@Cron(CronExpression.EVERY_5_MINUTES)
handle() {
console.log('5분마다 실행됨');
}
여기에서 커스텀 cron 표현식도 가능합니다.
ConExpression에 미리 정의되어있는 Enum을 확인해보면 다음과 같습니다.
export declare enum CronExpression {
EVERY_SECOND = "* * * * * *",
EVERY_5_SECONDS = "*/5 * * * * *",
EVERY_10_SECONDS = "*/10 * * * * *",
EVERY_30_SECONDS = "*/30 * * * * *",
EVERY_MINUTE = "*/1 * * * *",
EVERY_5_MINUTES = "0 */5 * * * *",
EVERY_10_MINUTES = "0 */10 * * * *",
EVERY_30_MINUTES = "0 */30 * * * *",
EVERY_HOUR = "0 0-23/1 * * *",
EVERY_2_HOURS = "0 0-23/2 * * *",
EVERY_3_HOURS = "0 0-23/3 * * *",
EVERY_4_HOURS = "0 0-23/4 * * *",
EVERY_5_HOURS = "0 0-23/5 * * *",
EVERY_6_HOURS = "0 0-23/6 * * *",
EVERY_7_HOURS = "0 0-23/7 * * *",
EVERY_8_HOURS = "0 0-23/8 * * *",
EVERY_9_HOURS = "0 0-23/9 * * *",
EVERY_10_HOURS = "0 0-23/10 * * *",
EVERY_11_HOURS = "0 0-23/11 * * *",
EVERY_12_HOURS = "0 0-23/12 * * *",
EVERY_DAY_AT_1AM = "0 01 * * *",
EVERY_DAY_AT_2AM = "0 02 * * *",
EVERY_DAY_AT_3AM = "0 03 * * *",
.
.
.
.
이런식으로 아래와 같이 넣을 수 있습니다.
@Cron('0 0 * * *') // 매일 0시 0분
2. @Interval()– 일정 ms 간격으로 반복
@Interval(10000) // 10초마다
handleInterval() {
console.log('10초마다 반복');
}
3. @Timeout() – 최초 실행 이후 한번만
@Timeout(5000) // 앱 시작 후 5초 뒤
handleTimeout() {
console.log('한번만 실행됨');
}
NestJS Schedule의 동작 방식은?
NestJS Schedule은 내부적으로 Node.js의 이벤트 루프 기반 타이머 시스템(setTimeout, setInterval)과 node-cron 라이브러리를 사용합니다.
구성 요소 | 역할 |
@Cron, @Interval, @Timeout 데코레이터 | 스케줄을 선언적으로 설정 |
node-cron or setInterval() | 지정한 주기에 맞게 콜백 실행 |
NestJS의 DI 컨테이너 | 스케줄러도 하나의 서비스로 주입되어 실행됨 |
Node.js는 싱글 스레드인데, 동시에 어떻게 작동하지?
Node.js는 싱글 스레드입니다. 하지만 Node.js는 내부에 이벤트 루프라는 강력한 비동기 처리 시스템을 가지고 있어서, 동시에 여러 작업을 병렬적으로 진행되는 것처럼 처리할 수 있습니다.
1. NestJS Schedule은 아래와 같이 동작합니다.
- 앱이 시작되면 @nestjs/schedule은 내부적으로 각 데코레이터에 따라 타이머를 등록합니다.
- 예를 들어 @Interval(10000)이라면 setInterval(() => handler(), 10000)과 같은 식으로 실행됨
- 이 작업은 이벤트 루프에 등록되고, 주기가 도래하면 콜백이 큐에 들어가 실행됨
- 이때 API 요청이 들어오면 Nest의 컨트롤러는 별개의 이벤트로 큐에 들어가 처리됨
- 따라서 스케줄러와 API는 서로 독립적으로 동작 가능
예시를 보자면 아래 job이 실행되면서 만약 클라이언트에서 API 요청이 들어오게 되면 어떻게 되는 것 일까요?
@Cron('*/10 * * * * *') // 매 10초마다
handleJob() {
console.log('백그라운드 작업 실행');
}
- 0초 → 스케줄러가 실행됨 → “백그라운드 작업 실행” 로그 출력
- 1초 → 클라이언트에서 API 요청 /users 발생
- 2초 → API 컨트롤러가 응답
- 10초 → 다시 스케줄러 실행됨
이 모든 작업은 이벤트 루프의 큐에 등록되어 하나씩 처리되기 때문에 충돌 없이 작동합니다.
2. 왜 비동기라서 가능한가?
- handleJob()과 @Get()은 서로 다른 함수
- 둘 다 이벤트 루프에 의해 큐잉되어 차례차례 실행됨
- NestJS 자체가 비동기 기반이라 Promise 기반 처리로 중첩/충돌 없음
위에서도 그렇고 여기에서도 '이벤트 루프'라는게 나오는데요
이벤트 루프와 Promise관계를 알아보자면
먼저 Nestjs는 어떻게 동작하냐면
NestJS나 Node.js는 싱글 스레드입니다.
즉, “한 번에 하나의 일만 한다”는 것 입니다.
하지만 마치 동시에 여러 작업을 하는 것처럼 느껴지죠?
그게 가능한 이유가 바로
이벤트 루프(Event Loop) + Promise(비동기 작업 예약) 구조 덕분입니다.
2-1. 이벤트 루프란?
“누가 시키면 일 처리하고, 끝나면 다음 일 하는 일꾼”
쉽게 풀어서 동작하는 과정을 설명하자면 아래와 같습니다.
0. 일 없나? → 일 있음! (DB 조회 요청)
1. 요청 날리고 기다림 (다른 일 함)
2. 일 또 있음! (API 요청 처리)
3. 아까 DB 조회 응답 옴 → 처리함
4. 또 다음 일...
2-2. Promise란?
“야, 이 일 끝나면 나한테 알려줘. 그때 내가 처리할게!”
(= 비동기 예약표)
async function handleJob() {
const result = await fetchFromDatabase(); // 이 부분이 Promise
console.log(result);
}
- fetchFromDatabase()는 DB에 요청만 날리고 바로 빠져나옵니다.
- 그리고 Promise라는 “예약표”를 이벤트 루프에게 맡깁니다.
- DB 응답이 오면, 그때 다시 이어서 실행하는 것 입니다.
2-3. 그럼 스케줄러에서는?
@Cron('*/10 * * * * *')
async handleJob() {
await this.dbService.findUnpaidOrders();
console.log('취소 처리 완료');
}
- 매 10초마다 NestJS가 handleJob()을 호출합니다.
- 내부의 await는 비동기 작업입니다.
- 이벤트 루프는 “오, 이건 기다리는 일이니까 다른 거 먼저 해야지!” 라고 생각합니다.
- API 요청도 자연스럽게 처리합니다.
- DB 응답이 오면 그때 다시 console.log()까지 이어서 실행합니다.
이렇게 이벤트 루프는 Blocking 없이 돌아가는 것 입니다.
즉
NestJS의 @Cron()은 내부에 async/await을 사용하면
→ Promise를 이벤트 루프에 맡기고
→ API나 다른 일과 나눠서 실행할 수 있는 구조예요.
즉, Promise가 없다면 NestJS는 절대 “동시에 여러 일”을 못 합니다.
자 여기서 '비동기가 뭐야?' 라고 하시는 분들을 위해 아주 간단히 설명해드리자면
“비동기란, 싱글 스레드 환경에서 요청만 해두고, 응답 오기 전까지 다른 일 먼저 처리하는 방식이다.”
예시로 풀어보면
- 👤 Node.js = 한 명의 직원 (싱글 스레드)
- 고객이 와서 “주문 좀요!” → 이건 DB 조회 같은 비동기 작업
- 직원은 말해요
“네~ 주방에 전달해둘게요! 일단 다음 손님 받을게요.” → 요청만 던짐 - 주방에서 음식(응답)이 나오면
→ 알림 벨 울림 → 직원이 그걸 받아서 전달 (callback / promise then / await)
여기서 포인트는 아래와 같습니다.
개념 | 설명 |
싱글 스레드 | “일하는 사람은 한 명” |
비동기 | “요청만 하고 결과 기다리진 않음” |
이벤트 루프 | “결과가 오면 알려주는 벨 시스템” |
콜백 / Promise / async/await | “결과 오면 할 일”을 등록하는 방식 |
즉, CPU는 한 명이지만 여러 작업을 동시에 잘 처리하는 것 처럼 보이는 이유는
비동기로 요청만 날려놓고,
응답이 오면 이벤트 루프가 알려줘서 처리하기 때문입니다.
그래서 CPU를 점유하고 있으면 이벤트 루프가 빠져나오지 못해서 처리가 안되게 됩니다.
@Cron('*/10 * * * * *')
handleJob() {
// 10초 동안 CPU 점유
const now = Date.now();
while (Date.now() - now < 10000) {}
}
3. 실전 선택 기준
정리해보자면 아래와 같습니다.
상황 | 추천 기술 | 이유 |
하루 1회 재고 초기화 | Schedule | 간단 주기 작업 |
결제 후 10분 내 미결제 취소 | Bull | 지연 처리 필요 |
서버가 1대고 단순한 작업만 한다면 | Schedule | 간편함 우선 |
로그 추적, 실패 재시도까지 하려면 | Bull | 신뢰성과 확장성 |
NestJS Schedule은 기본적으로 @Cron()이나 @Interval() 데코레이터로 “주기적 작업”을 수행하는 구조입니다.
따라서 한번 작업을 걸어넣고 "10분 동안 응답이 없는 주문”을 찾아 자동 취소할 수 있습니다.
하지만 문제는 정확도와 유연성입니다.
예를 들어)
주문이 생성된 순간부터 "딱 10분 후" 실행은 어렵습니다.
-> 이럴 경우에는 Redis를 사용하셔야합니다.