작업 예약을 임의의 코드(메서드/함수)를 특정한 날짜/시간에 실행하거나 반복 간격으로 실행하거나, 지정된 간격 후에 한 번 실행하는 기능을 제공합니다. Linux 환경에서는 이를 주로 cron과 같은 패키지가 OS 수준에서 처리합니다. Node.js 애플리케이션에서는 cron과 유사한 기능을 에뮬레이트하는 여러 패키지가 있습니다. Nest는 @nestjs/schedule 패키지를 제공하여 널리 사용되는 Node.js cron 패키지와 통합합니다. 이번 장에서는 해당 패키지를 다룰 예정입니다.
Task Scheduling을 사용하기 위해서 필요한 의존성들을 설치해야 합니다.
$ npm install --save @nestjs/schedule
$ npm install --save-dev @types/cron
AppModule의 ScheduleModule을 import하는 것으로 스케쥴링을 활성화 할 수 있습니다.
// app.module.ts
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [
ScheduleModule.forRoot()
],
})
export class AppModule {}
cron 작업은 임의의 함수(메소드 호출)가 자동으로 호출되도록 할 수 있습니다.
cron 작업은 다음과 같은 작업을 할 수 있습니다:
다음과 같이 실행할 코드가 포함된 메서드 앞에 @Cron() 데코레이터를 사용하여 cron 작업을 선언합니다.
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
// 45초마다 한번
@Cron('45 * * * * *')
handleCron() {
this.logger.debug('Called when the current second is 45');
}
}
위와 같이 작성하면 handleCron()
함수는 매번 45초마다 실행될 것입니다. 이 의미는 xx시 xx분 45초에 실행된다는 의미입니다. 즉 1분에 한번씩 실행됩니다.
@Cron() 데코레이터는 모든 표준 cron 패턴을 지원합니다.
위의 데코레이터 에서는 45 * * * * * 를 전달했습니다. cron 패턴 문자열의 각 위치가 가지는 의미는 다음과 같습니다.
* * * * * *
| | | | | |
| | | | | 요일 ( 1 = 월요일, 2 = 화요일)
| | | | 달
| | | 날짜
| | 시간
| 분
초 (optional)
예시를 한번 들어보면서 cron pattern을 이해해보죠:
* * * * * * 매 초마다.
45 * * * * * 매 분에 45초의 시간에 ( xx 분 45초가 될 때마다 실행) 1분마다 실행
0 10 * * * * 매 10분 0초의 시간에 ( xx 시 10분 0초가 될 때마다 실행) 1시간마다 실행
0 */30 9-17 * * * 9am 과 5pm 사이에 30분마다 실행
0 30 11 * * 1-5 월요일 부터 금요일까지 11시 30분의 시간에 실행. 하루에 한번 실행
@nestjs/schedule
패키지는 일반적으로 사용되는 cron 패턴이 포함된 enum을 제공합니다.
다음과 같이 사용할 수 있습니다:
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
@Cron(CronExpression.EVERY_30_SECONDS)
handleCron() {
this.logger.debug('Called every 30 seconds');
}
}
위의 예시에서는 handleCron()
메소드를 30초에 한번씩 호출할 것입니다.
CronExpression
에서 정말 많은 옵션을 이미 정의해놨더라구요. 한번 사용해보세요.
대신에, @Cron() 데코레이터에 JS의 Date 객체를 제공할 수 있습니다.
이렇게 하면 작업이 지정된 날짜에 정확히 한 번 실행됩니다.
힌트
JavaScript 날짜 산술을 사용하여 현재 날짜를 기준으로 작업을 예약할 수 있습니다. 예를 들어, @Cron(new Date(Date.now() + 10 * 1000))는 앱이 시작된 후 10초 후에 작업이 실행되도록 예약합니다.
또한, @Cron() 데코레이터의 두 번째 매개변수로 추가 옵션을 제공할 수도 있습니다.
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class NotificationService {
@Cron('* * 0 * * *', {
name: 'notifications',
timeZone: 'Europe/Paris',
})
triggerNotifications() {}
}
cron 작업이 선언된 후에는 해당 작업에 액세스하고 제어할 수 있으며, 동적 API를 사용하여 실행 시간에 cron 패턴이 정의된 cron 작업을 동적으로 생성할 수도 있습니다. API를 통해 선언적인 cron 작업에 액세스하려면, 옵션 객체의 두 번째 인수로 선택적으로 전달되는 name 속성을 사용하여 작업에 이름을 지정해야 합니다.
특정 간격마다 메서드를 실행해야 함을 선언하기 위해 메서드 정의 앞에 @Interval() 데코레이터를 사용합니다. 아래 예시와 같이 데코레이터에 밀리초 단위의 간격 값을 전달합니다.
@Interval(10000)
handleInterval() {
this.logger.debug('Called every 10 seconds');
}
힌트
이 메커니즘은 내부적으로 JavaScript의 setInterval() 함수를 사용합니다. 또한 cron 작업을 사용하여 반복 작업을 예약할 수도 있습니다.
Declarative interval을 선언 클래스 외부에서 Dynamic API를 통해 제어하려면 다음 구문을 사용하여 간격을 이름과 연결합니다.
@Interval('notifications', 2500) // 아까 name속성으로 지정했던 것
handleInterval() {}
Dynamic API를 사용하면 간격의 속성이 실행 시간에 정의되고, 간격을 나열하고 삭제하는 것도 가능합니다.
지정된 시간이 경과한 후 메서드가 한 번 실행되어야 함을 선언하려면 메서드 정의 앞에 @Timeout() 데코레이터를 사용합니다. 데코레이터에 응용 프로그램 시작으로부터의 상대 시간 오프셋(밀리초 단위)을 전달합니다. 아래와 같이 구현합니다:
@Timeout(5000)
handleTimeout() {
this.logger.debug('Called once after 5 seconds');
}
힌트
이 메커니즘은 내부적으로 JavaScript의 setTimeout() 함수를 사용합니다.
Declarative timeout을 선언 클래스 외부에서 Dynamic API를 통해 제어하려면 다음 구문을 사용하여 타임아웃을 이름과 연결합니다.
@Timeout('notifications', 2500)
handleTimeout() {}
Dynamic API를 사용하면 실행 시간에 타임아웃의 속성이 정의되는 동적 타임아웃을 생성하고, 타임아웃을 나열하고 삭제하는 것도 가능합니다.
@nestjs/schedule
모듈은 선언적인 cron 작업, 타임아웃 및 간격을 관리할 수 있는 동적 API를 제공합니다. 이 API는 실행 시간에 속성이 정의되는 동적 cron 작업, 타임아웃 및 간격을 생성하고 관리하는 것도 가능합니다.
코드의 어디에서든지 SchedulerRegistry
API를 사용하여 이름으로 CronJob
인스턴스에 대한 참조를 가져올 수 있습니다. 먼저, 표준 생성자 주입을 사용하여 SchedulerRegistry
를 주입합니다.
constructor(private schedulerRegistry: SchedulerRegistry) {}
@nestjs/schedule
패키지에서SchedulerRegistry
를 가져옵니다.
다음과 같은 방법으로 클래스에서 사용합니다. 다음 선언으로 cron 작업이 생성되었다고 가정합니다.
@Cron('* * 8 * * *', {
name: 'notifications',
})
triggerNotifications() {}
다음과 같이 해당 작업에 액세스할 수 있습니다.
const job = this.schedulerRegistry.getCronJob('notifications');
job.stop();
console.log(job.lastDate());
getCronJob()
메서드는 지정된 이름의 cron 작업을 반환합니다. 반환된 CronJob
객체에는 다음과 같은 메서드가 있습니다.
moment
객체에 대해toDate()
를 사용하여 사람이 읽을 수 있는 형태로 변환합니다.
다음과 같이 SchedulerRegistry
의 addCronJob
메서드를 사용하여 동적으로 새로운 cron 작업을 생성합니다.
addCronJob(name: string, seconds: string) {
const job = new CronJob(`${seconds} * * * * *`, () => {
this.logger.warn(`time (${seconds}) for job ${name} to run!`);
});
this.schedulerRegistry.addCronJob(name, job);
job.start();
this.logger.warn(
`job ${name} added for each minute at ${seconds} seconds!`,
);
}
이 코드에서는 cron 패키지의 CronJob
객체를 사용하여 cron 작업을 생성합니다. CronJob
생성자는 첫 번째 인수로 cron 패턴(@Cron() 데코레이터와 동일)을, 두 번째 인수로 cron 타이머가 작동할 때 실행되는 콜백을 받습니다. SchedulerRegistry
의 addCronJob
메서드는 두 개의 인수를 받습니다: CronJob의 이름과 CronJob 객체 자체입니다.
경고
SchedulerRegistry에 접근하기 전에 SchedulerRegistry를 주입해야 합니다. cron 패키지에서 CronJob을 가져오세요.
다음과 같이 SchedulerRegistry
의 deleteCronJob
메서드를 사용하여 이름으로 cron 작업을 삭제합니다.
deleteCron(name: string) {
this.schedulerRegistry.deleteCronJob(name);
this.logger.warn(`job ${name} deleted!`);
}
다음과 같이 SchedulerRegistry
의 getCronJobs
메서드를 사용하여 모든 cron 작업을 나열합니다.
getCrons() {
const jobs = this.schedulerRegistry.getCronJobs();
jobs.forEach((value, key, map) => {
let next;
try {
next = value.nextDates().toDate();
} catch (e) {
next = 'error: next fire date is in the past!';
}
this.logger.log(`job: ${key} -> next: ${next}`);
});
}
getCronJobs()
메서드는 map
을 반환합니다. 이 코드에서는 map을 반복하고 각 CronJob
의 nextDates()
메서드에 액세스를 시도합니다. CronJob
API에서 이미 실행되었고 미래의 실행 날짜가 없는 작업은 예외를 throw합니다.
SchedulerRegistry#getInterval 메서드를 사용하여 간격에 대한 참조를 얻을 수 있습니다. 앞서 설명한 대로, 표준 생성자 주입을 사용하여 SchedulerRegistry를 주입합니다:
constructor(private schedulerRegistry: SchedulerRegistry) {}
다음과 같이 사용합니다:
const interval = this.schedulerRegistry.getInterval('notifications');
clearInterval(interval);
SchedulerRegistry
의 addInterval
메서드를 사용하여 동적으로 새로운 간격을 생성합니다:
addInterval(name: string, milliseconds: number) {
const callback = () => {
this.logger.warn(`Interval ${name} executing at time (${milliseconds})!`);
};
const interval = setInterval(callback, milliseconds);
this.schedulerRegistry.addInterval(name, interval);
}
이 코드에서는 일반적인 JavaScript 간격을 생성한 다음 SchedulerRegistry
의 addInterval
메서드에 전달합니다. 이 메서드는 두 개의 인수를 받습니다: 간격의 이름과 간격 자체입니다.
SchedulerRegistry#deleteInterval 메서드를 사용하여 이름으로 간격을 삭제합니다:
deleteInterval(name: string) {
this.schedulerRegistry.deleteInterval(name);
this.logger.warn(`Interval ${name} deleted!`);
}
SchedulerRegistry#getIntervals 메서드를 사용하여 모든 간격을 나열합니다:
getIntervals() {
const intervals = this.schedulerRegistry.getIntervals();
intervals.forEach(key => this.logger.log(`Interval: ${key}`));
}
dynamic하게 만드는 방식이 다 똑같네용.
SchedulerRegistry
의 getTimeout
메서드를 사용하여 timeout에 대한 참조를 가져옵니다. 앞서 설명한 대로, 표준 생성자 주입을 사용하여 SchedulerRegistry를 주입합니다:
constructor(private readonly schedulerRegistry: SchedulerRegistry) {}
다음과 같이 사용합니다:
const timeout = this.schedulerRegistry.getTimeout('notifications');
clearTimeout(timeout);
SchedulerRegistry
의 addTimeout
메서드를 사용하여 동적으로 새로운 timeout을 생성합니다:
addTimeout(name: string, milliseconds: number) {
const callback = () => {
this.logger.warn(`Timeout ${name} executing after (${milliseconds})!`);
};
const timeout = setTimeout(callback, milliseconds);
this.schedulerRegistry.addTimeout(name, timeout);
}
이 코드에서는 일반적인 JavaScript timeout을 생성한 다음 SchedulerRegistry
의 addTimeout
메서드에 전달합니다. 이 메서드는 두 개의 인수를 받습니다: timeout의 이름과 timeout 자체입니다.
SchedulerRegistry
의 deleteTimeout
메서드를 사용하여 이름으로 timeout을 삭제합니다:
deleteTimeout(name: string) {
this.schedulerRegistry.deleteTimeout(name);
this.logger.warn(`Timeout ${name} deleted!`);
}
SchedulerRegistry
의 getTimeouts
메서드를 사용하여 모든 timeouts을 나열합니다:
getTimeouts() {
const timeouts = this.schedulerRegistry.getTimeouts();
timeouts.forEach(key => this.logger.log(`Timeout: ${key}`));
}