@m16khb/nestjs-sidequest 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +402 -0
- package/README.md +402 -0
- package/dist/index.cjs +1037 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +900 -0
- package/dist/index.d.ts +900 -0
- package/dist/index.js +1014 -0
- package/dist/index.js.map +1 -0
- package/package.json +85 -0
package/README.ko.md
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# @m16khb/nestjs-sidequest
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@m16khb/nestjs-sidequest)
|
|
4
|
+
[](https://www.gnu.org/licenses/lgpl-3.0.html)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://nestjs.com/)
|
|
7
|
+
|
|
8
|
+
[English](https://github.com/m16khb/npm-library/blob/main/packages/nestjs-sidequest/README.md) | [한국어](#)
|
|
9
|
+
|
|
10
|
+
**[Sidequest.js](https://sidequestjs.com/)용 NestJS 통합 라이브러리** - Redis 없이 데이터베이스 기반 백그라운드 Job 처리.
|
|
11
|
+
|
|
12
|
+
기존 데이터베이스(PostgreSQL, MySQL, MongoDB, SQLite)를 활용해 백그라운드 Job을 처리하세요. NestJS 데코레이터와 선택적 CLS 통합을 완벽 지원합니다.
|
|
13
|
+
|
|
14
|
+
## 특징
|
|
15
|
+
|
|
16
|
+
- **데이터베이스 네이티브 Job** - Redis 대신 기존 데이터베이스 사용
|
|
17
|
+
- **트랜잭션 일관성** - 데이터베이스 트랜잭션 내에서 원자적 Job 생성
|
|
18
|
+
- **데코레이터 기반 API** - 익숙한 `@Processor`, `@OnJob`, `@Retry` 데코레이터
|
|
19
|
+
- **선택적 CLS 지원** - nestjs-cls로 컨텍스트 전파
|
|
20
|
+
- **이벤트 핸들러** - Job 생명주기 이벤트용 `@OnJobComplete`, `@OnJobFailed`
|
|
21
|
+
- **다중 큐 지원** - 개별 동시성 설정으로 여러 큐 구성
|
|
22
|
+
- **대시보드** - Job 모니터링을 위한 내장 UI (선택 사항)
|
|
23
|
+
|
|
24
|
+
## 설치
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @m16khb/nestjs-sidequest sidequest
|
|
28
|
+
|
|
29
|
+
# 데이터베이스 백엔드 선택
|
|
30
|
+
npm install @sidequest/postgres-backend # PostgreSQL
|
|
31
|
+
npm install @sidequest/mysql-backend # MySQL
|
|
32
|
+
npm install @sidequest/sqlite-backend # SQLite
|
|
33
|
+
npm install @sidequest/mongo-backend # MongoDB
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 선택적 의존성
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# CLS 컨텍스트 전파를 위해
|
|
40
|
+
pnpm add nestjs-cls
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 요구사항
|
|
44
|
+
|
|
45
|
+
- Node.js >= 22.6.0
|
|
46
|
+
- NestJS >= 10.0.0
|
|
47
|
+
- TypeScript >= 5.7
|
|
48
|
+
- 지원되는 데이터베이스 백엔드
|
|
49
|
+
|
|
50
|
+
## 빠른 시작
|
|
51
|
+
|
|
52
|
+
### 1. 모듈 등록
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// app.module.ts
|
|
56
|
+
import { Module } from '@nestjs/common';
|
|
57
|
+
import { SidequestModule } from '@m16khb/nestjs-sidequest';
|
|
58
|
+
|
|
59
|
+
@Module({
|
|
60
|
+
imports: [
|
|
61
|
+
SidequestModule.forRoot({
|
|
62
|
+
backend: {
|
|
63
|
+
driver: '@sidequest/postgres-backend',
|
|
64
|
+
config: process.env.DATABASE_URL,
|
|
65
|
+
},
|
|
66
|
+
queues: [
|
|
67
|
+
{ name: 'email', concurrency: 5 },
|
|
68
|
+
{ name: 'reports', concurrency: 2 },
|
|
69
|
+
],
|
|
70
|
+
}),
|
|
71
|
+
],
|
|
72
|
+
})
|
|
73
|
+
export class AppModule {}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Job 클래스 정의 (Sidequest.js 패턴)
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// jobs/send-welcome-email.job.ts
|
|
80
|
+
import { Job } from 'sidequest';
|
|
81
|
+
|
|
82
|
+
export class SendWelcomeEmailJob extends Job {
|
|
83
|
+
constructor(
|
|
84
|
+
public readonly to: string,
|
|
85
|
+
public readonly name: string,
|
|
86
|
+
) {
|
|
87
|
+
super();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Processor 생성
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// email.processor.ts
|
|
96
|
+
import { Processor, OnJob, Retry, OnJobComplete, OnJobFailed } from '@m16khb/nestjs-sidequest';
|
|
97
|
+
import { SendWelcomeEmailJob } from './jobs/send-welcome-email.job';
|
|
98
|
+
|
|
99
|
+
@Processor('email')
|
|
100
|
+
export class EmailProcessor {
|
|
101
|
+
constructor(private readonly mailer: MailerService) {}
|
|
102
|
+
|
|
103
|
+
@OnJob(SendWelcomeEmailJob)
|
|
104
|
+
@Retry({ maxAttempts: 3, backoff: { type: 'exponential', delay: 1000 } })
|
|
105
|
+
async handleWelcomeEmail(job: SendWelcomeEmailJob) {
|
|
106
|
+
await this.mailer.send({
|
|
107
|
+
to: job.to,
|
|
108
|
+
subject: `${job.name}님, 환영합니다!`,
|
|
109
|
+
template: 'welcome',
|
|
110
|
+
});
|
|
111
|
+
return { sentAt: new Date() };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@OnJobComplete(SendWelcomeEmailJob)
|
|
115
|
+
async onComplete(event: JobCompleteEvent) {
|
|
116
|
+
console.log(`이메일 발송 완료: ${event.result.sentAt}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@OnJobFailed(SendWelcomeEmailJob)
|
|
120
|
+
async onFailed(event: JobFailedEvent) {
|
|
121
|
+
console.error(`이메일 발송 실패: ${event.error.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 4. Queue 주입 및 Job 추가
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// user.service.ts
|
|
130
|
+
import { Injectable } from '@nestjs/common';
|
|
131
|
+
import { InjectQueue, IQueueService } from '@m16khb/nestjs-sidequest';
|
|
132
|
+
import { SendWelcomeEmailJob } from './jobs/send-welcome-email.job';
|
|
133
|
+
|
|
134
|
+
@Injectable()
|
|
135
|
+
export class UserService {
|
|
136
|
+
constructor(
|
|
137
|
+
@InjectQueue('email') private emailQueue: IQueueService,
|
|
138
|
+
) {}
|
|
139
|
+
|
|
140
|
+
async createUser(email: string, name: string) {
|
|
141
|
+
// 데이터베이스에 사용자 생성...
|
|
142
|
+
const user = await this.userRepository.save({ email, name });
|
|
143
|
+
|
|
144
|
+
// 환영 이메일 큐에 추가 (백그라운드 실행)
|
|
145
|
+
await this.emailQueue.add(SendWelcomeEmailJob, email, name);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async scheduleWelcomeEmail(email: string, name: string, sendAt: Date) {
|
|
149
|
+
await this.emailQueue.addScheduled(
|
|
150
|
+
SendWelcomeEmailJob,
|
|
151
|
+
sendAt,
|
|
152
|
+
email,
|
|
153
|
+
name
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## 모듈 설정
|
|
160
|
+
|
|
161
|
+
### forRoot (동기)
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
SidequestModule.forRoot({
|
|
165
|
+
// 모듈
|
|
166
|
+
isGlobal: true, // 기본값: true
|
|
167
|
+
|
|
168
|
+
// 데이터베이스 백엔드
|
|
169
|
+
backend: {
|
|
170
|
+
driver: '@sidequest/postgres-backend',
|
|
171
|
+
config: process.env.DATABASE_URL,
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// 큐
|
|
175
|
+
queues: [
|
|
176
|
+
{
|
|
177
|
+
name: 'email',
|
|
178
|
+
concurrency: 5, // 최대 동시 Job 수
|
|
179
|
+
priority: 50, // 기본 우선순위 (높을수록 우선)
|
|
180
|
+
state: 'active', // 'active' | 'paused'
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
|
|
184
|
+
// 엔진 설정
|
|
185
|
+
maxConcurrentJobs: 10, // 전체 최대 동시성
|
|
186
|
+
minThreads: 4, // 최소 워커 스레드 (기본값: CPU 코어 수)
|
|
187
|
+
maxThreads: 8, // 최대 워커 스레드 (기본값: minThreads * 2)
|
|
188
|
+
jobPollingInterval: 100, // Job 폴링 간격 (ms)
|
|
189
|
+
releaseStaleJobsIntervalMin: 60, // 오래된 Job 해제 간격 (분)
|
|
190
|
+
cleanupFinishedJobsIntervalMin: 60, // 완료된 Job 정리 간격 (분)
|
|
191
|
+
|
|
192
|
+
// 로거
|
|
193
|
+
logger: {
|
|
194
|
+
level: 'info',
|
|
195
|
+
json: false, // 프로덕션용 JSON 출력
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// 대시보드 (선택 사항)
|
|
199
|
+
dashboard: {
|
|
200
|
+
enabled: true,
|
|
201
|
+
port: 8678,
|
|
202
|
+
path: '/',
|
|
203
|
+
auth: {
|
|
204
|
+
user: 'admin',
|
|
205
|
+
password: 'password',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// Graceful Shutdown
|
|
210
|
+
gracefulShutdown: {
|
|
211
|
+
enabled: true,
|
|
212
|
+
timeout: 30000, // 30초
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// CLS 통합 (선택 사항)
|
|
216
|
+
enableCls: true, // nestjs-cls 필요
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### forRootAsync (비동기)
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
SidequestModule.forRootAsync({
|
|
224
|
+
imports: [ConfigModule],
|
|
225
|
+
useFactory: (config: ConfigService) => ({
|
|
226
|
+
backend: {
|
|
227
|
+
driver: '@sidequest/postgres-backend',
|
|
228
|
+
config: config.get('DATABASE_URL'),
|
|
229
|
+
},
|
|
230
|
+
queues: [
|
|
231
|
+
{ name: 'email', concurrency: config.get('EMAIL_CONCURRENCY', 5) },
|
|
232
|
+
],
|
|
233
|
+
enableCls: config.get('ENABLE_CLS', false),
|
|
234
|
+
}),
|
|
235
|
+
inject: [ConfigService],
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## 데코레이터
|
|
240
|
+
|
|
241
|
+
### @Processor(queueName, options?)
|
|
242
|
+
|
|
243
|
+
클래스를 지정된 큐의 Job 프로세서로标记합니다.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
@Processor('email', { concurrency: 10 })
|
|
247
|
+
export class EmailProcessor {}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### @OnJob(JobClass, options?)
|
|
251
|
+
|
|
252
|
+
메서드를 지정된 Job 타입의 핸들러로标记합니다.
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
@OnJob(SendEmailJob, { timeout: 30000 })
|
|
256
|
+
async handleEmail(job: SendEmailJob) {
|
|
257
|
+
// ...
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### @Retry(options)
|
|
262
|
+
|
|
263
|
+
Job 핸들러의 재시도 정책을 설정합니다.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
@Retry({
|
|
267
|
+
maxAttempts: 3,
|
|
268
|
+
backoff: {
|
|
269
|
+
type: 'exponential', // 'exponential' | 'fixed'
|
|
270
|
+
delay: 1000, // 초기 지연 시간 (ms)
|
|
271
|
+
multiplier: 2, // 지수 승수
|
|
272
|
+
},
|
|
273
|
+
retryOn: ['NetworkError', 'TimeoutError'], // 이 에러들만 재시도
|
|
274
|
+
})
|
|
275
|
+
async handleJob(job: AnyJob) {
|
|
276
|
+
// ...
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### @InjectQueue(queueName)
|
|
281
|
+
|
|
282
|
+
큐 서비스 인스턴스를 주입합니다.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
constructor(@InjectQueue('email') private emailQueue: IQueueService) {}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### @OnJobComplete(JobClass?)
|
|
289
|
+
|
|
290
|
+
Job이 성공적으로 완료되었을 때 호출되는 핸들러입니다.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
@OnJobComplete(SendEmailJob)
|
|
294
|
+
async onComplete(event: JobCompleteEvent) {
|
|
295
|
+
console.log(`Job ${event.jobId} 완료:`, event.result);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### @OnJobFailed(JobClass?)
|
|
300
|
+
|
|
301
|
+
Job이 실패했을 때 호출되는 핸들러입니다.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
@OnJobFailed(SendEmailJob)
|
|
305
|
+
async onFailed(event: JobFailedEvent) {
|
|
306
|
+
console.error(`Job ${event.jobId} 실패:`, event.error);
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## 큐 서비스 API
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
interface IQueueService {
|
|
314
|
+
readonly name: string;
|
|
315
|
+
|
|
316
|
+
// 단일 Job 추가
|
|
317
|
+
add<T>(JobClass: new (...args: unknown[]) => T, ...args: Parameters<T['constructor']>): Promise<string>;
|
|
318
|
+
|
|
319
|
+
// 옵션과 함께 Job 추가
|
|
320
|
+
addWithOptions<T>(
|
|
321
|
+
JobClass: new (...args: unknown[]) => T,
|
|
322
|
+
options: JobAddOptions,
|
|
323
|
+
...args: Parameters<T['constructor']>
|
|
324
|
+
): Promise<string>;
|
|
325
|
+
|
|
326
|
+
// 예약된 Job 추가
|
|
327
|
+
addScheduled<T>(
|
|
328
|
+
JobClass: new (...args: unknown[]) => T,
|
|
329
|
+
scheduledAt: Date,
|
|
330
|
+
...args: Parameters<T['constructor']>
|
|
331
|
+
): Promise<string>;
|
|
332
|
+
|
|
333
|
+
// 여러 Job 추가 (벌크)
|
|
334
|
+
addBulk<T>(jobs: Array<{
|
|
335
|
+
JobClass: new (...args: unknown[]) => T;
|
|
336
|
+
args: Parameters<T['constructor']>;
|
|
337
|
+
options?: JobAddOptions;
|
|
338
|
+
}>): Promise<string[]>;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### JobAddOptions
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
interface JobAddOptions {
|
|
346
|
+
priority?: number; // 높을수록 먼저 처리 (기본값: 50)
|
|
347
|
+
timeout?: number; // Job 타임아웃 (ms)
|
|
348
|
+
maxAttempts?: number; // 재시도 횟수 재정의
|
|
349
|
+
startAfter?: Date; // 지연된 시작
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## CLS 통합
|
|
354
|
+
|
|
355
|
+
CLS 통합을 활성화하여 Job 실행 간 컨텍스트(traceId, userId 등)를 전파하세요:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// app.module.ts
|
|
359
|
+
SidequestModule.forRoot({
|
|
360
|
+
// ...
|
|
361
|
+
enableCls: true, // nestjs-cls 필요
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
// 컨텍스트가 자동으로 전파됨
|
|
365
|
+
@Processor('email')
|
|
366
|
+
export class EmailProcessor {
|
|
367
|
+
constructor(private readonly cls: ClsService) {}
|
|
368
|
+
|
|
369
|
+
@OnJob(SendEmailJob)
|
|
370
|
+
async handleEmail(job: SendEmailJob) {
|
|
371
|
+
const traceId = this.cls.getId();
|
|
372
|
+
const userId = this.cls.get('userId');
|
|
373
|
+
|
|
374
|
+
console.log(`[${traceId}] 사용자 ${userId}의 Job 처리 중`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## 왜 Sidequest.js인가요?
|
|
380
|
+
|
|
381
|
+
| 기능 | BullMQ + Redis | Sidequest.js |
|
|
382
|
+
|---------|----------------|--------------|
|
|
383
|
+
| 인프라 | 추가 Redis 서버 필요 | 기존 데이터베이스 사용 |
|
|
384
|
+
| 트랜잭션 지원 | 보상 트랜잭션 필요 | 네이티브 DB 트랜잭션 지원 |
|
|
385
|
+
| 운영 비용 | 추가 Redis 인스턴스 비용 | 추가 인프라 불필요 |
|
|
386
|
+
| 배포 단순성 | Redis 클러스터 관리 | 간단한 데이터베이스 연결 |
|
|
387
|
+
|
|
388
|
+
## 라이선스
|
|
389
|
+
|
|
390
|
+
**LGPL v3** - 이 라이브러리는 GNU Lesser General Public License v3.0 하에 라이선스됩니다.
|
|
391
|
+
|
|
392
|
+
의미:
|
|
393
|
+
- 상용 소프트웨어에서 이 라이브러리를 사용해도 소스코드를 공개할 의무가 없습니다
|
|
394
|
+
- 이 라이브러리 자체를 수정한 경우 수정본은 LGPL/GPL로 공개해야 합니다
|
|
395
|
+
- 라이선스 attribution을 제공하고 사용자가 라이브러리를 교체할 수 있도록 해야 합니다
|
|
396
|
+
- 동적 링킹을 권장합니다
|
|
397
|
+
|
|
398
|
+
전체 라이선스 텍스트는 [LICENSE](LICENSE)를 참고하세요.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
이 패키지는 LGPL v3로 라이선스된 [Sidequest.js](https://sidequestjs.com/)를 통합합니다.
|