NestJS 공식문서 Task Scheduling

GGAE99·2023년 7월 11일
0

NestJS 공식 문서

목록 보기
18/33

Task Scheduling

작업 예약을 임의의 코드(메서드/함수)를 특정한 날짜/시간에 실행하거나 반복 간격으로 실행하거나, 지정된 간격 후에 한 번 실행하는 기능을 제공합니다. Linux 환경에서는 이를 주로 cron과 같은 패키지가 OS 수준에서 처리합니다. Node.js 애플리케이션에서는 cron과 유사한 기능을 에뮬레이트하는 여러 패키지가 있습니다. Nest는 @nestjs/schedule 패키지를 제공하여 널리 사용되는 Node.js cron 패키지와 통합합니다. 이번 장에서는 해당 패키지를 다룰 예정입니다.

Installation

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 {}

Declarative cron jobs

cron 작업은 임의의 함수(메소드 호출)가 자동으로 호출되도록 할 수 있습니다.
cron 작업은 다음과 같은 작업을 할 수 있습니다:

  • 지정된 날짜/시간에 한번.
  • 지정된 간격마다 반복적으로 (ex_ 시간당 한 번, 주당 한 번, 5분마다 한 번) 수행.

다음과 같이 실행할 코드가 포함된 메서드 앞에 @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 패턴을 지원합니다.

  • 별표(예: *)
  • 범위(예: 1-3,5)
  • 단계(예: */2)

위의 데코레이터 에서는 45 * * * * * 를 전달했습니다. cron 패턴 문자열의 각 위치가 가지는 의미는 다음과 같습니다.

* * * * * *
| | | | | |
| | | | | 요일 ( 1 = 월요일, 2 = 화요일)
| | | || | | 날짜
| | 시간
| 분
초 (optional)

예시를 한번 들어보면서 cron pattern을 이해해보죠:

* * * * * *	    매 초마다.
45 * * * * *	매 분에 45초의 시간에 ( xx 분 45초가 될 때마다 실행) 1분마다 실행
0 10 * * * *	매 100초의 시간에 ( xx 시 100초가 될 때마다 실행) 1시간마다 실행
0 */30 9-17 * * *	9am 과 5pm 사이에 30분마다 실행
0 30 11 * * 1-5	월요일 부터 금요일까지 1130분의 시간에 실행. 하루에 한번 실행

@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() 데코레이터의 두 번째 매개변수로 추가 옵션을 제공할 수도 있습니다.

  • name : 선언된 후에 cron 작업에 액세스하고 제어하는 데 유용합니다.
  • timeZone : 실행을 위한 시간대를 지정합니다. 이는 실제 시간을 해당 시간대에 상대적으로 수정합니다. 시간대가 잘못된 경우 오류가 발생합니다. 사용 가능한 모든 시간대는 Moment Timezone에서 확인할 수 있습니다.
  • utcOffset : timeZone 매개변수 대신에 시간대의 오프셋을 지정할 수 있습니다.
  • disabled : 작업이 실행되는지 여부를 나타냅니다.
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 속성을 사용하여 작업에 이름을 지정해야 합니다.

Declarative intervals

특정 간격마다 메서드를 실행해야 함을 선언하기 위해 메서드 정의 앞에 @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를 사용하면 간격의 속성이 실행 시간에 정의되고, 간격을 나열하고 삭제하는 것도 가능합니다.

Declarative timeouts

지정된 시간이 경과한 후 메서드가 한 번 실행되어야 함을 선언하려면 메서드 정의 앞에 @Timeout() 데코레이터를 사용합니다. 데코레이터에 응용 프로그램 시작으로부터의 상대 시간 오프셋(밀리초 단위)을 전달합니다. 아래와 같이 구현합니다:

@Timeout(5000)
handleTimeout() {
  this.logger.debug('Called once after 5 seconds');
}

힌트
이 메커니즘은 내부적으로 JavaScript의 setTimeout() 함수를 사용합니다.
Declarative timeout을 선언 클래스 외부에서 Dynamic API를 통해 제어하려면 다음 구문을 사용하여 타임아웃을 이름과 연결합니다.

@Timeout('notifications', 2500)
handleTimeout() {}

Dynamic API를 사용하면 실행 시간에 타임아웃의 속성이 정의되는 동적 타임아웃을 생성하고, 타임아웃을 나열하고 삭제하는 것도 가능합니다.

Dynamic schedule module API

@nestjs/schedule 모듈은 선언적인 cron 작업, 타임아웃 및 간격을 관리할 수 있는 동적 API를 제공합니다. 이 API는 실행 시간에 속성이 정의되는 동적 cron 작업, 타임아웃 및 간격을 생성하고 관리하는 것도 가능합니다.

Dynamic cron jobs

코드의 어디에서든지 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 객체에는 다음과 같은 메서드가 있습니다.

  • stop(): 작업 예약을 중지합니다.
  • start(): 중지된 작업을 다시 시작합니다.
  • setTime(time: CronTime): 작업을 중지하고 새로운 시간을 설정한 후 시작합니다.
  • lastDate(): 작업이 실행된 마지막 날짜의 문자열 표현을 반환합니다.
  • nextDates(count: number): 예정된 작업 실행 날짜를 나타내는 moment 객체의 배열(크기 count)을 반환합니다.

moment 객체에 대해 toDate()를 사용하여 사람이 읽을 수 있는 형태로 변환합니다.

다음과 같이 SchedulerRegistryaddCronJob 메서드를 사용하여 동적으로 새로운 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 타이머가 작동할 때 실행되는 콜백을 받습니다. SchedulerRegistryaddCronJob 메서드는 두 개의 인수를 받습니다: CronJob의 이름과 CronJob 객체 자체입니다.

경고
SchedulerRegistry에 접근하기 전에 SchedulerRegistry를 주입해야 합니다. cron 패키지에서 CronJob을 가져오세요.

다음과 같이 SchedulerRegistrydeleteCronJob 메서드를 사용하여 이름으로 cron 작업을 삭제합니다.

deleteCron(name: string) {
  this.schedulerRegistry.deleteCronJob(name);
  this.logger.warn(`job ${name} deleted!`);
}

다음과 같이 SchedulerRegistrygetCronJobs 메서드를 사용하여 모든 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을 반복하고 각 CronJobnextDates() 메서드에 액세스를 시도합니다. CronJob API에서 이미 실행되었고 미래의 실행 날짜가 없는 작업은 예외를 throw합니다.

Dynamic intervals

SchedulerRegistry#getInterval 메서드를 사용하여 간격에 대한 참조를 얻을 수 있습니다. 앞서 설명한 대로, 표준 생성자 주입을 사용하여 SchedulerRegistry를 주입합니다:

constructor(private schedulerRegistry: SchedulerRegistry) {}

다음과 같이 사용합니다:

const interval = this.schedulerRegistry.getInterval('notifications');
clearInterval(interval);

SchedulerRegistryaddInterval 메서드를 사용하여 동적으로 새로운 간격을 생성합니다:

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 간격을 생성한 다음 SchedulerRegistryaddInterval 메서드에 전달합니다. 이 메서드는 두 개의 인수를 받습니다: 간격의 이름과 간격 자체입니다.

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 timeouts

dynamic하게 만드는 방식이 다 똑같네용.
SchedulerRegistrygetTimeout 메서드를 사용하여 timeout에 대한 참조를 가져옵니다. 앞서 설명한 대로, 표준 생성자 주입을 사용하여 SchedulerRegistry를 주입합니다:

constructor(private readonly schedulerRegistry: SchedulerRegistry) {}

다음과 같이 사용합니다:

const timeout = this.schedulerRegistry.getTimeout('notifications');
clearTimeout(timeout);

SchedulerRegistryaddTimeout 메서드를 사용하여 동적으로 새로운 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을 생성한 다음 SchedulerRegistryaddTimeout 메서드에 전달합니다. 이 메서드는 두 개의 인수를 받습니다: timeout의 이름과 timeout 자체입니다.
SchedulerRegistrydeleteTimeout 메서드를 사용하여 이름으로 timeout을 삭제합니다:

deleteTimeout(name: string) {
  this.schedulerRegistry.deleteTimeout(name);
  this.logger.warn(`Timeout ${name} deleted!`);
}

SchedulerRegistrygetTimeouts 메서드를 사용하여 모든 timeouts을 나열합니다:

getTimeouts() {
  const timeouts = this.schedulerRegistry.getTimeouts();
  timeouts.forEach(key => this.logger.log(`Timeout: ${key}`));
}

질문 및 생각

  • 사용자 설정에서 특정 시간대에 알람을 보내달라고 설정할 때 dynamic하게 사용할 수 있을 것 같다.

0개의 댓글