---
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<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)
```ts
// 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)
```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<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';
}
```

```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]]
