실시간 API:
사용자 요청 → 즉시 처리 → 응답 반환
배치 작업:
정해진 시간 → 서버가 자동 실행 → 결과 저장
반복 업무 자동화
운영자 실수 감소
서버 부하 분산
운영 데이터 품질 유지
예시:
매일 새벽 3시
↓
전날 상담 신청 통계 집계
↓
관리자 대시보드용 summary 테이블 업데이트
@nestjs/schedule을 사용하면 Cron 표현식으로 정기 작업을 만들 수 있습니다.매분 실행:
* * * * *
매일 새벽 3시 실행:
0 3 * * *
매주 월요일 새벽 4시 실행:
0 4 * * 1
매월 1일 새벽 2시 실행:
0 2 1 * *
* * * * *
│ │ │ │ │
│ │ │ │ └─ 요일
│ │ │ └─── 월
│ │ └───── 일
│ └─────── 시
└───────── 분
매일 00:10
↓
전날 00:00 ~ 23:59 데이터 집계
↓
DailySummary 테이블 저장
매 10분마다
↓
FAILED 상태 중 재시도 가능한 작업 조회
↓
Queue에 재처리 Job 등록
매일 새벽 4시
↓
7일 지난 Export 파일 조회
↓
S3 파일 삭제
↓
ExportJob 상태 EXPIRED 변경
매일 새벽 2시
↓
노출 상품 조회
↓
EP XML/CSV 생성
↓
S3 업로드
↓
외부 플랫폼이 파일 수집
| 구분 | 배치 작업 | Queue |
|---|---|---|
| 실행 기준 | 시간/주기 | 작업 등록 |
| 예시 | 매일 새벽 통계 집계 | 알림톡 발송 Job 처리 |
| 목적 | 정기 실행 | 비동기 처리 |
| 처리 방식 | 정해진 시간에 실행 | Worker가 작업을 꺼내 처리 |
Cron Scheduler
↓
오늘 재처리할 실패 알림 조회
↓
Queue에 Job 등록
↓
Worker가 순차 처리
@nestjs/schedule을 사용해 정기 작업을 만들 수 있습니다.SchedulerModule이나 BatchModule을 따로 두고 관리하는 것이 좋습니다.npm install @nestjs/schedule
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [
ScheduleModule.forRoot(),
],
})
export class AppModule {}
@Injectable()
export class DailyStatsScheduler {
constructor(
private readonly dailyStatsService: DailyStatsService,
) {}
@Cron('10 0 * * *', {
timeZone: 'Asia/Seoul',
})
async handleDailyStats() {
await this.dailyStatsService.aggregateYesterday();
}
}
timeZone: 'Asia/Seoul'을 명확히 지정하는 것이 좋습니다.한국 시간 2026-07-03 00:10
UTC 시간 2026-07-02 15:10
서버가 UTC 기준으로 어제 데이터를 집계하면:
한국 기준 날짜와 다를 수 있음
DB 저장:
UTC 권장
집계 기준:
Asia/Seoul 기준 날짜
관리자 표시:
Asia/Seoul 변환 표시
model DailyConsultSummary {
id Int @id @default(autoincrement())
date DateTime
source String?
productId Int?
totalCount Int @default(0)
pendingCount Int @default(0)
doneCount Int @default(0)
cancelCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([date, source, productId])
@@index([date])
@@index([source, date])
}
@@unique([date, source, productId])로 같은 기준의 중복 집계를 막습니다.1. 어제 날짜 범위 계산
2. consults 테이블에서 source/productId별 집계
3. DailyConsultSummary에 upsert
4. 집계 성공 이력 저장
5. 관리자 대시보드에서 summary 테이블 조회
await this.prisma.dailyConsultSummary.upsert({
where: {
date_source_productId: {
date,
source,
productId,
},
},
update: {
totalCount,
pendingCount,
doneCount,
cancelCount,
},
create: {
date,
source,
productId,
totalCount,
pendingCount,
doneCount,
cancelCount,
},
});
upsert를 사용합니다.model BatchJobLog {
id Int @id @default(autoincrement())
jobName String
status String
startedAt DateTime
endedAt DateTime?
durationMs Int?
targetDate DateTime?
processedCount Int?
successCount Int?
failCount Int?
errorMessage String?
createdAt DateTime @default(now())
@@index([jobName, startedAt])
@@index([status, startedAt])
}
jobName=DAILY_CONSULT_SUMMARY
status=SUCCESS
targetDate=2026-07-02
processedCount=428
durationMs=1540
매일 00:10 통계 집계 시작
↓
작업이 20분 걸림
↓
00:20에 같은 작업이 수동 실행됨
↓
같은 날짜 데이터가 중복 처리될 수 있음
배치 시작
↓
Redis에 lock key 생성 시도
↓
성공하면 작업 실행
↓
실패하면 이미 실행 중으로 판단하고 종료
↓
작업 완료 후 lock 해제
lock:batch:daily-consult-summary:2026-07-02
lock:batch:ep-generate:naver
lock:batch:export-cleanup
Lock TTL 없음:
서버 장애 시 lock이 영원히 남음
Lock TTL 있음:
일정 시간 후 자동 해제
매일 통계 집계 실행
↓
summary 테이블에 insert
↓
같은 날짜 작업 재실행
↓
같은 날짜 summary가 두 줄 생성
매일 통계 집계 실행
↓
date + source + productId 기준 upsert
↓
같은 날짜 작업 재실행
↓
기존 summary row 업데이트
1. 노출 중인 상품 조회
2. 플랫폼별 필드로 변환
3. XML/CSV/TXT 파일 생성
4. S3 또는 서버 public 경로에 업로드
5. 생성 이력 저장
6. 외부 플랫폼이 파일 수집
주의:
EP 파일 생성 실패 후 빈 파일을 업로드하면
외부 플랫폼에서 전체 상품이 사라진 것처럼 인식할 수 있음
1. expiresAt이 지난 ExportJob 조회
2. S3 fileKey 확인
3. S3 파일 삭제
4. ExportJob 상태 EXPIRED 변경
5. 정리 결과 로그 저장
async cleanupExpiredExports() {
const expiredJobs = await this.prisma.exportJob.findMany({
where: {
status: 'COMPLETED',
expiresAt: {
lt: new Date(),
},
},
select: {
id: true,
fileKey: true,
},
take: 100,
});
for (const job of expiredJobs) {
if (job.fileKey) {
await this.s3Service.deleteObject(job.fileKey);
}
await this.prisma.exportJob.update({
where: {
id: job.id,
},
data: {
status: 'EXPIRED',
},
});
}
return {
processedCount: expiredJobs.length,
};
}
take로 제한할 수 있습니다.재처리 가능:
TIMEOUT
NETWORK_ERROR
PROVIDER_500
RATE_LIMIT
재처리 제외:
INVALID_PHONE
INVALID_TEMPLATE
AUTH_FAILED
PERMISSION_DENIED
1. FAILED 상태 알림 로그 조회
2. retryable=true인 것만 필터
3. attempts < maxAttempts 확인
4. Queue에 재발송 Job 등록
5. attempts 증가 또는 RETRYING 상태 변경
6. 결과 이력 저장
배치 실패
↓
Slack/이메일/관리자 알림 등록
↓
운영자 확인
↓
수동 재실행 또는 원인 수정
[배치 실패] DAILY_CONSULT_SUMMARY
대상 날짜: 2026-07-02
시작 시간: 2026-07-03 00:10
실패 시간: 2026-07-03 00:11
에러: DB connection timeout
처리 건수: 0
관리자가 1년치 통계를 재집계
↓
DB 부하 급증 가능
대응:
날짜 범위 제한
Queue 분리
권한 제한
실행 전 확인
src/
batch/
batch.module.ts
batch-log.service.ts
daily-stats/
daily-stats.scheduler.ts
daily-stats.service.ts
ep-generator/
ep-generator.scheduler.ts
ep-generator.service.ts
export-cleanup/
export-cleanup.scheduler.ts
export-cleanup.service.ts
notification-retry/
notification-retry.scheduler.ts
notification-retry.service.ts
Scheduler:
언제 실행할지 결정
Service:
무엇을 처리할지 결정
Queue Worker:
실제 무거운 작업 처리
LogService:
성공/실패 이력 저장
NestJS + Prisma 서비스에서 정기 배치 작업을 설계하고 싶어.
상황:
1. 매일 새벽 00:10에 전날 상담 신청 통계를 집계해야 함
2. 기준 타임존은 Asia/Seoul
3. 유입경로와 상품별로 total, pending, done, cancelled 수를 저장하고 싶음
4. 같은 날짜 배치를 수동으로 다시 실행할 수 있어야 함
5. 중복 실행은 막아야 함
6. 집계 결과는 upsert로 저장하고 싶음
7. 배치 실행 성공/실패 이력을 남기고 싶음
8. 실패하면 Slack 또는 관리자 알림으로 알려주고 싶음
9. 나중에 EP 파일 생성, Export 파일 정리 배치도 같은 구조로 추가하고 싶음
요청:
- Prisma 모델 설계
- Cron 설정
- 타임존 처리
- 중복 실행 방지
- 멱등성 처리
- BatchJobLog 구조
- 실패 알림 구조
- 폴더 구조
를 실무 기준으로 설명해줘.
@nestjs/schedule을 사용해 Cron 기반 스케줄러를 구성할 수 있습니다.Asia/Seoul 기준 타임존을 명확히 지정하고, 집계 날짜 범위를 정확히 계산해야 합니다.BatchJobLog 같은 테이블에 기록해 성공/실패, 처리 건수, 실행 시간을 추적해야 합니다.