Temporal 완벽 가이드 — 분산 워크플로우 오케스트레이션의 새 표준

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성숙
RubyGA 초기
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로 배포, 태스크 큐 단위로 오토스케일.

운영 모범 사례

  1. Workflow는 idempotent·결정적으로: 랜덤·시간은 workflow.now()·workflow.random() 사용
  2. Activity는 외부 상호작용만: 워크플로우에서 직접 네트워크 호출 금지
  3. Task Queue 분리: CPU-heavy, I/O-heavy, priority 별로
  4. Retry 정책: 각 Activity에 명시
  5. Heartbeat: 긴 Activity는 주기 heartbeat로 죽음 감지
  6. Observability: 메트릭·로그·히스토리 모두 활용
  7. 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 완벽 가이드
  • 마이크로서비스 패턴 가이드