민프

[Nest.js] NestJS Schedule로 스케줄링을 손쉽게 처리하자(@nestjs/schedule) (feat. Redis) 본문

Backend/[Nest.js]

[Nest.js] NestJS Schedule로 스케줄링을 손쉽게 처리하자(@nestjs/schedule) (feat. Redis)

민프야 2025. 5. 14. 21:00

NestJS에서 스케줄링이 필요한 이유?

 

백엔드를 개발하다 보면 “주기적으로 실행되어야 하는 작업”이 필요할 때가 많습니다. 예를 들어

  • 매 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은 아래와 같이 동작합니다. 

  1. 앱이 시작되면 @nestjs/schedule은 내부적으로 각 데코레이터에 따라 타이머를 등록합니다.
  2. 예를 들어 @Interval(10000)이라면 setInterval(() => handler(), 10000)과 같은 식으로 실행됨
  3. 이 작업은 이벤트 루프에 등록되고, 주기가 도래하면 콜백이 큐에 들어가 실행됨
  4. 이때 API 요청이 들어오면 Nest의 컨트롤러는 별개의 이벤트로 큐에 들어가 처리됨
  5. 따라서 스케줄러와 API는 서로 독립적으로 동작 가능

예시를 보자면 아래 job이 실행되면서 만약 클라이언트에서 API 요청이 들어오게 되면 어떻게 되는 것 일까요?

@Cron('*/10 * * * * *') // 매 10초마다
handleJob() {
  console.log('백그라운드 작업 실행');
}

 

  1. 0초 → 스케줄러가 실행됨 → “백그라운드 작업 실행” 로그 출력
  2. 1초 → 클라이언트에서 API 요청 /users 발생
  3. 2초 → API 컨트롤러가 응답
  4. 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('취소 처리 완료');
}
  1. 매 10초마다 NestJS가 handleJob()을 호출합니다.
  2. 내부의 await는 비동기 작업입니다.
  3. 이벤트 루프는 “오, 이건 기다리는 일이니까 다른 거 먼저 해야지!” 라고 생각합니다.
  4. API 요청도 자연스럽게 처리합니다.
  5. DB 응답이 오면 그때 다시 console.log()까지 이어서 실행합니다.

이렇게 이벤트 루프는 Blocking 없이 돌아가는 것 입니다.

 

NestJS의 @Cron()은 내부에 async/await을 사용하면

Promise를 이벤트 루프에 맡기고

API나 다른 일과 나눠서 실행할 수 있는 구조예요.

 

즉, Promise가 없다면 NestJS는 절대 “동시에 여러 일”을 못 합니다.

 

자 여기서 '비동기가 뭐야?' 라고 하시는 분들을 위해 아주 간단히 설명해드리자면

“비동기란, 싱글 스레드 환경에서 요청만 해두고, 응답 오기 전까지 다른 일 먼저 처리하는 방식이다.”

 

 예시로 풀어보면

  1. 👤 Node.js = 한 명의 직원 (싱글 스레드)
  2. 고객이 와서 “주문 좀요!” → 이건 DB 조회 같은 비동기 작업
  3. 직원은 말해요
    “네~ 주방에 전달해둘게요! 일단 다음 손님 받을게요.” → 요청만 던짐
  4. 주방에서 음식(응답)이 나오면
    → 알림 벨 울림 → 직원이 그걸 받아서 전달 (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를 사용하셔야합니다. 

Comments