메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
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둜 두고 메인 ν”„λ‘œμ„ΈμŠ€μ—μ„œ 처리:
pnpm dev

ν”„λ‘œλ•μ…˜ ν™˜κ²½

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 섀정을 μ™„λ£Œν–ˆλ‹€λ©΄:
  • workflows - Workflow μ •μ˜ν•˜κΈ°
  • steps - Step으둜 μž‘μ—… 뢄리
  • error-handling - μ—λŸ¬ 처리 및 μž¬μ‹œλ„