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