Sonamuλ λΉλκΈ° λ°±κ·ΈλΌμ΄λ μμ
μ μ²λ¦¬νλ Workflow μμ€ν
μ μ 곡ν©λλ€. κΈ΄ μμ
μ λ°±κ·ΈλΌμ΄λλ‘ λΆλ¦¬νμ¬ API μλ΅ μλλ₯Ό κ°μ νκ³ , Worker νλ‘μΈμ€λ‘ μμ
μ λΆμ° μ²λ¦¬ν μ μμ΅λλ€.
기본 ꡬ쑰
import { defineConfig } from "sonamu";
export default defineConfig({
tasks: {
enableWorker: true,
workerOptions: {
concurrency: 1,
usePubSub: true,
listenDelay: 500,
},
contextProvider: (defaultContext) => ({
...defaultContext,
ip: "127.0.0.1",
session: {},
}),
},
// ...
});
enableWorker
Worker νλ‘μΈμ€λ₯Ό νμ±νν μ§ κ²°μ ν©λλ€.
νμ
: boolean (μ νμ )
κΈ°λ³Έκ°: false
export default defineConfig({
tasks: {
enableWorker: true, // Worker νμ±ν
},
});
Workerλ?
- λ³λ νλ‘μΈμ€μμ μμ
μ μ²λ¦¬
- λ©μΈ API μλ²μ μν₯ μμ
- μ¬λ¬ Workerλ₯Ό μ€ννμ¬ λΆμ° μ²λ¦¬ κ°λ₯
νλ‘λμ
νκ²½μμλ enableWorker: trueλ‘ μ€μ νμ¬ Worker μ μ© νλ‘μΈμ€λ₯Ό μ€ννλ κ²μ
κΆμ₯ν©λλ€.
νκ²½ λ³μλ‘ μ μ΄
export default defineConfig({
tasks: {
enableWorker: !["true", "1"].includes(process.env.DISABLE_WORKER ?? "false"),
},
});
.env:
# Worker λΉνμ±ν (κ°λ°)
DISABLE_WORKER=true
# Worker νμ±ν (νλ‘λμ
)
DISABLE_WORKER=false
workerOptions
Workerμ λμ λ°©μμ μ€μ ν©λλ€.
νμ
: WorkflowOptions (μ νμ )
type WorkflowOptions = {
concurrency?: number;
usePubSub?: boolean;
listenDelay?: number;
};
concurrency
Workerκ° λμμ μ²λ¦¬ν μμ
μμ
λλ€.
νμ
: number
κΈ°λ³Έκ°: 1
export default defineConfig({
tasks: {
workerOptions: {
concurrency: 1, // ν λ²μ 1κ° μμ
λ§
},
},
});
κΆμ₯κ°:
1 - λ¨μν μμ
, μμ°¨ μ²λ¦¬ νμ
2-5 - μΌλ°μ μΈ λ°±κ·ΈλΌμ΄λ μμ
10+ - κ°λ²Όμ΄ I/O μμ£Ό μμ
concurrencyλ₯Ό λμ΄λ©΄ λ λ§μ μμ
μ λμμ μ²λ¦¬νμ§λ§, CPUμ λ©λͺ¨λ¦¬ μ¬μ©λμ΄ μ¦κ°ν©λλ€.
usePubSub
λ€μ€ Worker κ° μμ
λΆμ°μ μν΄ Pub/Subμ μ¬μ©ν©λλ€.
νμ
: boolean
κΈ°λ³Έκ°: true
export default defineConfig({
tasks: {
workerOptions: {
usePubSub: true, // PostgreSQL LISTEN/NOTIFY μ¬μ©
},
},
});
λμ λ°©μ:
false: κ° Workerκ° λ
립μ μΌλ‘ μμ
νλ§
true: PostgreSQL LISTEN/NOTIFYλ‘ μ μμ
μλ¦Όμ μ¦μ μ ν
Pub/Subμ PostgreSQLμ λ΄μ₯ LISTEN/NOTIFY λ©μ»€λμ¦μ μ¬μ©νλ―λ‘, λ³λμ μΈνλΌ(Redis λ±) μ€μΉκ°
νμνμ§ μμ΅λλ€. μ΄λ―Έ ꡬμ±λ λ°μ΄ν°λ² μ΄μ€ μ°κ²°μ κ·Έλλ‘ νμ©ν©λλ€.
listenDelay
μμ
μ νμΈνλ μ£ΌκΈ°(λ°λ¦¬μ΄)μ
λλ€.
νμ
: number (ms)
κΈ°λ³Έκ°: 500 (0.5μ΄)
export default defineConfig({
tasks: {
workerOptions: {
listenDelay: 500, // 0.5μ΄λ§λ€ μμ
νμΈ
},
},
});
κΆμ₯κ°:
100-500ms - λΉ λ₯Έ μλ΅μ΄ νμν μμ
500-1000ms - μΌλ°μ μΈ λ°±κ·ΈλΌμ΄λ μμ
1000-5000ms - κΈ΄κΈνμ§ μμ μμ
contextProvider
Workerμμ μ€νλλ μμ
μ Contextλ₯Ό μμ±ν©λλ€.
νμ
: (defaultContext) => Context | Promise<Context>
export default defineConfig({
tasks: {
contextProvider: (defaultContext) => {
return {
...defaultContext,
ip: "127.0.0.1",
session: {},
};
},
},
});
defaultContext ν¬ν¨ νλ:
reply - Fastify reply κ°μ²΄ (null in worker)
request - Fastify request κ°μ²΄ (null in worker)
headers - μμ² ν€λ
createSSE - SSE μ€νΈλ¦Ό μμ±
naiteStore - Naite λ‘κΉ
μ€ν μ΄
컀μ€ν
Context
export default defineConfig({
tasks: {
contextProvider: (defaultContext) => {
return {
...defaultContext,
ip: "worker",
session: {},
config: {
apiUrl: process.env.API_URL,
},
logger: console,
};
},
},
});
Worker Contextμμλ requestμ replyκ° nullμ
λλ€. HTTP κ΄λ ¨ κΈ°λ₯μ μ¬μ©ν μ μμ΅λλ€.
κΈ°λ³Έ μμ
λ¨μΌ Worker (κ°λ°)
import { defineConfig } from "sonamu";
export default defineConfig({
tasks: {
enableWorker: process.env.NODE_ENV === "production",
workerOptions: {
concurrency: 1,
usePubSub: false, // λ¨μΌ Worker
listenDelay: 500,
},
contextProvider: (defaultContext) => ({
...defaultContext,
ip: "127.0.0.1",
session: {},
}),
},
});
λ€μ€ Worker (νλ‘λμ
)
import { defineConfig } from "sonamu";
export default defineConfig({
tasks: {
enableWorker: true,
workerOptions: {
concurrency: 5,
usePubSub: true, // PostgreSQL LISTEN/NOTIFYλ‘ λΆμ°
listenDelay: 500,
},
contextProvider: (defaultContext) => ({
...defaultContext,
ip: "worker",
session: {},
}),
},
});
μ€μ μμ
νκ²½λ³ μ€μ
import { defineConfig } from "sonamu";
const isDev = process.env.NODE_ENV === "development";
const isProd = process.env.NODE_ENV === "production";
export default defineConfig({
tasks: {
// κ°λ°: Worker μμ (λ©μΈ νλ‘μΈμ€μμ μ²λ¦¬)
// νλ‘λμ
: Worker νμ±ν
enableWorker: isProd,
workerOptions: {
concurrency: isProd ? 5 : 1,
usePubSub: isProd,
listenDelay: isDev ? 100 : 500, // κ°λ°: λΉ λ₯Έ μλ΅
},
contextProvider: (defaultContext) => ({
...defaultContext,
ip: isProd ? "worker" : "127.0.0.1",
session: {},
}),
},
});
κ³ λΆν μν¬νλ‘μ°
export default defineConfig({
tasks: {
enableWorker: true,
workerOptions: {
concurrency: 10, // λμμ 10κ° μμ
usePubSub: true, // λ€μ€ Worker μ§μ
listenDelay: 1000, // 1μ΄ κ°κ²©
},
contextProvider: (defaultContext) => ({
...defaultContext,
ip: "worker",
session: {},
logger: {
info: (msg) => console.log(`[Worker] ${msg}`),
error: (msg) => console.error(`[Worker Error] ${msg}`),
},
}),
},
});
컀μ€ν
Context
export default defineConfig({
tasks: {
enableWorker: true,
workerOptions: {
concurrency: 3,
usePubSub: true,
listenDelay: 500,
},
contextProvider: async (defaultContext) => {
// λΉλκΈ° μ΄κΈ°ν
const config = await loadConfig();
return {
...defaultContext,
ip: "worker",
session: {},
config,
services: {
email: new EmailService(),
notification: new NotificationService(),
},
};
},
},
});
Workflow μ¬μ©
tasks μ€μ ν workflow() ν¨μλ‘ λ°±κ·ΈλΌμ΄λ μμ
μ μ μν©λλ€.
import { workflow } from "sonamu";
export const sendWelcomeEmail = workflow(
{ name: "send_welcome_email" },
async ({ input, step }) => {
const user = await step
.define({ name: "fetch_user" }, async () => {
return await UserModel.findById(input.userId);
})
.run();
await step
.define({ name: "send_email" }, async () => {
await sendEmail(user.email, "Welcome!");
})
.run();
},
);
νΈμΆ:
import { Sonamu } from "sonamu";
// λ°±κ·ΈλΌμ΄λμμ μ€ν
await Sonamu.workflows.run({ name: "send_welcome_email", version: null }, { userId: 123 });
β Workflow μ¬μ©λ²
Worker μ€ν
κ°λ° νκ²½
κ°λ° νκ²½μμλ enableWorker: falseλ‘ λκ³ λ©μΈ νλ‘μΈμ€μμ μ²λ¦¬:
νλ‘λμ
νκ²½
Worker μ μ© νλ‘μΈμ€λ₯Ό λ³λλ‘ μ€ν:
# API μλ²
pnpm start
# Worker νλ‘μΈμ€ (λ³λ ν°λ―Έλ)
pnpm start:worker
package.json:
{
"scripts": {
"start": "node dist/server.js",
"start:worker": "ENABLE_WORKER=true node dist/worker.js"
}
}
λ€μ€ Worker
μ¬λ¬ Workerλ₯Ό μ€ννμ¬ μ²λ¦¬λ μ¦κ°:
# Worker 1
ENABLE_WORKER=true node dist/worker.js
# Worker 2
ENABLE_WORKER=true node dist/worker.js
# Worker 3
ENABLE_WORKER=true node dist/worker.js
PM2λ Dockerλ₯Ό μ¬μ©νλ©΄ μ¬λ¬ Workerλ₯Ό μ½κ² κ΄λ¦¬ν μ μμ΅λλ€.
Pub/Sub λμ λ°©μ
usePubSub: true(κΈ°λ³Έκ°)μΌ λ, Workerλ PostgreSQLμ λ΄μ₯ LISTEN/NOTIFY λ©μ»€λμ¦μ μ¬μ©νμ¬ μ μμ
μλ¦Όμ μμ ν©λλ€. λ³λμ λ©μμ§ λΈλ‘컀(Redis λ±)κ° νμνμ§ μμΌλ©°, μ΄λ―Έ ꡬμ±λ λ°μ΄ν°λ² μ΄μ€ μ°κ²°μ κ·Έλλ‘ νμ©ν©λλ€.
export default defineConfig({
tasks: {
enableWorker: true,
workerOptions: {
usePubSub: true, // PostgreSQL LISTEN/NOTIFY μ¬μ© (κΈ°λ³Έκ°)
},
},
});
μ£Όμμ¬ν
1. Worker Context μ ν
// β λμ μ: Workerμμ HTTP μλ΅ μλ
export const processData = workflow({ name: "process_data" }, async () => {
const ctx = Sonamu.getContext();
ctx.reply.send({ done: true }); // Workerμμλ null!
});
// β
μ’μ μ: κ²°κ³Όλ₯Ό DBμ μ μ₯
export const processData = workflow({ name: "process_data" }, async () => {
const result = await heavyProcessing();
await ResultModel.save({ data: result });
});
2. enableWorker μ€μ
// β λμ μ: νλ‘λμ
μμ Worker μμ
enableWorker: false; // λͺ¨λ μμ
μ΄ λ©μΈ νλ‘μΈμ€μμ!
// β
μ’μ μ: νκ²½λ³λ‘ λ€λ₯΄κ²
enableWorker: process.env.NODE_ENV === "production";
3. concurrency κ³Όλ€ μ€μ
// β λμ μ: λ무 λμ λμμ±
workerOptions: {
concurrency: 100, // CPUμ λ©λͺ¨λ¦¬ λΆμ‘± μν
}
// β
μ’μ μ: μ μ ν μμ€
workerOptions: {
concurrency: 5, // μλ² λ¦¬μμ€μ λ§κ²
}
λ€μ λ¨κ³
Task μ€μ μ μλ£νλ€λ©΄: