TypeScript

[NestJS] Schedule Lock

yonikim 2025. 4. 13. 08:41
728x90

 

NestJS 에서 Task Scheduling 을 통해 반복적인 작업을 진행할 때, 단일 인스턴스에서 실행되고 작업이 짧게 끝나는 경우에는 Lock 이 불필요하다. 

하지만 아래와 같은 문제가 발생할 수 있기 때문에, Lock 을 설정해주는 것이 좋다.

1. 중복 실행 문제 

  • 같은 배치 작업이 여러 개의 서버에서 동시에 실행되면 데이터가 중복 처리될 가능성이 있음
  • ex. 이메일 발송, 결제 처리, 재고 업데이트 등

2. 데이터 무결성 문제

  • 여러 개의 배치 프로세스가 동일한 데이터를 동시에 업데이트하면 데이터 충돌 발생 가능
  • ex. 같은 주문을 여러 번 처리하거나, 같은 데이터를 여러 번 변경하는 경우 

3. 리소스 낭비 

  • 동일한 배치 작업이 여러 번 실행되면, 불필요한 데이터베이스 요청이나 API 호출로 서버 부하 증가

 

Lock 구현 방법

1. Redis 기반의 Lock (Redlock)

▷ batch.service.ts

import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import Redis from 'ioredis';
import Redlock from 'redlock';

@Injectable()
export class BatchService {
  private readonly redisClient = new Redis();
  private readonly redlock = new Redlock([this.redisClient], {
    retryCount: 3,
    retryDelay: 200, // 200ms 후 재시도
  });

  @Cron('*/5 * * * * *') // 5초마다 실행
  async processBatch() {
    const resource = 'locks:batch:process';
    const ttl = 5000; // 5초 동안 락 유지

    try {
      const lock = await this.redlock.lock(resource, ttl);
      console.log('🚀 배치 작업 실행 중...');

      // ✅ 실제 배치 로직 실행
      await this.executeJob();

      // 락 해제
      await lock.unlock();
    } catch (err) {
      console.log('⚠️ 다른 프로세스에서 이미 실행 중...');
    }
  }

  private async executeJob() {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    console.log('✅ 배치 완료!');
  }
}

 

2. 데이터베이스 기반의 Lock

import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { DataSource } from 'typeorm';

@Injectable()
export class BatchService {
  constructor(private readonly dataSource: DataSource) {}

  @Cron('*/10 * * * * *') // 10초마다 실행
  async processBatch() {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      const result = await queryRunner.manager.query(
        `SELECT * FROM batch_locks WHERE id = 1 FOR UPDATE`
      );

      if (!result.length) {
        console.log('⚠️ 실행 가능한 배치 없음');
        return;
      }

      console.log('🚀 배치 작업 실행');
      await this.executeJob();

      await queryRunner.commitTransaction();
    } catch (err) {
      console.error('❌ 배치 작업 중 오류:', err);
      await queryRunner.rollbackTransaction();
    } finally {
      await queryRunner.release();
    }
  }

  private async executeJob() {
    await new Promise((resolve) => setTimeout(resolve, 5000));
    console.log('✅ 배치 완료!');
  }
}

 

 

🤔어떤 방법을 선택할까? 

방법 장점 단점 추천 환경
Redis Redlock 빠른 락 관리, 분산 환경 지원 Redis 필요, TTL 설정 주의 서버 여러 대에서 배치 실행 시
DB Lock 추가 인프라 불필요, 강력한 락 성능 저하 가능, 트랜잭션 유지 부담 단일 서버에서 DB 중심 관리 시
728x90