Temporal 1.2x+ íµì¬ ìì ê°ìŽë: Temporal CLI ë° Docker ë¡ì»¬ ê°ë° í겜 1ìŽ êµ¬ì¶, Workflow vs Activity ë©í ëªšëž ë° Event Sourcing ìí€í
ì², TypeScript/Node.js ìŠì ì€í ìœë(Hello Worldë¶í° ë¶ì° ížëìì
Saga íšíŽ, Human-in-the-Loop ì¹ìž, Signals & Queries, ëŽêµ¬ì± íìŽëšž), ê²°ì ë¡ ì (Determinism) ì ìœ ê·ì¹ ë° íë¡ëì
ìŽì 첎í¬ëЬì€íž
## 1. íì ì¬ì ì€ì¹ ì걎 & í겜 êµ¬ì± (Prerequisites)
### ð» ìì€í
ì€ì¹ ë° ì€í ì구ì¬ì (System Requirements)
| í목 | ìµì ì¬ì (Minimum) | ê¶ì¥ ì¬ì (Recommended) | ì°žê³ ë° ë¹ê³ |
| :--- | :--- | :--- | :--- |
| **ìŽì첎ì (OS)** | Linux (몚ë ë°°í¬í), macOS (Apple Silicon / Intel), Windows (WSL2 ëë ë€ìŽí°ëž CLI) | Ubuntu 22.04 LTS / macOS Sonoma / Windows 11 WSL2 | ë€ìŽí°ëž ëšìŒ ë°ìŽë늬 ëë Docker 컚í
ìŽë ì§ì |
| **CPU** | 2 ìœìŽ ìŽì (ë¡ì»¬ ê°ë° í겜 êž°ì€) | 4 ìœìŽ ~ 8 ìœìŽ ìŽì (íë¡ëì
ì컀 ë° íŽë¬ì€í°) | Workflow Replay ë° gRPC ëì ìì
ì²ëЬ ì±ë¥ ì¢ì° |
| **ë©ëªšëЬ (RAM)** | 2 GB ìŽì (ë¡ì»¬ ê°ë° ìë² êµ¬ë ì) | 4 GB ~ 8 GB ìŽì (PostgreSQL ììí ë° ì¹ UI í¬íš) | íë¡ëì
Temporal Clusterë ë
žëë¹ 8GB~16GB ê¶ì¥ |
| **ëì€í¬ (Storage)** | 1 GB ìŽìì ì¬ì ê³µê° | 10 GB ~ 50 GB ìŽìì ê³ ì NVMe SSD | SQLite(ê°ë°) ëë PostgreSQL/Cassandra(ìŽë²€íž íì€í 늬 볎êŽ) |
| **ë€ížìí¬ / ë°íì** | Node.js 18+ (LTS), Python 3.10+, Go 1.21+, Java 17+ | Node.js 20+ / TypeScript 5+, Go 1.22+ | í¬íž 7233 (Temporal Server gRPC), 8233 (Temporal Web UI) |
---
### â³ TemporalìŽë?
**Temporal**ì ë¶ì° ìì€í
ìì ë°ìíë ë€ížìí¬ ëšì , ìë² ë€ìŽ, ìëíí° API ì€ë¥ ìììë **'ì ë ì€íšíì§ ìê³ ìœëê° ìì±ë ììëë¡ ëê¹ì§ ì죌'**íëë¡ ë³Žì¥íë **ì€íìì€ ìœë êž°ë° ìí¬íë¡ì° ì€ìŒì€ížë ìŽì
ìì§(Durable Execution Engine)**ì
ëë€.
- **Durable Execution (ëŽêµ¬ì± ìë ì€í)**: ì컀 íë¡ìžì€ê° ê°ì ì¢
ë£ëê±°ë ìë² ì ììŽ êºŒì žë, ì¬ë¶í
ì ì íí ì€ëšëìë ìœë ëŒìž(Line)ìì ìí륌 ê·žëë¡ ë³µìíì¬ ê³ì ì€íí©ëë€.
- **ìœë êž°ë° ì€ìŒì€ížë ìŽì
**: ë³µì¡í YAMLìŽë JSON, Drag & Drop GUIê° ìë ìŒë° íë¡ê·žëë° ìžìŽ(TypeScript, Python, Go, Java)ì ìì ìœë(`async/await`, `try/catch`, `sleep()`)ë¡ ë¹ìŠëì€ íìŽíëŒìžì ìì±í©ëë€.
- **ìë ì¥ì 복구 & 묎í ì¬ìë**: ìŒìì ìž ë€ížìí¬ ì€ë¥ë Activity ì¬ìë ì ì±
(`RetryPolicy`)ìŒë¡ ìë 복구ëë©°, ë¶ì° ížëìì
례백ì Saga íšíŽìŒë¡ ì§êŽì ìŽê² ì²ëЬí©ëë€.
---
### ð 1. Temporal CLIë¡ 1ìŽ ë§ì ë¡ì»¬ ê°ë° ìë² ì€ííêž° (ê°ì¥ ì¶ì²)
Temporal ê³µì CLIë ì첎 ëŽì¥ SQLite ì€í 늬ì§ì ì¹ UI(Web Dashboard)륌 í¬íšíê³ ììŽ, ìžë¶ ë°ìŽí°ë² ìŽì€ ì€ì¹ ììŽ ëª
ë ¹ìŽ í ì€ë¡ ìŠì ë¡ì»¬ ê°ë° íŽë¬ì€í°ë¥Œ ëìž ì ììµëë€.
#### â CLI ì€ì¹
```bash
# macOS (Homebrew)
brew install temporal
# Linux / WSL (ê³µì ì€ì¹ ì€í¬ëŠœíž)
curl -sSf https://temporal.download/cli.sh | sh
# Windows (Scoop ëë Winget)
scoop install temporal
# ëë winget install TemporalTechnologies.temporal
```
#### â¡ ë¡ì»¬ ê°ë° ìë² ìì (ëŽì¥ ì¹ ëì볎ë í¬íš)
```bash
# ë¡ì»¬ ê°ë° ìë² ì€í (SQLite êž°ë° ë°ìŽí° ì ì§, Web UI ìë 구ë)
temporal server start-dev
```
ìë²ê° 구ëë멎 ë€ì ë ìëí¬ìžížê° ìŠì íì±íë©ëë€:
- **`localhost:7233`**: íŽëŒìŽìžíž ë° ìì»€ê° ì°ê²°íë gRPC ìë² ìëí¬ìžíž
- **`http://localhost:8233`**: ì€í ì€ìž ìí¬íë¡ì°, ìí, ìŽë²€íž íì€í 늬륌 ìê°ì ìŒë¡ íìžíë **Temporal Web UI**
#### â
CLI ì€í ìí íìž
ë€ë¥ž í°ë¯žë ì°œì ìŽê³ íŽë¬ì€í° ìí륌 íìží©ëë€:
```bash
temporal operator cluster health
# ì¶ë ¥ ìì: {"status": "SERVING"}
```
---
### ð³ 2. Docker Composeë¡ ìì í Temporal íŽë¬ì€í° 구ë (ì í)
íë¡ëì
곌 ëìŒíê² PostgreSQL ìì ë°ìŽí°ë² ìŽì€ì ë
늜ì ìž Frontend/History/Matching ìë¹ì€ë¥Œ ì€ííê³ ì¶ë€ë©Ž ê³µì Docker Compose ì ì¥ì륌 íì©í©ëë€:
```bash
# ê³µì temporalio/docker-compose ì ì¥ì ë³µì í 구ë
git clone https://github.com/temporalio/docker-compose.git
cd docker-compose
docker compose up -d
```
---
## 2. Temporal íµì¬ ìí€í
ì² & ë©í ëªšëž (Core Concepts)
Temporalì ì ëë¡ ìŽíŽíêž° ìí 4ê°ì§ íµì¬ êµ¬ì± ììì ìŽë²€íž ìì±(Event Sourcing) êž°ë° ë©í 몚ëžì
ëë€.
### ð Temporal ìí€í
ì² íëŠë (ASCII Diagram)
```text
âââââââââââââââââââ gRPC (7233) ââââââââââââââââââââââââââââââââââââââââ
â Client App â ââââââââââââââââââââââââââââââ> â Temporal Cluster â
â (API Server ë±) â workflow.start("OrderFlow") â ââââââââââââââââââââââââââââââââââââ â
âââââââââââââââââââ â â Frontend Service (gRPC ê²ìŽížìšìŽ)â â
â âââââââââââââââââââââââââââââââââââ†â
â â Matching Service (ìì
í ì€ê°) â â
â âââââââââââââââââââââââââââââââââââ†â
â â History Service (ìí ëšžì ìì§) â â
â ââââââââââââââââââ¬ââââââââââââââââââ â
â â Event Sourcing â
â ⌠â
â [(PostgreSQL / SQLite)] â
âââââââââââââââââââââ¬âââââââââââââââââââ
â
Task Queue Polling â Task Queue
("order-task-queue") â (ìì
ë¶ë°° í)
âŒ
ââââââââââââââââââââââââââââââââââââââââ
â Worker Process â
â ââââââââââââââââââââââââââââââââââ â
â â Workflow Worker (ì€ìŒì€ížë ìŽì
) â â
â â - Deterministic ìì ë¡ì§ â â
â âââââââââââââââââââââââââââââââââ†â
â â Activity Worker (ì€ì ìì
ì€í)â â
â â - ìžë¶ ê²°ì API ížì¶, DB I/O â â
â ââââââââââââââââââââââââââââââââââ â
ââââââââââââââââââââââââââââââââââââââââ
```
---
### âïž Workflow vs Activity ë¹êµ (ê°ì¥ ì€ìí ì°šìŽ)
| êµ¬ë¶ | ìí¬íë¡ì° (Workflow) | ì¡í°ë¹í° (Activity) |
| :--- | :--- | :--- |
| **ìí ** | ìì
ìì ì§í ë° ìí ì¡°ìš (ì€ìŒì€ížë ìŽí°) | ì€ì ìžë¶ I/O, ì°ì°, ë¶ìì©(Side Effect) ìí |
| **ìœë ì ìœ** | **ë°ëì ê²°ì ë¡ ì (Deterministic)**ìŽìŽìŒ íš | ìŒë°ì ìž ëªšë ìœë íì© (HTTP, DB, 묎ììê° ë±) |
| **íì© ìì
** | `sleep()`, 조걎 ë¶êž°, Signal/Query ì²ëЬ, Activity ížì¶ | Stripe ê²°ì , ë©ìŒ ë°ì¡, DB CRUD, AI ëªšëž ì¶ë¡ |
| **êžì§ ìì
** | `Date.now()`, `Math.random()`, ì§ì ë€ížìí¬ ížì¶, ì ì ë³ì | ìì |
| **ì¥ì ì 복구** | ìŽë²€íž íì€í 늬륌 ì²ìë¶í° ë¹ ë¥Žê² ì¬ì€í(**Replay**)íì¬ ìí ë³µì | ì€íš ì ì§ì ë `RetryPolicy`ì ë°ëŒ ìë ì¬ìë |
| **ì€í 죌첎** | Workflow Worker (ê°ì ì€ë ë/ìŽë²€íž 룚í) | Activity Worker |
> [!IMPORTANT]
> **ì Workflowë ê²°ì ë¡ ì (Deterministic)ìŽìŽìŒ í ê¹ì?**
> Temporal ìë²ë ìí¬íë¡ì°ì ë©ëªšëЬ ë€í륌 ì ì¥íì§ ììµëë€. ëì **ì€íë ìŽë²€íž 목ë¡(Event History)**ë§ì DBì ììíí©ëë€. ì컀 ìë²ê° ë€ìŽëìë€ê° ìŽìë멎, 곌거ì ìŽë²€ížë¥Œ ê·žëë¡ ì¬ì(**Replay**)íì¬ ë¹ìì ìœë ì€í ìì¹ì ì§ì ë³ì ê°ì 100% ëìŒíê² ì¬êµ¬ì±í©ëë€. ë§ìœ Workflow ìì `Math.random()`ìŽë `Date.now()`ê° ìë€ë©Ž Replay ì 곌거ì ë€ë¥ž 겜ë¡ë¡ ë¶êž°ëìŽ **`NonDeterministicWorkflowError`**ê° ë°ìí©ëë€.
---
## 3. [ìŽë³Žì 1ëšê³] í¬ë¡ìë / ìµì ëì ìì (Quickstart)
Node.js + TypeScript í겜ìì ëš 5ë¶ ë§ì ëìíë ìì í Temporal ìí¬íë¡ì°ë¥Œ ìì±í©ëë€.
### ðŠ 1. ì€ìµ ëë í 늬 ë° ìì¡Žì± ì€ì¹
```bash
mkdir temporal-starter && cd temporal-starter
npm init -y
npm install @temporalio/workflow @temporalio/activity @temporalio/worker @temporalio/client
npm install -D typescript tsx @types/node
npx tsc --init
```
---
### ð ïž 2. ì¡í°ë¹í° ì ì (`src/activities.ts`)
ìžë¶ API ížì¶ìŽë ë¹ìŠëì€ ë¡ì§ì ìííë íšìì
ëë€.
```typescript
// src/activities.ts
export async function greetUser(name: string): Promise<string> {
console.log(`[Activity] ì¬ì©ì íì ë©ìì§ ìì± ì€: ${name}`);
// ìžë¶ API ížì¶ìŽë DB ì¡°í륌 ì뮬ë ìŽì
await new Promise((resolve) => setTimeout(resolve, 500));
return `ìë
íìžì, ${name}ë! Temporalì ëŽêµ¬ì± ìë ì€íì ìžê³ì ì€ì ê²ì íìí©ëë€.`;
}
```
---
### ð 3. ìí¬íë¡ì° ì ì (`src/workflows.ts`)
ì¡í°ë¹í°ë¥Œ ížì¶íê³ ìì륌 ì ìŽíë ì€ìŒì€ížë ìŽì
ìœëì
ëë€.
```typescript
// src/workflows.ts
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
// ì¡í°ë¹í° íë¡ì ìì± (íììì ì€ì íì)
const { greetUser } = proxyActivities<typeof activities>({
startToCloseTimeout: '10 seconds', // ì¡í°ë¹í° 1í ìµë ì€í íì© ìê°
});
export async function exampleWorkflow(userName: string): Promise<string> {
console.log(`[Workflow ìì] ëì: ${userName}`);
// ì¡í°ë¹í° ížì¶ (ì€íš ì Ʞ볞 ì¬ìë ì ì±
ì ìíŽ ìë ì¬ìëëš)
const greeting = await greetUser(userName);
console.log(`[Workflow ìë£] 결곌: ${greeting}`);
return greeting;
}
```
---
### âïž 4. ì컀 íë¡ìžì€ 구ë (`src/worker.ts`)
ìì
í(Task Queue)륌 ê°ìíê³ ì€ì ìí¬íë¡ì° ë° ì¡í°ë¹í°ë¥Œ ì€ííë 백귞ëŒìŽë íë¡ìžì€ì
ëë€.
```typescript
// src/worker.ts
import { Worker } from '@temporalio/worker';
import * as activities from './activities';
async function run() {
// ì컀 ìžì€íŽì€ ìì± ë° 'hello-world-queue' ìì
í 구ë
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: 'hello-world-queue',
});
console.log('ð· [Temporal Worker] ìì
í ìì ëêž° ì€... (Task Queue: hello-world-queue)');
await worker.run();
}
run().catch((err) => {
console.error('ì컀 구ë ì€íš:', err);
process.exit(1);
});
```
---
### ð 5. ìí¬íë¡ì° ì€í íŽëŒìŽìžíž (`src/client.ts`)
íŽë¬ì€í°ì ì°ê²°íì¬ ìí¬íë¡ì° ì€íì ìì²íê³ ê²°ê³Œë¥Œ êž°ë€ëЬë íŽëŒìŽìžíž ìœëì
ëë€.
```typescript
// src/client.ts
import { Connection, Client } from '@temporalio/client';
import { exampleWorkflow } from './workflows';
async function main() {
// 1. Temporal gRPC ìë² ì°ê²° (Ʞ볞 localhost:7233)
const connection = await Connection.connect({ address: 'localhost:7233' });
const client = new Client({ connection });
const workflowId = `greeting-wf-${Date.now()}`;
console.log(`ð [Client] ìí¬íë¡ì° ìì ìì²: ID=${workflowId}`);
// 2. ìí¬íë¡ì° ìì ë° ê²°ê³Œ ëêž°
const handle = await client.workflow.start(exampleWorkflow, {
taskQueue: 'hello-world-queue',
args: ['íêžžë'],
workflowId: workflowId,
});
console.log(`â³ ì€í ì€... Workflow Run ID: ${handle.firstExecutionRunId}`);
// 3. ìµì¢
ìë£ ê²°ê³Œ ìì
const result = await handle.result();
console.log(`ð [Client ìµì¢
ìë£ ê²°ê³Œ]:\n${result}`);
}
main().catch((err) => {
console.error('íŽëŒìŽìžíž ì€í ì€ë¥:', err);
process.exit(1);
});
```
---
### ð» 6. ì€í ë° ê²ìŠ (Run & Verify)
í°ë¯žëì 2ê° ìŽìŽ ê°ê° ì컀ì íŽëŒìŽìžížë¥Œ ìì°šì ìŒë¡ ì€íí©ëë€:
```bash
# [í°ë¯žë 1] ì컀 íë¡ìžì€ ì€í
npx tsx src/worker.ts
```
```bash
# [í°ë¯žë 2] íŽëŒìŽìžíž ì€í
npx tsx src/client.ts
```
#### â
í°ë¯žë 2 ìì ì¶ë ¥ 결곌:
```text
ð [Client] ìí¬íë¡ì° ìì ìì²: ID=greeting-wf-1760070000000
â³ ì€í ì€... Workflow Run ID: a82c1b92-4f9e-4e8c-8f4b-123456789abc
ð [Client ìµì¢
ìë£ ê²°ê³Œ]:
ìë
íìžì, íêžžëë! Temporalì ëŽêµ¬ì± ìë ì€íì ìžê³ì ì€ì ê²ì íìí©ëë€.
```
#### ð Web UIìì ìê°ì íìž
ì¹ ëžëŒì°ì ìì `http://localhost:8233`ì ì ìí멎, ë°©êž ì€íë `greeting-wf-...`ì **Status(Completed)**, **Execution Duration**, ê·žëŠ¬ê³ íŽëŠ ì ëíëë ëšê³ë³ **Event History(WorkflowTaskStarted, ActivityTaskScheduled, ActivityTaskCompleted)**륌 íëì 볌 ì ììµëë€.
---
## 4. [ìŽë³Žì 2ëšê³] ì¡í°ë¹í° ì ìŽ: ì¬ìë, 4ë íììì, íížë¹íž
ìžë¶ APIë ìžì ë ì€íší ì ììµëë€. Temporalì ì§ê°ë ê°ë ¥í ì¬ìë ë° íììì ì ìŽìì ëìµëë€.
### â±ïž Temporalì 4ë ì¡í°ë¹í° íììì
| íììì ì¢
ë¥ | ì€ëª
| ê¶ì¥ ì€ì ê°ìŽë |
| :--- | :--- | :--- |
| **`startToCloseTimeout`** | ìì»€ê° ì¡í°ë¹í° ì€íì ììí í ì¢
ë£ë ëê¹ì§ì **1í ìµë ì€í ìê°** | **(íì ê¶ì¥)** ê°ë³ API ížì¶ ìì ìê°ì 2~3ë°° (ì: 5ìŽ, 30ìŽ) |
| **`scheduleToCloseTimeout`** | ìµìŽ í ìžì
ë¶í° **몚ë ì¬ìë륌 ê±°ì³ ìµì¢
ìë£ë ëê¹ì§ì ì 첎 ìí ìê°** | ì 첎 ë¹ìŠëì€ íì© ë§ê° ìê° (ì: 10ë¶, 1ìê°) |
| **`scheduleToStartTimeout`** | ì¡í°ë¹í°ê° Task Queueì ë€ìŽê° í ìì»€ê° ì§ìŽê° ëê¹ì§ì ìµë ëêž° ìê° | ì컀 ë€ìŽ ê°ì§ì© (ë³Žíµ ë¯žì§ì ê¶ì¥, Ʞ볞 묎ì í) |
| **`heartbeatTimeout`** | ì¥êž° ì€í ì¡í°ë¹í°ìì ìì»€ê° "ë ìì§ ìŽììì" ì ížë¥Œ 볎ëŽìŒ íë ìµë 죌Ʞ | 30ìŽ ìŽìì ꞎ ìì
ì íì ì§ì (ì: 10ìŽ) |
---
### ð ì¬ìë ì ì±
(`RetryPolicy`) ì€ì ìì
```typescript
// src/workflows.ts ëŽë¶
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { processPayment } = proxyActivities<typeof activities>({
startToCloseTimeout: '15 seconds',
retry: {
initialInterval: '1 second', // ìµìŽ ì¬ìë ëêž° ìê°
backoffCoefficient: 2, // ì§ì ë°±ì€í ë°°ì (1s -> 2s -> 4s -> 8s)
maximumInterval: '30 seconds', // ì¬ìë ê°ê²© ìµë ìí
maximumAttempts: 5, // ìµë ì¬ìë íì (0 ëë 믞ì§ì ì 묎í ì¬ìë)
nonRetryableErrorTypes: ['InvalidCardError', 'CustomerNotFoundError'], // ì¬ìë ììŽ ìŠì ì€íšìí¬ ìë¬
},
});
```
---
### ð ì¥êž° ì€í ì¡í°ë¹í°ì íížë¹íž (Heartbeat)
ìì ë¶ ìŽì 걞늬ë ëì©ë íìŒ ë€ìŽë¡ë, ìì ìžìœë©, AI ëªšëž ì¶ë¡ ì¡í°ë¹í°ë 죌Ʞì ìŒë¡ íížë¹ížë¥Œ ì ì¡íì¬ ì컀 ì¥ì 륌 ìŠê° ê°ì§íê³ , ì¬ìë ì ì§íë륌 ìŽìŽì ìíí ì ììµëë€.
```typescript
// src/activities.ts
import { Context } from '@temporalio/activity';
export async function processBatchData(totalChunks: number): Promise<void> {
const activityContext = Context.current();
// ìŽì ì€íš ì§ì ì 첎í¬í¬ìžíž íìž
let startChunk = 0;
if (activityContext.info.heartbeatDetails) {
startChunk = activityContext.info.heartbeatDetails as number;
console.log(`[Activity] ìŽì ì¥ì ì§ì (${startChunk}ë² ì²í¬)ë¶í° ìì
ì ì¬ê°í©ëë€.`);
}
for (let i = startChunk; i < totalChunks; i++) {
await doHeavyComputation(i);
// íížë¹íž ì ì¡ (íì¬ ì§í ìí륌 ììž ë°ìŽí°ë¡ íšê» ì ì¡)
activityContext.heartbeat(i);
}
}
async function doHeavyComputation(chunkIndex: number): Promise<void> {
await new Promise((res) => setTimeout(res, 1000));
}
```
---
## 5. [ìŽë³Žì 3ëšê³] ë¹ëêž° ìížìì©: Signals, Queries, Updates & íìŽëšž
Temporal ìí¬íë¡ì°ë ë©°ì¹ , ëª ë¬ ëì 백귞ëŒìŽëìì ìŽììì ì ììµëë€. ìžë¶ìì ëì ìíµì ìíŽ **Signal**, **Query**, **Update**륌 ì ê³µí©ëë€.
### ð 3ë ìížìì© ë©ì»€ëìŠ ë¹êµ
```text
ââââââââââââââââ 1. Signal (ë¹ëêž° ìŽë²€íž ì ë¬: "ì¹ìž ìë£ëš", Fire-and-Forget)
â â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ> ââââââââââââââââ
â External â 2. Query (ëêž° ìí ì¡°í: "íì¬ ì§íë¥ ìŽ ëª %ìžê°?", Read-Only)â Running â
â Client / UI â <âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â Workflow â
â â 3. Update (ëêž° ìí ë³ê²œ + ìŠì 결곌 ë°í: "ììŽí
ì¶ê°íê³ ì ìŽì¡ ë°í")â â
ââââââââââââââââ <âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ> ââââââââââââââââ
```
---
### ð» Signal곌 Query륌 íì©í 죌묞 ì²ëЬ ìí¬íë¡ì° ìì
```typescript
// src/orderWorkflow.ts
import { defineSignal, defineQuery, setHandler, condition, sleep } from '@temporalio/workflow';
// 1. Signal ë° Query ìê·žëì² ì ì
export const approvePaymentSignal = defineSignal<[string]>('approvePayment');
export const cancelOrderSignal = defineSignal<[string]>('cancelOrder');
export const getOrderStatusQuery = defineQuery<string>('getOrderStatus');
export async function orderFulfillmentWorkflow(orderId: string): Promise<string> {
let status = 'WAITING_FOR_PAYMENT';
let paymentApproved = false;
let orderCancelled = false;
let cancellationReason = '';
// 2. Query ížë€ë¬ ë±ë¡ (ìžë¶ìì íì¬ ìí ì€ìê° ì¡°í ê°ë¥)
setHandler(getOrderStatusQuery, () => status);
// 3. Signal ížë€ë¬ ë±ë¡ (ìžë¶ ê²°ì ìë£ ì늌 ìì )
setHandler(approvePaymentSignal, (transactionId) => {
paymentApproved = true;
status = `PAID_TX_${transactionId}`;
});
setHandler(cancelOrderSignal, (reason) => {
orderCancelled = true;
cancellationReason = reason;
status = 'CANCELLED';
});
console.log(`[Order ${orderId}] ê²°ì ì¹ìž ì ížë¥Œ êž°ë€ëŠœëë€ (ìµë 1ìŒ ëêž°)...`);
// 4. condition: ì¡°ê±ŽìŽ ì°žìŽ ëê±°ë íììì(1ìŒ)ìŽ ì§ë ëê¹ì§ ëŽêµ¬ì± ìê² ëêž°
const receivedInTime = await condition(() => paymentApproved || orderCancelled, '1 day');
if (!receivedInTime) {
status = 'EXPIRED';
return `죌묞 ë²íž ${orderId}: ê²°ì ìê° ìŽê³Œë¡ ìë ì·šìëììµëë€.`;
}
if (orderCancelled) {
return `죌묞 ë²íž ${orderId}: ì·šìëììµëë€ (ì¬ì : ${cancellationReason}).`;
}
// 5. ê²°ì ì¹ìž í ëŽêµ¬ì± íìŽëšž(sleep)륌 ì¬ì©íŽ 10ìŽ í ë°°ì¡ ì²ëЬ
status = 'SHIPPING';
await sleep('10 seconds');
status = 'DELIVERED';
return `죌묞 ë²íž ${orderId}: ë°°ì¡ìŽ ìë£ëììµëë€!`;
}
```
#### ð¡ íŽëŒìŽìžížìì Signal ì ì¡ ë° Query ì¡°í ë°©ë²:
```typescript
// ìžë¶ íŽëŒìŽìžíž ìœë ìì
const handle = client.workflow.getHandle(orderWorkflowId);
// íì¬ ìí ì¡°í (Query)
const currentStatus = await handle.query(getOrderStatusQuery);
console.log(`íì¬ ì£Œë¬ž ìí: ${currentStatus}`);
// ê²°ì ì¹ìž ì íž ì ì¡ (Signal)
await handle.signal(approvePaymentSignal, 'TX-987654');
```
---
## 6. [ì€ë¬Ž 4ëšê³] íë¡ëì
ë¶ì° íšíŽ: Saga ë¶ì° ížëìì
& Human-in-the-Loop
ë§ìŽí¬ë¡ìë¹ì€ ìí€í
ì²(MSA)ìì ê°ì¥ ê³šì¹ ìí 묞ì ë **ë¶ì° ížëìì
**곌 **ì¬ëì ì¹ìžì êž°ë€ëЬë ì¥êž° ìí¬íë¡ì°**ì
ëë€. Temporalììë ìì JavaScript ìœëë¡ ì°ìíê² íŽê²°í ì ììµëë€.
### ð¡ïž 1. Saga íšíŽ (볎ì ížëìì
ìì 례백)
ëšê³ë³ë¡ ì¡í°ë¹í°ë¥Œ ì€ííë€ê° ì€ê°ì ì€íší멎, **ì±ê³µíë ìŽì ëšê³ë€ì 볎ì ì¡í°ë¹í°(Compensation)륌 ìì(LIFO)ìŒë¡ ì€í**íì¬ ë°ìŽí° ìŒêŽì±ì ë§ì¶¥ëë€.
```typescript
// src/sagaWorkflow.ts
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const {
reserveHotel,
cancelHotelReservation,
bookFlight,
cancelFlightBooking,
chargeCreditCard,
refundCreditCard
} = proxyActivities<typeof activities>({
startToCloseTimeout: '10 seconds',
retry: { maximumAttempts: 3 },
});
export async function travelBookingSagaWorkflow(tripId: string): Promise<string> {
// 볎ì ížëìì
íšì ì€í (ìì ì€íì©)
const compensations: Array<() => Promise<void>> = [];
try {
// 1ëšê³: íží
ììœ
console.log('[Saga] 1. íží
ììœ ì§í ì€...');
const hotelId = await reserveHotel(tripId);
compensations.push(async () => {
console.log('[볎ì ì€í] íží
ììœ ì·šì ì€...');
await cancelHotelReservation(hotelId);
});
// 2ëšê³: íê³µê¶ ììœ
console.log('[Saga] 2. íê³µê¶ ììœ ì§í ì€...');
const flightId = await bookFlight(tripId);
compensations.push(async () => {
console.log('[볎ì ì€í] íê³µê¶ ììœ ì·šì ì€...');
await cancelFlightBooking(flightId);
});
// 3ëšê³: ì ì©ì¹Žë ê²°ì (ì¬êž°ì ê³ ìë¡ íë ìŽê³Œ ì€ë¥ ë°ì ê°ì )
console.log('[Saga] 3. ì ì©ì¹Žë ê²°ì ì§í ì€...');
await chargeCreditCard(tripId, 500000);
return `ì¬í ììœ ì±ê³µ! TripId: ${tripId}`;
} catch (err) {
console.error(`ðš [Saga ì€íš ë°ì] ížëìì
례백(볎ì ìì
)ì ììí©ëë€: ${err}`);
// ì€íš ì ë±ë¡ë 볎ì ìì
ì ìì(LIFO)ìŒë¡ ì€í
for (const compensate of compensations.reverse()) {
try {
await compensate();
} catch (compensationError) {
console.error('볎ì ìì
ì€í ì€íš (ì늌 ë° ìë ê°ì
íì):', compensationError);
}
}
throw new Error(`ì¬í ììœ ì 첎 ì€íš ë° ë¡€ë°± ìë£: ${(err as Error).message}`);
}
}
```
---
### ð§âðŒ 2. Human-in-the-Loop (êŽëЬì ì¹ìž ìí¬íë¡ì°)
ë¹ì© ê²°ì¬ë íŽê° ì ì²ì²ëŒ **ì¬ëì ì¹ìžìŽ íìí ìì
**ì íì ììëê³ , ì¹ìžë ëê¹ì§ 3ìŒìŽë 7ìŒìŽë ìì íê² ëêž°í©ëë€.
```typescript
// src/expenseApprovalWorkflow.ts
import { defineSignal, setHandler, condition, proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { notifyEmployee, transferFunds, notifyRejection } = proxyActivities<typeof activities>({
startToCloseTimeout: '10 seconds',
});
export const managerDecisionSignal = defineSignal<[boolean, string]>('managerDecision');
export async function expenseApprovalWorkflow(
expenseId: string,
amount: number
): Promise<string> {
let isApproved: boolean | null = null;
let reason = '';
// êŽëЬìì ì¹ìž/ë°ë € ìê·žë ížë€ë¬
setHandler(managerDecisionSignal, (decision, comment) => {
isApproved = decision;
reason = comment;
});
// êŽëЬì ê²°ì ëêž° (ìµë 3ìŒê° ëŽêµ¬ì± ìê² sleep ëêž°)
const decisionReceived = await condition(() => isApproved !== null, '3 days');
if (!decisionReceived) {
// 3ìŒ ëŽ ì¹ìžìŽ ììŒë©Ž ìë ìì€ì»¬ë ìŽì
ëë ìë ë°ë €
await notifyRejection(expenseId, 'ê²°ì¬ ëêž° ìê°(3ìŒ) ìŽê³Œë¡ ìë ë°ë €ëììµëë€.');
return 'EXPIRED_REJECTED';
}
if (isApproved) {
await transferFunds(expenseId, amount);
await notifyEmployee(expenseId, `ì¹ìž ìë£ (ì¬ì : ${reason})`);
return 'APPROVED';
} else {
await notifyRejection(expenseId, `êŽëЬì ë°ë € (ì¬ì : ${reason})`);
return 'REJECTED';
}
}
```
---
## 7. â ïž ê°ì¥ ì€ìí ê·ì¹: ê²°ì ë¡ (Determinism) ì ìœ & ë²ì êŽëЬ
Temporalì ì¬ì©í ë ìëìŽ ê°ë°ìë ì죌 ì€ìíë íµì¬ 죌ìì¬íì
ëë€.
### ð¥ ì ë Workflow ëŽì ìì±í멎 ì ëë êžì§ ìœë
| êµ¬ë¶ | â ì ë êžì§ (ë¹ê²°ì ë¡ ì ìœë) | â
ì¬ë°ë¥ž íŽê²° ë°©ë² |
| :--- | :--- | :--- |
| **íì¬ ìê°** | `Date.now()`, `new Date()` | `workflow.now()` ì¬ì© (Replay ì 곌거 ìê° ê·žëë¡ ì ì§ëš) |
| **ëì ìì±** | `Math.random()`, `crypto.randomUUID()` | `workflow.uuid4()` ì¬ì© (ê²°ì ë¡ ì ëì ìë êž°ë° ìì±) |
| **ì§ì ë€ížìí¬ I/O** | `fetch()`, `axios.get()`, `axios.post()` | **ë°ëì Activityë¡ ë¶ëЬ**íì¬ ížì¶ |
| **ì§ì DB ì ê·Œ** | Prisma, TypeORM, SQL 쿌늬 ì§ì ì€í | **ë°ëì Activityë¡ ë¶ëЬ**íì¬ ížì¶ |
| **íë¡ìžì€ íìŽëšž** | `setTimeout()`, `setInterval()` | `workflow.sleep('10 seconds')` ì¬ì© |
| **ì ì ê°ë³ ìí** | íìŒ ìµìëš ì ì ë³ì ìì (`globalCounter++`) | Workflow ëŽë¶ ì§ì ë³ì ëë Context íì© |
---
### ð ìí¬íë¡ì° ìœë ë³ê²œê³Œ ë²ì êŽëЬ (`patched`)
ìŽë¯ž ì€í ì€ìž ìí¬íë¡ì°ê° ìì² ê° ì¡Žì¬íë ìíìì ìœë륌 ë³ê²œí멎, Replay ì Ʞ졎 ìŽë²€íž íì€í 늬ì ë¶ìŒì¹íì¬ ì¥ì ê° ë°ìí©ëë€. ìŽë¥Œ ìíŽ **`patched` API**륌 ì¬ì©í©ëë€.
```typescript
// src/workflows.ts
import { patched, proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { sendEmail, sendKakaoNotification } = proxyActivities<typeof activities>({
startToCloseTimeout: '10 seconds',
});
export async function userOnboardingWorkflow(userId: string): Promise<void> {
// ì ê· íšì¹ ë²ì ìë³ì ì§ì
if (patched('use-kakao-notification-v2')) {
// ìë¡ìŽ ìí¬íë¡ì° ì€í ì ì€íëë ìœë
await sendKakaoNotification(userId);
} else {
// Ʞ졎ì ìŽë¯ž ì€í ì€ìŽìë 구ë²ì ìí¬íë¡ì°ì Replay ì ì€íëë ìœë
await sendEmail(userId);
}
}
```
---
## 8. CLI ì¹ížìíž: `temporal` ëª
ë ¹ì€ ë구 ìì ì ë³µ
ì죌 ì¬ì©íë `temporal` CLI ëª
ë ¹ìŽ ëªšìì
ëë€.
### ð 1. íŽë¬ì€í° ë° ê°ë° ìë² êŽëЬ
```bash
# ë¡ì»¬ ê°ë° ìë² ìì (Ʞ볞 í¬íž 7233, ì¹ UI 8233)
temporal server start-dev
# ë°ìŽí° ììí ëë í 늬 ì§ì íì¬ ì€í
temporal server start-dev --db-filename ~/temporal-data.db
# íŽë¬ì€í° ìí íìž
temporal operator cluster health
```
### ð 2. ìí¬íë¡ì° ì¡°í ë° ì€í
```bash
# ì€í ì€ìž 몚ë ìí¬íë¡ì° ëª©ë¡ ì¡°í
temporal workflow list
# í¹ì ìí¬íë¡ì° ììž ì 볎 ë° ìí ì¡°í
temporal workflow show --workflow-id <WORKFLOW_ID>
# CLIìì ìŠì ìí¬íë¡ì° ížëŠ¬ê±°
temporal workflow start \
--task-queue hello-world-queue \
--type exampleWorkflow \
--workflow-id my-first-wf \
--input '"ê¹ì² ì"'
# ìí¬íë¡ì° ìŽë²€íž íì€í 늬 ì€ìê° ì¶ì (Follow)
temporal workflow show --workflow-id my-first-wf --follow
```
### ð¡ 3. Signal ì ì¡ ë° Query ì¡°í
```bash
# ì€í ì€ìž ìí¬íë¡ì°ì ìê·žë(Signal) ì ì¡
temporal workflow signal \
--workflow-id my-first-wf \
--name approvePayment \
--input '"TX-998877"'
# ìí¬íë¡ì° ìí ì€ìê° ì¿ŒëŠ¬(Query)
temporal workflow query \
--workflow-id my-first-wf \
--name getOrderStatus
```
### ð 4. ì·šì ë° ê°ì ì¢
ë£
```bash
# ë¶ëë¬ìŽ ì·šì ìì² (Workflow ëŽë¶ ì·šì ížë€ë¬ ëì ê°ë¥)
temporal workflow cancel --workflow-id my-first-wf
# ê°ì ìŠì ì¢
ë£ (ìŠì Terminated ìíë¡ ê°ì ì í)
temporal workflow terminate --workflow-id my-first-wf --reason "êžŽêž ìŽìì ì€ëš"
```
---
## 9. íë¡ëì
ìŽì & ìí€í
ì² ì²Ží¬ëЬì€íž
ì€ì íë¡ëì
í겜ì Temporalì ëì
í ë ë°ëì ê²í íŽìŒ íë íµì¬ ìŽì ìì¹ì
ëë€.
```text
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â íë¡ëì
ìŽì 첎í¬ëЬì€íž (Checklist) â
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ€
â [ ] Task Queue ë¶ëЬ: ë¬Žê±°ìŽ CPU ìì
곌 ê°ë²ŒìŽ I/O ìì
ì í ë¶ëЬ ë°°ì â
â [ ] ì컀 ì€í ì€ìŒìŒë§: Task Queue Backlog ì§í êž°ë° HPA(ì¿ ë²ë€í°ì€) ì°ëâ
â [ ] Payload 2MB ì í: ëì©ë ë°ìŽí°ë S3/GCSì ì ì¥ í Claim-Check ì ë¬â
â [ ] Determinism ëŠ°í° ëì
: eslint-plugin-temporalë¡ ë¹ê²°ì ë¡ ìœë ì°šëš â
â [ ] TLS & mTLS ìíží: íŽë¬ì€í°ì ì컀 ê° ì ì¡ êµ¬ê° ë³Žì ìžìŠì ì ì© â
â [ ] ë°ìŽí° 컚ë²í°: 믌ê°í PII(ê°ìžì 볎)ì íŽëŒìŽìžíž ë 벚 ì¢
ëšê° ìížíâ
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
```
1. **Task Queue ë¶ëЬ ì ëµ**:
- `cpu-intensive-queue` (AI ëªšëž ì¶ë¡ , ëì©ë ìì¶)ì `io-lightweight-queue` (ì¹í
ë°ì¡, ì늌í¡)륌 ë³ëì Task Queueë¡ ë¶ëЬíê³ ê°ê° ë
늜ë ì ì© ì컀 íì í ë¹íì¬ ìíž ê°ìì ì°šëší©ëë€.
2. **ëì©ë Payload Claim-Check íšíŽ**:
- Temporalì ëšìŒ íìŽë¡ë ì íì 2MB (gRPC ë©ìì§ ìí 4MB)ì
ëë€. ëì©ë íìŒìŽë ìì MBì JSONì S3, GCS ëë MinIOì ì ì¥íê³ , ìí¬íë¡ì°ìë ê°ì²Ž URL(Key)ë§ ì ë¬í©ëë€.
3. **mTLS ë° ë°ìŽí° ìíží (Data Converter)**:
- êžìµ, ìë£ ë± ë¯Œê°í ê°ìžì 볎(PII)륌 ì²ëЬí ëë Custom Data Converter륌 ë±ë¡íì¬ ìì»€ê° íŽë¬ì€í°ë¡ ìŽë²€ížë¥Œ ì ì¡íêž° ì ì íìŽë¡ë륌 AES-GCM ë±ìŒë¡ ìížíí©ëë€. (Temporal Serverë ìížíë ëŽì©ì ìœì§ 못íŽë ì ì ì€ìŒì€ížë ìŽì
ê°ë¥)
4. **íµì¬ 몚ëí°ë§ ë©ížëŠ (Prometheus & Grafana)**:
- `temporal_workflow_task_schedule_to_start_latency`: ìì»€ê° ë¶ì¡±íì¬ ìì
ìŽ íìì ëêž°íë ìê° (ì€ìŒìŒìì ížëŠ¬ê±° ì§í)
- `temporal_activity_execution_failed`: ì¡í°ë¹í° ì€íšìš (ìëíí° ì¥ì ê°ì§)
- `temporal_workflow_execution_failed`: ìµì¢
ì€íší ìí¬íë¡ì° 걎ì
ì견 ë° ì§ë¬ž
0ìì§ ë±ë¡ë ìê²¬ìŽ ììµëë€. 첫 ë²ì§ž ëêžì ëšê²šë³Žìžì!
ëêž ìì
ëêž ìì