Temporal 완벽 가이드 — 분산 워크플로우 오케스트레이션의 새 표준
이 글의 핵심
Temporal은 "비즈니스 워크플로우를 평범한 코드처럼 쓰지만 장애·재시작에 자동으로 견디게 한다"는 Durable Execution 개념을 프로덕션 수준에서 구현한 오케스트레이션 플랫폼입니다. 결제·주문·온보딩·배치 처리·Saga·장기 실행 프로세스를 신뢰성 있게 처리하며 Uber·Netflix·Snap·Box·코인베이스 등이 내부 표준으로 채택했습니다. 이 글은 핵심 개념·주요 SDK·실전 패턴·운영을 정리합니다.
핵심 개념
Workflow
장기 실행 가능한 결정적 함수. Temporal이 이벤트를 기록하면서 실행해, 언제든 정확히 같은 결과로 재생(replay) 가능.
Activity
실제 부수 효과를 일으키는 단위(DB 쓰기, 외부 API 호출, 이메일 전송). 재시도·타임아웃이 명시적.
Task Queue
워커가 구독하는 큐. 워크플로우·액티비티 작업이 여기로 분배.
Worker
워크플로우/액티비티 코드를 실행하는 프로세스. 사용자가 배포·운영.
Temporal Service
워크플로우 이벤트 히스토리·큐를 관리하는 중앙 서비스. 자체 호스팅 또는 Temporal Cloud.
사용자 코드 Temporal Service Worker (당신의 코드)
───────── ────────────────── ────────────────────
client.start ────────► [Workflow History] ◄───── 실행(결정적 replay)
▲ │
│ │ Activity Task
│ ▼
Task Queue ─────────────► Activity 실행 (DB, API, ...)
설치 / Quickstart
로컬 서버
# 단일 바이너리
curl -sSf https://temporal.download/cli.sh | sh
temporal server start-dev
# Web UI: http://localhost:8233
# gRPC: localhost:7233
TypeScript SDK
mkdir demo && cd demo
npm init -y
npm i @temporalio/workflow @temporalio/activity @temporalio/worker @temporalio/client
TypeScript 예제: 결제 워크플로우
Activities
// src/activities.ts
import { Context } from "@temporalio/activity"
export async function chargeCreditCard(orderId: string, amount: number): Promise<string> {
Context.current().heartbeat()
// 실제 결제 API 호출
const txId = await callPaymentApi(orderId, amount)
return txId
}
export async function reserveInventory(orderId: string, items: Item[]): Promise<void> {
// 재고 차감
}
export async function sendReceipt(userId: string, orderId: string): Promise<void> {
// 이메일 전송
}
export async function refund(orderId: string): Promise<void> {
// 환불
}
Workflow
// src/workflows.ts
import { proxyActivities, sleep, defineSignal, setHandler, condition } from "@temporalio/workflow"
import type * as activities from "./activities"
const { chargeCreditCard, reserveInventory, sendReceipt, refund } =
proxyActivities<typeof activities>({
startToCloseTimeout: "1 minute",
retry: {
initialInterval: "1s",
maximumInterval: "1m",
backoffCoefficient: 2,
maximumAttempts: 5,
},
})
export const cancelSignal = defineSignal("cancel")
export async function processOrder(order: Order): Promise<{ status: string; txId?: string }> {
let cancelled = false
setHandler(cancelSignal, () => { cancelled = true })
const txId = await chargeCreditCard(order.id, order.total)
try {
await reserveInventory(order.id, order.items)
} catch (err) {
await refund(order.id)
throw err
}
await sleep("30 seconds") // 사용자 취소 대기
if (cancelled) {
await refund(order.id)
return { status: "cancelled" }
}
await sendReceipt(order.userId, order.id)
return { status: "completed", txId }
}
Worker
// src/worker.ts
import { Worker } from "@temporalio/worker"
import * as activities from "./activities"
async function run() {
const worker = await Worker.create({
workflowsPath: require.resolve("./workflows"),
activities,
taskQueue: "orders",
})
await worker.run()
}
run().catch(console.error)
Client (워크플로우 시작)
// src/start.ts
import { Client } from "@temporalio/client"
import { processOrder, cancelSignal } from "./workflows"
async function main() {
const client = new Client()
const handle = await client.workflow.start(processOrder, {
args: [{ id: "ord_123", userId: "u_1", total: 99.9, items: [...] }],
taskQueue: "orders",
workflowId: "order-ord_123",
})
console.log("Started", handle.workflowId)
// 필요 시 취소
// await handle.signal(cancelSignal)
const result = await handle.result()
console.log(result)
}
main()
Durable Execution의 의미
워커 프로세스가 sleep("30 seconds") 중 크래시해도, 다른 워커가 해당 워크플로우를 이어받아 정확히 같은 지점부터 실행합니다. 이벤트 히스토리에 결정이 기록돼 있어 리플레이 시 이전 Activity 결과를 재생성하지 않고 기록된 값을 재사용합니다.
이 덕분에:
- 하루 지속되는 워크플로우를 메모리에 담아두지 않아도 됨
- 워커 배포·재시작이 워크플로우 진행에 영향을 주지 않음
- 중간 크래시·네트워크 장애가 투명하게 복구됨
Saga 패턴
export async function bookTrip(trip: Trip): Promise<void> {
const compensations: (() => Promise<void>)[] = []
try {
const flight = await bookFlight(trip)
compensations.unshift(() => cancelFlight(flight.id))
const hotel = await bookHotel(trip)
compensations.unshift(() => cancelHotel(hotel.id))
const car = await bookCar(trip)
compensations.unshift(() => cancelCar(car.id))
await sendItinerary(trip.userId)
} catch (err) {
for (const comp of compensations) {
await comp()
}
throw err
}
}
Saga의 보상 트랜잭션도 자동 재시도로 안전하게 실행.
Signals·Queries·Updates
- Signal: 외부에서 워크플로우로 비동기 메시지 (취소·승인)
- Query: 워크플로우 내부 상태 읽기 (부수효과 없음)
- Update (2024+): Signal + 응답 값을 받는 동기 요청
import { defineQuery, defineUpdate } from "@temporalio/workflow"
export const statusQuery = defineQuery<string>("status")
export const approveUpdate = defineUpdate<boolean, [string]>("approve")
export async function workflow() {
let status = "waiting"
let approved = false
setHandler(statusQuery, () => status)
setHandler(approveUpdate, (reason: string) => {
approved = true
return true
})
await condition(() => approved)
status = "approved"
}
스케줄러 (Cron·Interval)
await client.schedule.create({
scheduleId: "daily-cleanup",
spec: { cronExpressions: ["0 3 * * *"] },
action: {
type: "startWorkflow",
workflowType: "cleanupWorkflow",
taskQueue: "maintenance",
args: [],
},
})
setInterval(1d) 루프 대신 Temporal 스케줄을 사용하면 시간대·일시 정지·수동 실행 UI 모두 공짜.
Child Workflow
import { executeChild } from "@temporalio/workflow"
export async function batchWorkflow(jobs: Job[]) {
const results = await Promise.all(jobs.map((j) =>
executeChild("singleJob", { args: [j], workflowId: `job-${j.id}` })
))
return results
}
수천 개 워크플로우를 병렬 실행해도 Temporal이 분산 스케줄링.
버전 관리
워크플로우 코드는 결정적이어야 하므로 로직 변경 시 기존 실행 중인 워크플로우의 replay가 깨질 수 있습니다.
import { patched, deprecatePatch } from "@temporalio/workflow"
export async function myWorkflow() {
if (patched("feature-x")) {
await newActivity()
} else {
await oldActivity()
}
}
patched로 안전하게 새 경로 추가, 오래된 워크플로우 종료 후 deprecatePatch.
Python SDK
from datetime import timedelta
from temporalio import workflow, activity
from temporalio.client import Client
from temporalio.worker import Worker
@activity.defn
async def charge(order_id: str, amount: float) -> str:
return await call_payment_api(order_id, amount)
@workflow.defn
class ProcessOrder:
@workflow.run
async def run(self, order: dict) -> dict:
tx = await workflow.execute_activity(
charge,
args=[order["id"], order["total"]],
start_to_close_timeout=timedelta(minutes=1),
)
return {"tx": tx}
async def main():
client = await Client.connect("localhost:7233")
worker = Worker(client, task_queue="orders",
workflows=[ProcessOrder], activities=[charge])
await worker.run()
주요 SDK 상태 (2026)
| SDK | 상태 |
|---|---|
| Go | 성숙 |
| Java | 성숙 |
| TypeScript | 성숙 |
| Python | 성숙 |
| .NET | 성숙 |
| Ruby | GA 초기 |
| PHP | 커뮤니티 |
배포 / Kubernetes
# Helm
helm repo add temporalio https://go.temporal.io/helm-charts
helm install temporal temporalio/temporal \
--set server.replicaCount=3 \
--set cassandra.config.cluster_size=3 \
--set prometheus.enabled=false
- Persistence: Cassandra, MySQL, PostgreSQL 중 선택
- Visibility: Elasticsearch/OpenSearch로 advanced search
- Temporal Cloud: 관리형 옵션, 운영 부담 제거
워커는 애플리케이션과 함께 Kubernetes Deployment로 배포, 태스크 큐 단위로 오토스케일.
운영 모범 사례
- Workflow는 idempotent·결정적으로: 랜덤·시간은
workflow.now()·workflow.random()사용 - Activity는 외부 상호작용만: 워크플로우에서 직접 네트워크 호출 금지
- Task Queue 분리: CPU-heavy, I/O-heavy, priority 별로
- Retry 정책: 각 Activity에 명시
- Heartbeat: 긴 Activity는 주기 heartbeat로 죽음 감지
- Observability: 메트릭·로그·히스토리 모두 활용
- Namespaces: 환경·팀별 격리
트러블슈팅
Non-deterministic workflow
워크플로우 코드에서 Date.now()·Math.random()·파일 I/O 등 비결정 호출. 워크플로우 API 사용.
Workflow History가 큼
기본 이벤트 히스토리 제한 ≈ 51200 이벤트. 넘을 가능성이면 continueAsNew()로 주기 재시작.
Activity 타임아웃 선택
StartToCloseTimeout: 1회 실행 시간 상한ScheduleToCloseTimeout: 전체 시도 시간HeartbeatTimeout: 마지막 heartbeat 이후 허용 시간
로컬 Activity vs 일반 Activity
workflow.executeLocalActivity는 동일 워커에서 즉시 실행, 네트워크 왕복 없음. 짧은 보조 작업에 적합.
Worker 과부하
Task queue당 maxConcurrentActivityTaskExecutions·maxConcurrentWorkflowTaskExecutions 튜닝. 워커 수 수평 확장이 우선.
체크리스트
- Workflow 코드 결정성 유지 (sleep·timers·activity만 side-effect)
- Activity 재시도·타임아웃 명시
- Task Queue 워크로드별 분리
- Saga로 보상 트랜잭션
- 스케줄·Cron은 Temporal Schedule
- Continue-as-new로 긴 히스토리 관리
- Prometheus 메트릭·Grafana 대시보드
- Temporal Cloud 또는 HA 자체 호스팅
- 워커 수평 확장·오토스케일
마무리
Temporal은 “장기 실행 비즈니스 워크플로우를 어떻게 안전하게?”라는 깊은 문제를 Durable Execution이라는 근본적 해답으로 풀어낸 드문 사례입니다. 큐·상태 머신·Redis·Cron을 덕지덕지 붙여 만드는 오케스트레이션 대신, 평범한 코드로 작성한 워크플로우가 자동으로 장애에 견디게 만듭니다. Uber·Netflix·Snap·Stripe·Coinbase 같은 조직이 내부 표준으로 채택했고, 2026년 현재 Temporal Cloud까지 성숙해 관리형 도입도 쉬워졌습니다. 결제·주문·구독·온보딩·ETL 같은 도메인에서 복잡한 상태 관리에 시달리고 있다면 Temporal은 재발명 대신 바로 도입할 만한 검증된 기반입니다.
관련 글
- 이벤트 기반 아키텍처 가이드
- Saga 패턴 완벽 가이드
- Airflow 완벽 가이드
- 마이크로서비스 패턴 가이드
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Temporal 완벽 가이드. Durable Execution으로 장기 실행 비즈니스 프로세스를 코드로 작성하고 안정적으로 실행. Workflow·Activity·Signal·Saga·스케줄러·재시도 실전. Go/T… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
이 글에서 다루는 키워드 (관련 검색어)
Temporal, Workflow, Orchestration, Distributed Systems, Durable Execution, Saga, Microservices 등으로 검색하시면 이 글이 도움이 됩니다.