BackEnd/Nest.js

[Nest.js] 태스크 스케줄링

Grace 2023. 5. 31. 17:15

@nestjs/schedule 패키지

서비스를 개발하다 보면 주기적으로 동일한 작업을 처리해야 하는 경우가 생깁니다. 이런 주기적 반복 작업을 태스크 또는 배치라고 부릅니다. 태스크 스케줄링을 잘 활용하면 특정 기간마다 수행해야 하는 귀찮은 작업을 신경쓰지 않아도 됩니다.

태스크 스케줄링을 반드시 반복적인 작업에만 적용해야 하는 것은 아닙니다. 1회성 태스크를 만들 수도 있습니다.

리눅스에는 태스크 스케줄링을 담당하는 크론이라는 기능이 있습니다. Node.js에는 cron과 같은 기능을 하는 여러 라이브러리가 있습니다. Nest는 인기 패키지인 node-cron을 통합한 @nestjs/schedule 패키지를 제공합니다.

$ npm i @nestjs/schedule @types/cron

태스크 스케줄링은 @nestjs/schedule 패키지에 포함된 ScheduleModule을 사용합니다. 이 모듈은 AppModule로 바로 가져와도 되지만 태스크 관련 작업을 담당하는 별도의 모듈인 BatchModule에 작성해서 가져올 수도 있습니다.

import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { TaskService } from './task.service';

@Module({
  import: [
    ScheduleModule.forRoot()
  ],
  providers: [TaskService]
}) 
export class BatchModule {}

ScheduleModuleforRoot() 메서드를 통해 가져오는데, 이 과정에서 Nest는 스케줄러를 초기화하고 앱에 선언한 크론 잡과 타임아웃, 인터벨을 등록합니다. 타임아웃은 스케줄링이 끝나는 시각이고 인터벌은 주기적으로 반복되는 시간 간격을 뜻합니다. 데스크 스케줄링은 모든 모듈이 예약된 작업을 로드하고 확인하는 onApplicationBootstrap 생명주기 훅이 발생할 때 등록됩니다. ScheduleModule에는 데스크를 등록하는 방법이 여러 가지 있습니다.

태스크 스케줄링을 선언하는 3가지 방식

BatchModule에는 TaskService 프로바이더를 가지고 있습니다. TaskService에 실제 수행되는 태스크를 구현하고 있습니다.

크론 잡 선언 방식

크론 잡 선언 방식은 @Cron 데코레이터를 선언한 메서드를 태스크로 구현하는 방식입니다.

import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';

@Injectable()
export class TaskService {
  private readonly logger = new Logger(TaskService.name);
  
  @Cron('* * * * * *', { name: 'cronTask'})
  handleCron() {
    this.logger.log('Task Called');
  }
}

@Cron의 첫 번째 인수는 태스크의 반복 주기로서 표준 크론 패턴을 따릅니다. 공백으로 구분된 여서 개의 값을 가지는 문자열을 입력받는데, 각 자리별 의미는 다음과 같습니다. 첫 번째 자리는 초를 나타내는 자리인데, 이를 생략하고 다섯 자리면 표기하면 초를 0으로 취급합니다.

* * * * * *
| | | | | |
| | | | | day of week (요일, 0-7의 값을 가짐, 0과 7은 일요일)
| | | | month (월, 0-12의 값을 가짐. 0과 12는 12월)
| | | day of month (날, 1-31의 값을 가짐)
| | hour (시간, 0-23의 값을 가짐)
| minuate (분, 0-59의 값을 가짐)
second (초, 0-59의 값을 가짐, 선택 사항)

패턴의미

* * * * * * 초마다
45 * * * * * 매분 45초에
0 10 * * * * 매시간, 10분에
0 /30 9-17 * * 오전 9시부터 오후 5시까지 30분마다
0 30 11 * * 1-5 월요일~금요일 오전 11시 30분에

한 번만 수행되는 태스크를 등록하려면 수행되는 시각을 Date 객체로 직접 설정하면 됩니다.

@Cron(new Date(Date.now() + 3 * 1000))

Nest는 자주 사용할 만한 크론 패턴을 CronExpression 열거형으로 제공합니다.

@Cron(CronExpression.MONDAY_TO_FRIDAY_AT_1AM)

@Cron 데코레이터의 두 번째 인수는 CronOptions 객체입니다. CronOptions 속성은 다음과 같이 활용할 수 있습니다.

속성설명

name 데스크의 이름, 선언한 크론 잡에 액세스하고 이를 제어하는 데 유용합니다.
timeZone 실행 시간대를 지정합니다. 시간대가 유효하지 않으면 오류를 발생합니다. Moment Timezone 등의 웹 페이지에서 사용 가능한 모든 시간대를 확인할 수 있습니다.
utcOffset timeZone 대신 UTC 기반으로 시간대의 오프셋을 지정할 수 있습니다. 우리나라의 시간대를 설정하려면 문자열 ‘+09:00’을 사용하거나 숫자 9를 사용합니다.
unrefTimeout 이 속성은 Node.js의 timeout.unref()와 관련 있습니다. 이벤트 루프를 계속 실행하는 코드가 있고 크론 잡의 상태에 관계없이 잡이 완료될 때 노드 프로세스를 중지하고 싶을 때 사용할 수 있습니다.
⚠️ timeZone 옵션과 utcOffset 옵션을 함께 사용하면 이상 동작을 일으킬 수 있습니다.

인터벌 선언 방식

태스크 수행 함수에 @Interval 데코레이터를 사용할 수도 있습니다. 첫 번쨰 인수는 태스크의 이름, 두 번째 인수는 타임아웃 시간(밀리세컨드)입니다.

@Interval('intervalTask', 3000)
hadleInterval() {
  this.logger.log('Task Called by interval')
}

타임아웃 선언 방식

타임아웃 선언 방식은 앱이 실행된 후 태스크를 단 한번만 수행합니다. @Timeout 데코레이터를 사용하고, 인수는 인터벌과 동일합니다.

@Timeout('timeoutTask', 5000)
handleTimeout() {
  this.logger.log('Task Called by timeout')
}

동적 태스크 스케줄링

앱 구동 중 특정 조건을 만족했을 때 태스크를 등록해야 하는 요구 사항이 있을 경우 동적으로 태스크를 등록/해제할 방법이 필요합니다. 동적 태스크 스케줄링은 SchedulerRegistry에서 제공하는 API를 사용합니다.

import { CronJob } from 'cron';

@Injectable()
export class TaskService {
  private readonly logger = new Logger(TaskService.name);
  
  // SchedulerRegistry 객체를 TaskService에 주입
  constuctor(private schedulerRegistry: SchedulerRegistry) { 
    this.addCronJob(); /* TaskService가 생성될 떄 크론 잡 하나를 SchedulerRegistry에
      추가합니다. SchedulerRegistry에 크론 잡을 추가만 해두는 것이고 태스크 스케줄링을 등록하는
      것이 아닙니다 */
  }
  
  addCronJob() {
    const name = 'cronSample';
    
    const job = new CronJob('* * * * * *', () => {
      this.logger.warn(`run! ${name}`)
    })
    
    this.schedulerRegistry.addCronJob(name, job);
    
    this.logger.warn(`job ${name} added!`);
  }
}
import { Controller, Post } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';

@Controller('batches')
export class BatchController {
  //컨트롤러에 SchedulerRegistry 주입
  constructor(private scheduler: SchedulerRegistry) { } 
  
  @Post('/start-sample')
  start() {
    // SchedulerRegistry에 등록된 크론 잡을 가져옵니다. 등록할 때는 선언한 이름을 사용합니다.
    const job = this.scheduler.getCronJob('cronSample');
    
    job.start(); // 크론 잡을 실행하거나 중지
    console.log('start!!', job.lastDate())
  }
  
  @Post('/stop-sample')
  stop() {
    // SchedulerRegistry에 등록된 크론 잡을 가져옵니다. 등록할 때는 선언한 이름을 사용합니다.
    const job = this.scheduler.getCronJob('cronSample')
    
    job.stop() // 크론 잡을 실행하거나 중지
    console.log('stopped!!', job.lastDate())
  }
}

BatchController를 모듈에 선언합니다.

import { BatchController } from './batch.controller';

@Module({
  controllers: [BatchController],
  ...
})
export class BatchModule { }

@Cron 데코레이터를 사용할 때 인수로 이름을 지정할 수 있습니다. 마찬가지로 이 이름을 이용하여 크론 잡 객체를 얻을 수 있습니다. CronJob 객체가 제공하는 주요 메서드는 다음과 같습니다.

  • stop(): 실행이 예약된 작업을 중지합니다.
  • start(): 중지된 작업을 다시 시작합니다.
  • setTime(time: Crontime): 현재 작업을 중지하고 새로운 시간을 설정하여 다시 시작합니다.
  • lastDate(): 작업이 마지막으로 실행된 날짜를 반환합니다.
  • nextDates(count: number): 예정된 작업의 실행 시각을 count 개수만큼 배열로 반환합니다. 배열의 각 요소는 moment 객체입니다.

인터벌과 타임아웃 역시 SchedulerRegistry에서 제공하는 메서드를 이용하여 동적으로 제어할 수 있습니다.

'BackEnd > Nest.js' 카테고리의 다른 글

[Nest.js] 헬스 체크  (0) 2023.06.01
[Nest.js] 인터셉터  (0) 2023.05.31
[Nest.js] 예외 필터  (0) 2023.05.30
[Nest.js] 로깅  (0) 2023.05.30
[Nest.js] JWT 인증/인가  (0) 2023.05.26