--- id: backend-job-scheduling-temporal title: Temporal — Workflow / Activity / Durable category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [backend, temporal, workflow, durable, vibe-coding] tech_stack: { language: "TS / Temporal", applicable_to: ["Backend"] } applied_in: [] aliases: [Temporal, Cadence, durable execution, workflow, activity, signal, query] --- # 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 권장) ```ts // activities/payment.ts export async function chargeCard(orderId: string): Promise { const id = await stripe.paymentIntents.create({ amount: 1000, currency: 'usd', }, { idempotencyKey: orderId }); return id.id; } export async function refund(paymentId: string): Promise { await stripe.refunds.create({ payment_intent: paymentId }); } export async function reserveInventory(orderId: string): Promise { return await db.inventory.reserve(orderId); } export async function releaseInventory(reservationId: string): Promise { await db.inventory.release(reservationId); } ``` ### Workflow (deterministic) ```ts // workflows/order.ts import { proxyActivities, log } from '@temporalio/workflow'; import type * as activities from '../activities'; const { chargeCard, refund, reserveInventory, releaseInventory, ship } = proxyActivities({ startToCloseTimeout: '1 minute', retry: { maximumAttempts: 3 }, }); export async function orderWorkflow(orderId: string): Promise { 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) ```ts // 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 (시작) ```ts 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 (외부 이벤트) ```ts 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); } ``` ```ts // 외부에서 signal 보내기 const handle = client.workflow.getHandle(`order-42`); await handle.signal(cancelSignal); ``` ### Query (외부에서 read) ```ts import { defineQuery, setHandler } from '@temporalio/workflow'; export const statusQuery = defineQuery('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'; } ``` ```ts const handle = client.workflow.getHandle(`order-42`); const s = await handle.query(statusQuery); ``` ### Sleep / timer (durable) ```ts import { sleep } from '@temporalio/workflow'; await sleep('7 days'); // 7일 후 재개. server crash 도 OK. await sendReminderEmail(userId); ``` ### Cron workflow ```ts await client.workflow.start(reportWorkflow, { cronSchedule: '0 9 * * 1', // 월요일 9시 workflowId: 'weekly-report', taskQueue: 'reports', }); ``` ### Versioning (schema 변경) ```ts 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. ## 🔗 관련 문서 - [[Backend_Saga_Patterns]] - [[Backend_Cron_Patterns]] - [[Backend_Idempotency_Keys]]