Nest.js를 배워보자/8. NestJS에서 Cron, Scheduler 구현하기

DB 백업 및 정기 작업 실습 (MySQL 예시)

_Blue_Sky_ 2025. 12. 2. 21:08
728x90


실제 프로덕션 환경에서 DB 백업과 같은 정기 작업은 매우 중요합니다. NestJS의 스케줄러를 사용하여 MySQL 데이터베이스를 정기적으로 백업하는 실습 코드를 구현해 보겠습니다.

1. 🛠️ 사전 준비: 외부 명령어 실행 환경

DB 백업은 보통 **mysqldump**와 같은 외부 명령어를 실행하여 데이터베이스 스키마와 데이터를 파일로 추출하는 방식으로 이루어집니다. NestJS (Node.js)에서 외부 명령어를 실행하려면 child_process 모듈을 사용합니다. 

# child_process는 내장 모듈이므로 별도 설치 불필요
# 💡 .env 파일에 DB 접속 정보가 정의되어 있어야 합니다.
# DATABASE_HOST=localhost
# DATABASE_USER=root
# DATABASE_PASSWORD=password
# DATABASE_NAME=mynestdb
728x90

2. ⚙️ TasksService에 Cron 백업 로직 구현

TasksService에 @Cron 데코레이터를 사용하여 매일 정해진 시각에 mysqldump 명령어를 실행하는 메서드를 구현합니다.

// src/tasks/tasks.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Cron } from '@nestjs/schedule';
import { exec } from 'child_process'; // 💡 외부 명령어 실행을 위한 모듈
import * as path from 'path'; // 경로 처리를 위한 모듈

@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);

  // ConfigService를 주입받아 .env의 DB 정보를 사용합니다.
  constructor(private configService: ConfigService) {}

  // 💡 매일 새벽 3시 0분 0초에 실행
  @Cron('0 0 3 * * *') 
  async handleDbBackup() {
    this.logger.warn('--- ⏳ 정기 MySQL DB 백업 작업 시작 ---');

    const host = this.configService.get<string>('DATABASE_HOST');
    const user = this.configService.get<string>('DATABASE_USER');
    const password = this.configService.get<string>('DATABASE_PASSWORD');
    const dbName = this.configService.get<string>('DATABASE_NAME');
    
    // 💡 백업 파일 경로 설정 (프로젝트 루트/backup 폴더에 저장)
    const backupDir = path.join(process.cwd(), 'backup');
    const timestamp = new Date().toISOString().replace(/:/g, '-');
    const backupFileName = `${dbName}_${timestamp}.sql`;
    const backupPath = path.join(backupDir, backupFileName);

    // 💡 mysqldump 명령어 생성
    // --single-transaction: 백업 중에도 DB 작업이 가능하도록 함 (InnoDB 사용 시)
    const command = `mysqldump -h ${host} -u ${user} -p${password} ${dbName} --single-transaction > ${backupPath}`;

    // 💡 exec 명령을 Promise로 감싸 비동기 처리
    try {
      await new Promise<void>((resolve, reject) => {
        exec(command, (error, stdout, stderr) => {
          if (error) {
            this.logger.error(`백업 실패: ${error.message}`);
            return reject(error);
          }
          if (stderr) {
            this.logger.warn(`백업 경고: ${stderr}`);
          }
          resolve();
        });
      });
      
      this.logger.warn(`--- ✅ DB 백업 완료: ${backupFileName} ---`);
    } catch (e) {
      this.logger.error(`DB 백업 중 치명적인 오류 발생!`);
    }
  }
}
728x90

3. ⏱️ @Interval(): 정기 작업 실습 (오래된 백업 파일 정리)

DB 백업 외에도, 정기적으로 서버의 임시 파일을 정리하거나 오래된 로그를 삭제하는 등의 관리 작업을 @Interval로 구현할 수 있습니다. 여기서는 1시간마다 오래된 백업 파일을 정리하는 예시를 추가합니다.

// src/tasks/tasks.service.ts (계속)
import { Interval } from '@nestjs/schedule';
import * as fs from 'fs/promises'; // Node.js 파일 시스템 Promise API 사용

// ... TasksService 내부 ...

  // 💡 1시간(3,600,000ms)마다 실행
  @Interval(3600000)
  async cleanOldBackups() {
    const backupDir = path.join(process.cwd(), 'backup');
    const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); // 7일 전 시간

    try {
      const files = await fs.readdir(backupDir);

      for (const file of files) {
        if (file.endsWith('.sql')) {
          const filePath = path.join(backupDir, file);
          const stat = await fs.stat(filePath);
          
          // 💡 파일 생성 시간이 7일이 넘었으면 삭제
          if (stat.birthtimeMs < oneWeekAgo) {
            await fs.unlink(filePath);
            this.logger.log(`오래된 백업 파일 삭제: ${file}`);
          }
        }
      }
    } catch (e) {
      this.logger.error(`오래된 백업 파일 정리 중 오류 발생: ${e.message}`);
    }
  }
}

 

4. 📝 실습 시 주의 사항

  1. mysqldump 경로: 위 코드는 mysqldump 명령어가 시스템 환경 변수에 등록되어 있다고 가정합니다. 그렇지 않다면 명령어 경로를 명시해야 합니다.
  2. 권한: mysqldump를 실행하는 사용자(NestJS 프로세스)에게 DB 접근 권한 및 백업 폴더에 쓰기/삭제 권한이 있어야 합니다.
  3. 동시성: @Cron 메서드가 실행되는 동안 서버가 재부팅되면 작업이 중단되거나, 여러 인스턴스 환경에서 코드가 중복 실행될 수 있습니다. 프로덕션에서는 이를 방지하기 위해 **잠금 메커니즘(Locking)**을 구현해야 합니다.
728x90