5.8 KiB
5.8 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend-job-scheduling-temporal | Temporal — Workflow / Activity / Durable | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Temporal
긴 / 복잡 / 신뢰 critical workflow. 코드가 곧 state machine, 자동 재시도 / 영속 / 시간. Saga 의 정답. AWS Step Functions / Airflow 의 코드 버전.
📖 핵심 개념
- Workflow: 결정 로직 (deterministic).
- Activity: 외부 부수효과 (network, DB).
- Signal: 외부에서 workflow 에 이벤트.
- Query: workflow 상태 read.
- Durable: 모든 step history 영속 — server crash 후 재개.
💻 코드 패턴
Activity (idempotent 권장)
// activities/payment.ts
export async function chargeCard(orderId: string): Promise<string> {
const id = await stripe.paymentIntents.create({
amount: 1000, currency: 'usd',
}, { idempotencyKey: orderId });
return id.id;
}
export async function refund(paymentId: string): Promise<void> {
await stripe.refunds.create({ payment_intent: paymentId });
}
export async function reserveInventory(orderId: string): Promise<string> {
return await db.inventory.reserve(orderId);
}
export async function releaseInventory(reservationId: string): Promise<void> {
await db.inventory.release(reservationId);
}
Workflow (deterministic)
// workflows/order.ts
import { proxyActivities, log } from '@temporalio/workflow';
import type * as activities from '../activities';
const { chargeCard, refund, reserveInventory, releaseInventory, ship }
= proxyActivities<typeof activities>({
startToCloseTimeout: '1 minute',
retry: { maximumAttempts: 3 },
});
export async function orderWorkflow(orderId: string): Promise<void> {
let paymentId: string | undefined;
let reservationId: string | undefined;
try {
paymentId = await chargeCard(orderId);
reservationId = await reserveInventory(orderId);
await ship(orderId);
} catch (e) {
log.error('order failed', { orderId, e });
if (reservationId) await releaseInventory(reservationId);
if (paymentId) await refund(paymentId);
throw e;
}
}
Worker (run)
// worker.ts
import { Worker } from '@temporalio/worker';
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities: require('./activities'),
taskQueue: 'orders',
});
await worker.run();
Client (시작)
import { Client } from '@temporalio/client';
const client = new Client();
const handle = await client.workflow.start(orderWorkflow, {
args: ['order-42'],
taskQueue: 'orders',
workflowId: `order-42`, // 멱등 — 같은 ID = 같은 workflow
workflowExecutionTimeout: '1 hour',
});
// 결과 기다림
await handle.result();
Signal (외부 이벤트)
import { defineSignal, setHandler, condition } from '@temporalio/workflow';
export const cancelSignal = defineSignal('cancel');
export async function orderWorkflow(orderId: string) {
let cancelled = false;
setHandler(cancelSignal, () => { cancelled = true; });
// payment 후 사용자가 cancel 가능
paymentId = await chargeCard(orderId);
// 30초 동안 cancel 가능
await condition(() => cancelled, '30 seconds');
if (cancelled) {
await refund(paymentId);
return;
}
await ship(orderId);
}
// 외부에서 signal 보내기
const handle = client.workflow.getHandle(`order-42`);
await handle.signal(cancelSignal);
Query (외부에서 read)
import { defineQuery, setHandler } from '@temporalio/workflow';
export const statusQuery = defineQuery<string>('status');
export async function orderWorkflow(orderId: string) {
let status = 'init';
setHandler(statusQuery, () => status);
status = 'charging';
paymentId = await chargeCard(orderId);
status = 'shipping';
await ship(orderId);
status = 'done';
}
const handle = client.workflow.getHandle(`order-42`);
const s = await handle.query(statusQuery);
Sleep / timer (durable)
import { sleep } from '@temporalio/workflow';
await sleep('7 days'); // 7일 후 재개. server crash 도 OK.
await sendReminderEmail(userId);
Cron workflow
await client.workflow.start(reportWorkflow, {
cronSchedule: '0 9 * * 1', // 월요일 9시
workflowId: 'weekly-report',
taskQueue: 'reports',
});
Versioning (schema 변경)
import { patched } from '@temporalio/workflow';
export async function workflow() {
if (patched('new-step')) {
await newStep(); // 새 logic
}
// 옛 in-flight workflow 도 안전
}
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Saga / 멀티 step | Temporal |
| 시간 의존 (3일 후 알림) | Temporal sleep |
| 큐 / 단순 job | BullMQ / SQS |
| Cron 만 | k8s CronJob |
| 큰 throughput stream | Kafka |
| AWS only | Step Functions |
| Self-host vs Cloud | Temporal Cloud / 자체 |
❌ 안티패턴
- Workflow 안 외부 호출 (fetch / DB): deterministic 깨짐. activity 로.
- Random / Date.now() workflow 안: deterministic 깨짐 — workflowInfo, sleep, random 사용.
- Activity 가 무한 retry: backoff + max attempts.
- Worker 영원 한 task queue: scaling 어려움.
- Signal 무한 받음: 메시지 누적. condition 으로 처리.
- Workflow 너무 큼 (월 단위): continueAsNew 로 reset.
- Activity timeout 없음: hang.
🤖 LLM 활용 힌트
- Workflow = 순수 결정 / Activity = 부수효과.
- workflowId = idempotency key.
- Signal + Query 로 외부 통신.
- Sleep 이 마법 — 7일 후도 OK.