Skip to content
Published on

Durable Execution 엔진 2026 — Temporal·Restate·Inngest·Trigger.dev·DBOS 깊이 비교: 크론과 재시도 지옥에서 벗어나는 법

Authors

프롤로그 — 크론과 재시도 지옥을 벗어나는 길

2019년의 백엔드 엔지니어는 이런 코드를 짰다. "결제를 받고, 5분 후에 영수증을 보내고, 2일 후에 후기 요청 메일을 발송한다." 답은 보통 이렇게 생겼다.

  • 결제 처리 — 동기 API.
  • 영수증 — 큐(SQS·RabbitMQ)에 푸시, 워커가 5분 지연 처리.
  • 후기 메일 — 크론으로 매일 한 번 스캔, "2일 지난 결제" 골라 발송.
  • 실패 시? retry_count 컬럼을 만들고, 7일 후 포기.

이 패턴은 동작했다. 다만 매번 새로 짰고, 매번 버그가 났고, 매번 옵저버빌리티가 없었다. "어디까지 진행됐는지" 보려면 DB를 직접 까야 했다.

2026년의 답은 다르다.

@workflow
async function postPurchaseFlow(orderId: string) {
  await chargeCustomer(orderId)
  await sleep('5 minutes')
  await sendReceipt(orderId)
  await sleep('2 days')
  await sendReviewRequest(orderId)
}

이게 끝이다. 함수 그대로가 워크플로 정의다. 서버가 죽어도, 5분 후에도, 2일 후에도, 함수는 정확히 그 줄에서 다시 시작된다. 큐도, 크론도, retry_count 컬럼도 없다.

이 마법의 이름이 Durable Execution(이하 DE). 2024년부터 본격적으로 화제가 됐고, 2026년에는 백엔드 인프라의 표준 카테고리가 됐다.

  • Temporal은 2026년 2월 Series D로 3억 달러를 50억 달러 밸류에이션에 모았다. 누적 액션 실행 9.1조 회, AI 네이티브 회사만 1.86조 회.
  • AWS는 2025 re:Invent에서 Lambda Durable Functions를 발표했다.
  • Cloudflare Workflows가 GA로 풀렸다.
  • Vercel은 Workflow DevKit을 내놓았다.
  • Inngest·Trigger.dev·Restate·DBOS는 각자의 무기로 점유율을 끌어올렸다.

왜 지금인가? AI 에이전트다. 1시간 동안 도구를 30번 호출하는 에이전트 루프는 서버가 한 번이라도 죽으면 처음부터 다시 시작해야 한다. 그건 곧 청구서다. 토큰 비용이 한 번 깨질 때마다 5달러씩 증발한다. DE는 그걸 막는다.

이 글의 흐름은 이렇다.

  1. Durable Execution이 정확히 무엇인가 — 결정적 재실행과 워크플로 코드
  2. 2026년 엔진 지형 한눈에 — 비교 매트릭스
  3. Temporal — DE의 표준이 된 사연
  4. Restate — 가벼운 도전자
  5. Inngest — 이벤트 기반 DX 우위
  6. Trigger.dev — Next.js 시대의 DE
  7. DBOS — Postgres만 있으면 되는 라이트웨이트
  8. 같은 워크플로를 세 엔진으로 — 실제 코드 비교
  9. AWS Step Functions·Azure Durable Functions·Cadence 자리잡기
  10. AI 에이전트와 DE — 왜 폭발했나
  11. 도입 의사결정과 안티패턴

다 읽고 나면 "우리 팀에는 뭐가 맞나"가 5분 안에 정해져야 한다.


1장 · Durable Execution이란 무엇인가

1.1 정의 — 의도와 실행의 분리

Durable Execution은 함수의 의도(비즈니스 로직)와 실제 실행(언제·어디서·몇 번 돌릴지)을 분리하는 패러다임이다. 함수는 평범한 코드처럼 보이지만, 런타임이 매 단계 진행 상황을 영속 저장소에 체크포인팅한다. 서버가 죽으면 다른 워커가 그 줄부터 이어 실행한다.

핵심 보장:

  • Exactly-once 효과: 같은 단계는 두 번 실행되지 않는다. 부작용을 한 번만 일으킨다.
  • 장시간 sleep: sleep('30 days') 가 그대로 의미 있다. 30일 후에도 깨어난다.
  • 재시도 with 상태: 실패한 단계만 재시도, 성공한 단계 결과는 기억.
  • 결정적 재실행: 워커가 죽었다 살아나면 이력을 재생해 정확히 같은 상태로 복원.

1.2 두 가지 메커니즘

DE 엔진은 보통 두 갈래 중 하나다.

메커니즘설명대표
저널 기반 재생(journal-based replay)완료된 모든 단계를 append-only 로그에 기록. 워커가 깨어나면 처음부터 코드를 재실행하되, 이미 기록된 결과는 다시 호출하지 않고 기록값 그대로 사용.Temporal · Cadence · Restate
데이터베이스 체크포인팅(DB checkpointing)각 단계가 끝나면 DB 트랜잭션으로 상태 저장. 재개 시 DB에서 마지막 상태를 읽고 그 다음 단계부터 실행.DBOS · Inngest · Trigger.dev (계열별 다름)

저널 기반은 코드를 결정적으로 짜야 한다는 제약이 있다. 예를 들면 Math.random()이나 Date.now()를 직접 부르면 재생 시 다른 값이 나와서 깨진다. 엔진이 제공하는 결정적 API를 써야 한다.

DB 체크포인팅은 그 제약은 약하지만, 단계의 입력·출력 직렬화·DB 부하·트랜잭션 경계가 운영의 핵심이 된다.

1.3 워크플로 코드(workflow-as-code) 모델

옛날 워크플로 엔진(예: Airflow·Cadence 초기·Step Functions)은 DAG·JSON·YAML로 짰다. DE는 그냥 코드다. if·for·try·await이 그대로 의미를 가진다.

@workflow
async function orderFlow(input: OrderInput) {
  const payment = await ctx.run('charge', () => chargeCard(input))

  try {
    await ctx.run('ship', () => shipItem(input))
  } catch (e) {
    // saga: 보상 트랜잭션
    await ctx.run('refund', () => refundCard(payment))
    throw e
  }

  await ctx.sleep('1 day')
  await ctx.run('review-request', () => sendReviewMail(input))
}

이 코드는 그냥 평범한 JS다. 하지만 런타임은 ctx.run마다 결과를 체크포인팅하고, ctx.sleep은 워커 메모리를 점유하지 않고 1일 후 다른 워커에서 재개된다.

1.4 DE가 푸는 패턴

DE가 빛나는 워크플로 모양:

  • 장시간 승인 플로 — 휴가 신청, 매니저가 3일 후 클릭해야 진행.
  • Saga·보상 트랜잭션 — 결제 → 배송 → 메일, 어디서 실패해도 보상이 따라옴.
  • 재시도 with 상태 — 외부 API 5번 실패해도 멱등하게 재시도, 진행 상태 보존.
  • 스케줄·팬아웃 — 매일 자정에 사용자 1만 명에 푸시 알림, 각 푸시 결과 추적.
  • AI 에이전트 루프 — LLM 호출 → 도구 실행 → 다음 LLM 호출, 중간에 죽어도 컨텍스트 유지.

1.5 DE가 필요 없는 경우

모든 워크플로가 DE를 부르는 건 아니다.

  • 단순 요청-응답: 100ms 안에 끝나면 HTTP·gRPC로 충분.
  • 읽기 전용 파이프라인: ETL·배치 분석은 Spark·dbt·Airflow가 적합.
  • 하루 100건 미만: 운영 부담이 이득을 넘는다. 로그·DB 컬럼이 더 싸다.
  • 팀이 1명: DE 엔진 자체의 학습 곡선 ≧ 직접 짜는 비용.

"워크플로가 5분을 넘기고, 외부 호출이 3개 이상이고, 실패가 비싸다" → DE를 검토하라.


2장 · 2026년 엔진 비교 매트릭스

항목TemporalRestateInngestTrigger.devDBOS
카테고리DE 표준 · 엔터프라이즈DE + 상태 · 라이트DE + 이벤트 · 서버리스DE + Next.js 친화DE 라이브러리 · Postgres
언어 SDKGo · Java · TS · Python · .NET · PHP · RubyTS · Java · Kotlin · Go · Python · RustTS · Python · Go · JavaTS · PythonTS · Python · Java · Go
자기호스팅가능 (Apache 2.0)가능 (Single binary · BSL→Apache)가능 (Apache 2.0, Inngest server)가능 (Apache 2.0, v4)가능 (Apache 2.0, lib)
인프라 의존자체 서버 + Cassandra/PostgresSingle binary + RocksDB자체 서버 + Postgres자체 서버 + Postgres + RedisPostgres만
가격 모델 (클라우드)액션당 약 0.00025달러부터자체 SaaS 시작 (early)실행 횟수+이벤트 freemium동시 실행+월정액 freemium체크포인트 100만 당 50달러
무료 티어자기호스팅 무료자기호스팅 무료5만 실행/월 무료5달러 크레딧/월자기호스팅 무료
워크플로 모델결정적 함수 (저널)결정적 함수 (저널 + 상태키)단계 함수 (이벤트 트리거)태스크 함수 (트리거 기반)트랜잭션 함수 (Postgres)
AI 에이전트 친화도매우 높음 (OpenAI Codex 채택)높음 (Pydantic AI 통합)매우 높음 (Agent Kit)매우 높음 (Session/Realtime)중간 (라이브러리 통합)
옵저버빌리티Web UI + tctl + OTelWeb UI + OTelWeb UI + 실시간 스트림Web UI + 실시간 로그·트리거DB 쿼리 + 대시보드
최강점규모 · 멀티언어 · 성숙도가벼움 · 단일 바이너리이벤트 기반 · DX · 가격TS 풀스택 · 긴 작업 무제한단순함 · Postgres 한 개
약점학습 곡선 · 운영 부담생태계 신생멀티언어 약함TS 중심결정적 재생 없음
타깃 사용자대기업·금융·AI 인프라풀스택 백엔드 · DB 친화TS·Python 스타트업Next.js·풀스택 팀Postgres 중심 백엔드

이 표 하나로 의사결정의 80%가 끝난다. 어떤 행이 너희 팀의 deal-breaker인지 짚어내라. 보통 셋 중 하나다 — 언어·자기호스팅 의무·가격 모델.


3장 · Temporal — DE의 표준이 된 사연

3.1 출자 · 채택 · 규모

Temporal은 2019년 Cadence(우버에서 Maxim Fateev·Samar Abbas가 만든 오픈소스)를 떠나 같은 팀이 만든 후속작이다. 2026년 2월 Series D 3억 달러, 밸류에이션 50억 달러. 누적 9.1조 액션 실행, AI 네이티브 회사 1.86조. OpenAI Codex가 Temporal로 매일 수백만 코딩 에이전트 요청을 처리한다는 공개 사례가 있다. NVIDIA·Netflix 등 3,000+ 유료 고객.

3.2 아키텍처 — 저널과 워커의 분리

핵심 구성:

  • Temporal Server: 워크플로 상태(저널 = Event History)를 저장. Cassandra·Postgres·MySQL 백엔드.
  • Worker: 사용자 코드를 실행. Server와 long-poll로 통신.
  • Frontend·History·Matching·Worker 서비스로 내부적으로 분할 가능.

워커는 상태가 없다. 워크플로 진행 상태는 전부 Server에 있다. 워커가 죽으면 다른 워커가 같은 워크플로의 다음 작업을 받아서 이력을 재생하고 이어간다.

3.3 결정적 워크플로 코드 — 제약과 보상

규칙은 단순하지만 엄격하다. 워크플로 함수 안에서는:

  • 직접 시간을 부르지 마라 (Date.now() 금지). workflow.now().
  • 직접 랜덤을 부르지 마라. workflow.random().
  • 외부 호출을 직접 하지 마라. proxyActivities로 활동(activity) 호출.
  • 멀티스레드·전역 변수 금지.

이 제약을 지키면 재생이 결정적이 된다. 워크플로 코드를 처음부터 다시 돌려도 같은 결과 시퀀스가 나온다. Server의 이력을 따라가며 이미 끝난 활동은 결과를 캐시에서 꺼내 쓴다.

3.4 액티비티 — 외부 세계와 만나는 곳

// activities.ts
export async function chargeCard(orderId: string): Promise<PaymentReceipt> {
  return await stripe.charge({ orderId })
}

export async function shipItem(orderId: string): Promise<TrackingNumber> {
  return await fedex.createShipment({ orderId })
}

// workflows.ts
import { proxyActivities, sleep } from '@temporalio/workflow'
import type * as activities from './activities'

const { chargeCard, shipItem } = proxyActivities<typeof activities>({
  startToCloseTimeout: '30 seconds',
  retry: { maximumAttempts: 5, initialInterval: '1s', backoffCoefficient: 2 },
})

export async function orderFlow(orderId: string) {
  const receipt = await chargeCard(orderId)
  await sleep('5 minutes')
  const tracking = await shipItem(orderId)
  return { receipt, tracking }
}

활동은 일반 함수다. 재시도·타임아웃·하트비트·idempotency 토큰을 옵션으로 받는다.

3.5 가격과 자기호스팅

  • Temporal Cloud: 액션당 약 0.00025달러. 저장된 액션·활성 워커·저장 기간(7일·30일·90일)이 비용 곱셈 인자다. 저트래픽 약 월 200달러부터.
  • 자기호스팅: Apache 2.0 라이선스, 무료. 단 Cassandra/Postgres 운영·HA·튜닝이 만만찮다. 보통 월 2,500 ~ 4,500달러 인프라+인건비.

규모와 성숙도가 무기. 작은 팀에 비싼 학습 곡선이 약점이다.


4장 · Restate — 가벼운 도전자

4.1 포지션

Restate는 "Temporal이 너무 무겁다"는 문제 의식에서 출발했다. 단일 바이너리(Rust로 작성, 내부 RocksDB)로 돌아가고, 외부 DB 의존이 없다. 사용자의 기존 서비스 앞단에 프록시처럼 끼어서 호출 저널을 잡는다.

4.2 핵심 차별점

  • Single binary: Docker 한 줄, 별도 DB·메시지 큐 불필요.
  • 상태(state)와 통신을 함께: 워크플로 안에서 키-값 상태(ctx.set('cart', items))를 다룰 수 있다. Redis·DB가 끼지 않아도 OK.
  • HTTP·gRPC 핸들러: 워크플로가 곧 HTTP 서비스다. 외부에서 그냥 부른다.
  • 결정적 함수 모델: Temporal과 비슷한 제약. JS·TS·Java·Kotlin·Go·Python·Rust SDK.

4.3 코드 모양

import * as restate from '@restatedev/restate-sdk'

const order = restate.workflow({
  name: 'order',
  handlers: {
    run: async (ctx: restate.WorkflowContext, orderId: string) => {
      const receipt = await ctx.run('charge', () => chargeCard(orderId))
      await ctx.sleep(5 * 60_000)
      const tracking = await ctx.run('ship', () => shipItem(orderId))
      ctx.set('status', 'shipped')
      return { receipt, tracking }
    },
  },
})

restate.endpoint().bind(order).listen(9080)

이걸 띄우고, Restate 서버 한 개를 띄우고, 등록만 하면 끝이다.

4.4 가격과 자기호스팅

  • Restate Cloud: 2025년 후반 GA, 초기에는 무료 티어 + 곧 유료 티어 신설 예고. SLA 강화 중.
  • 자기호스팅: BSL → Apache 2.0(시간 경과 후) 라이선스. 단일 바이너리 무료.

Temporal보다 운영 부담이 작고, DBOS보다 워크플로 모델이 표현력 있고. 다만 생태계는 아직 신생이다.


5장 · Inngest — 이벤트 기반 DX 우위

5.1 포지션

Inngest는 "이벤트 → 함수" 모델이 직관이다. 함수가 이벤트를 구독하고, 함수 안의 모든 step.run 호출이 자동으로 체크포인팅된다. 워크플로 정의가 따로 없다 — 함수 자체가 워크플로.

5.2 코드 모양

import { Inngest } from 'inngest'

const inngest = new Inngest({ id: 'shop' })

export const orderFlow = inngest.createFunction(
  { id: 'order-flow' },
  { event: 'order.placed' },
  async ({ event, step }) => {
    const receipt = await step.run('charge', async () => {
      return await chargeCard(event.data.orderId)
    })

    await step.sleep('wait-5m', '5m')

    const tracking = await step.run('ship', async () => {
      return await shipItem(event.data.orderId)
    })

    await step.sleep('wait-2d', '2d')

    await step.run('review-mail', async () => {
      return await sendReviewMail(event.data.orderId)
    })

    return { receipt, tracking }
  }
)

eventfunction 모델이라 도메인 이벤트 발행과 자연스럽게 맞물린다. AWS EventBridge 감각인데 자기호스팅 가능.

5.3 2026년 주목할 점

  • Checkpointing 발표: 단계 간 지연을 거의 0으로 줄여 워크플로 총 실행시간 50% 단축.
  • Agent Kit: AI 에이전트 빌드용 헬퍼. 도구 호출·LLM 응답을 자동으로 단계로 분해.
  • Agent Skills: Claude Code·Cursor·Windsurf용 사전 정의 스킬 6종.
  • 자기호스팅 Inngest server: 프로덕션 그레이드 자기호스팅, Postgres 백엔드.

5.4 가격

  • 클라우드: 첫 5만 실행 무료. 그 이후 사용량 기반 + 이벤트당 과금. 첫 100만 이벤트는 무료.
  • 자기호스팅: Apache 2.0. 무료.

TypeScript·Python 팀, 특히 Vercel·Next.js 배포와 잘 어울린다. 멀티언어가 약하다.


6장 · Trigger.dev — Next.js 시대의 DE

6.1 포지션

Trigger.dev는 "Vercel에서 못 돌리는 긴 작업을 어디서 돌릴까"라는 절실한 문제에 답한 도구다. v4에서 결정적 재생 모델·세션 기반 양방향 채널·실시간 로그를 도입했다.

6.2 코드 모양

import { task, logger, wait } from '@trigger.dev/sdk/v3'

export const orderFlow = task({
  id: 'order-flow',
  retry: { maxAttempts: 5, factor: 2, minTimeoutInMs: 1000 },
  run: async (payload: { orderId: string }, { ctx }) => {
    const receipt = await chargeCard(payload.orderId)
    logger.info('charged', { receipt })

    await wait.for({ minutes: 5 })

    const tracking = await shipItem(payload.orderId)
    logger.info('shipped', { tracking })

    await wait.for({ days: 2 })

    await sendReviewMail(payload.orderId)

    return { receipt, tracking }
  },
})

세 가지 강점:

  1. 타임아웃 없음: Lambda·Vercel·Cloudflare의 시간 제한 없이 시간 단위 작업 가능.
  2. 대기 중 결제 안 함: 작업이 wait에 들어가면 컨테이너가 동결돼 비용이 안 든다.
  3. Realtime 로그: 사용자가 실행을 보고 있는 동안 로그가 SSE로 흘러나온다.

6.3 2026년 주목할 점

  • Session primitive: 실행을 넘어서 살아남는 양방향 I/O 채널. 채팅 에이전트 매니저 역할.
  • Concurrency·Queue 제어: 작업별로 동시 실행 한도와 큐 우선순위 조정.
  • v4 결정적 모드: 옵트인. 더 강한 정확성 보장이 필요한 경우.

6.4 가격

  • Free: 월 5달러 크레딧, 동시 실행 10개.
  • Hobby: 월 10달러.
  • Pro: 월 50달러, 동시 실행 200개+.
  • Enterprise: 별도.
  • 자기호스팅: v4 Apache 2.0.

Next.js·풀스택 TS 팀에 거의 디폴트 선택지가 됐다.


7장 · DBOS — Postgres만 있으면 되는 라이트웨이트

7.1 포지션

DBOS는 "Postgres가 이미 있는데 왜 또 인프라를 띄우나"라는 질문에 답한다. 라이브러리로 끝난다. 별도 서버 없음. 워크플로 상태는 사용자의 Postgres에 그대로 저장.

7.2 코드 모양 (TypeScript)

import { DBOS, Workflow, Step } from '@dbos-inc/dbos-sdk'

class OrderFlow {
  @Step()
  static async chargeCard(orderId: string) {
    return await stripe.charge({ orderId })
  }

  @Step()
  static async shipItem(orderId: string) {
    return await fedex.create({ orderId })
  }

  @Workflow()
  static async run(orderId: string) {
    const receipt = await OrderFlow.chargeCard(orderId)
    await DBOS.sleep(5 * 60_000)
    const tracking = await OrderFlow.shipItem(orderId)
    return { receipt, tracking }
  }
}

@Step은 단일 Postgres 트랜잭션 안에서 실행될 수 있다 — DB 작업이라면 정확히 한 번 실행을 DB 자체가 보장한다. 워크플로가 실패하면 DB 행이 그 사실을 기록하고, 워커가 재시작 시 같은 워크플로 ID로 재개한다.

7.3 강점·약점

  • 강점: 인프라 한 개(Postgres만). 학습 곡선 가장 낮다. 트랜잭셔널 정합성.
  • 약점: 결정적 재생 모델은 아님. 워크플로 코드를 처음부터 재실행하지 않는다. 대신 마지막 체크포인트부터. 매우 긴 워크플로·복잡한 분기에는 표현력 한계.
  • 2026: Java 0.8에서 Spring Boot 통합. Conductor(클라우드 옵저버빌리티) 자기호스팅 가능.

7.4 가격

  • 자기호스팅 라이브러리: 무료.
  • DBOS Cloud + Conductor: 추가 체크포인트 100만 당 50달러. 엔터프라이즈 커스텀.

Postgres-중심 백엔드 팀에 가장 적은 마찰.


8장 · 같은 워크플로를 세 엔진으로 — 실제 코드 비교

시나리오: 결제 받기 → 4번까지 재시도 → 성공 시 영수증 → 2일 후 리뷰 메일. 같은 모양을 Temporal·Inngest·Trigger.dev로 짠다.

8.1 Temporal (TS)

// activities.ts
export async function chargeCard(orderId: string) {
  return await stripe.charge({ orderId })
}
export async function sendReceipt(orderId: string, receipt: Receipt) {
  await mailer.send({ to: orderId, body: receipt })
}
export async function sendReviewMail(orderId: string) {
  await mailer.send({ to: orderId, template: 'review' })
}

// workflow.ts
import { proxyActivities, sleep } from '@temporalio/workflow'
import type * as acts from './activities'

const { chargeCard, sendReceipt, sendReviewMail } = proxyActivities<typeof acts>({
  startToCloseTimeout: '60 seconds',
  retry: {
    maximumAttempts: 4,
    initialInterval: '2s',
    backoffCoefficient: 2,
    nonRetryableErrorTypes: ['CardDeclinedError'],
  },
})

export async function postPurchaseFlow(orderId: string) {
  const receipt = await chargeCard(orderId)
  await sendReceipt(orderId, receipt)
  await sleep('2 days')
  await sendReviewMail(orderId)
  return receipt
}

특징: 재시도·타임아웃·예외 분류가 활동 옵션으로 분리. 워크플로 코드 자체는 깨끗하다.

8.2 Inngest (TS)

import { Inngest, NonRetriableError } from 'inngest'

const inngest = new Inngest({ id: 'shop' })

export const postPurchaseFlow = inngest.createFunction(
  {
    id: 'post-purchase',
    retries: 4,
  },
  { event: 'order.placed' },
  async ({ event, step }) => {
    const receipt = await step.run('charge', async () => {
      try {
        return await stripe.charge({ orderId: event.data.orderId })
      } catch (e) {
        if (e.code === 'card_declined') {
          throw new NonRetriableError('card declined')
        }
        throw e
      }
    })

    await step.run('send-receipt', () => mailer.send({
      to: event.data.orderId, body: receipt,
    }))

    await step.sleep('wait-2d', '2 days')

    await step.run('review-mail', () => mailer.send({
      to: event.data.orderId, template: 'review',
    }))

    return receipt
  }
)

특징: 재시도는 함수 옵션과 NonRetriableError 던지기. 이벤트 페이로드가 워크플로 입력.

8.3 Trigger.dev (TS)

import { task, wait, AbortTaskRunError } from '@trigger.dev/sdk/v3'

export const postPurchaseFlow = task({
  id: 'post-purchase-flow',
  retry: {
    maxAttempts: 4,
    factor: 2,
    minTimeoutInMs: 2000,
    maxTimeoutInMs: 30000,
  },
  run: async (payload: { orderId: string }) => {
    let receipt
    try {
      receipt = await stripe.charge({ orderId: payload.orderId })
    } catch (e) {
      if (e.code === 'card_declined') {
        throw new AbortTaskRunError('card declined')
      }
      throw e
    }

    await mailer.send({ to: payload.orderId, body: receipt })

    await wait.for({ days: 2 })

    await mailer.send({ to: payload.orderId, template: 'review' })

    return receipt
  },
})

특징: 단일 함수에 재시도 옵션. AbortTaskRunError로 재시도 중단. 매우 작은 표면적.

8.4 같은 일을 Go로 — Temporal 활동

Temporal의 강점인 멀티언어 예시:

// activities.go
package activities

import (
    "context"
    "errors"
)

type ChargeInput struct {
    OrderID string
}

type Receipt struct {
    PaymentID string
    Amount    int
}

func ChargeCard(ctx context.Context, input ChargeInput) (*Receipt, error) {
    r, err := stripeClient.Charge(ctx, input.OrderID)
    if err != nil {
        if errors.Is(err, stripe.ErrCardDeclined) {
            return nil, temporal.NewNonRetryableApplicationError(
                "card declined", "CardDeclinedError", err,
            )
        }
        return nil, err
    }
    return &Receipt{PaymentID: r.ID, Amount: r.Amount}, nil
}
// workflow.go
package workflows

import (
    "time"
    "go.temporal.io/sdk/workflow"
)

func PostPurchaseFlow(ctx workflow.Context, orderID string) (*Receipt, error) {
    ao := workflow.ActivityOptions{
        StartToCloseTimeout: time.Minute,
        RetryPolicy: &temporal.RetryPolicy{
            MaximumAttempts:    4,
            BackoffCoefficient: 2.0,
            InitialInterval:    2 * time.Second,
        },
    }
    ctx = workflow.WithActivityOptions(ctx, ao)

    var receipt Receipt
    if err := workflow.ExecuteActivity(ctx, ChargeCard, ChargeInput{OrderID: orderID}).Get(ctx, &receipt); err != nil {
        return nil, err
    }

    if err := workflow.Sleep(ctx, 48*time.Hour); err != nil {
        return nil, err
    }

    if err := workflow.ExecuteActivity(ctx, SendReviewMail, orderID).Get(ctx, nil); err != nil {
        return nil, err
    }
    return &receipt, nil
}

같은 의미. Temporal은 Go·Java·Python·.NET 등 멀티언어 통일된 모델이 강점이다.

8.5 정리 — 같지만 다르다

세 엔진 모두 "재시도 with 상태"를 깔끔하게 표현한다. 차이는 주변 환경이다.

  • Temporal: 재시도 옵션이 활동 단위, 정밀하고 표현력 있다. 코드는 가장 추상화돼 있다.
  • Inngest: 이벤트 발행 → 함수 자동 트리거. 도메인 이벤트와 결합도가 높은 팀에 자연스럽다.
  • Trigger.dev: 단일 파일에 전부 표현. Next.js 한 저장소 안에 들어간다.

9장 · AWS Step Functions·Azure Durable Functions·Cadence — 어디 자리잡고 있나

9.1 AWS Step Functions

오래된 클래식. JSON·YAML 기반의 ASL(Amazon States Language). 상태 머신을 선언적으로 그리는 모델. 강점: AWS 서비스 통합이 깊다(Lambda·SQS·SNS·DynamoDB가 거의 한 줄). 약점: 코드가 아니라 JSON이라 큰 워크플로의 유지보수가 어렵다, 상태 전환당 과금이 의외로 비싸진다.

2025 re:Invent에서 발표된 AWS Lambda Durable Functions가 게임을 바꿨다. Lambda 안에서 직접 결정적 재생 워크플로를 짤 수 있다 — Python 3.12+·Node.js 22+·TypeScript 5+ 지원. Step Functions의 대안이자 보완이다.

9.2 Azure Durable Functions

Microsoft의 정통파. C#·JS·Python·F#·PowerShell. Orchestrator 함수는 결정적이어야 하고, Activity 함수는 부작용을 일으킨다. Temporal과 같은 패러다임이지만 Azure Functions 인프라에 묶여 있다. 2026년 .NET Microsoft Agent Framework에 통합돼 에이전트 워크플로 표준 자리를 노린다.

9.3 Cadence

Temporal의 부모뻘. 우버에서 여전히 핵심으로 쓰이고 오픈소스로 살아있다. 새 프로젝트라면 Temporal을 고르는 게 거의 항상 맞다 — Temporal이 후속이고 활발하다.

9.4 자리 잡기

  • AWS/Azure에 깊게 묶인 팀: Step Functions · Lambda Durable · Azure Durable이 자연스럽다. 클라우드 락인을 받아들이는 대가로 운영 마찰이 없다.
  • 멀티 클라우드·온프레미스 의무·기존 코드 통합: Temporal·Restate·Inngest 자기호스팅.
  • 기존 우버 코드: Cadence 그대로 유지, 새 워크플로는 Temporal 검토.

10장 · 왜 2024 ~ 2026에 폭발했나 — AI 에이전트가 답이다

10.1 다섯 가지 폭발 요인

  1. AI 에이전트의 장시간 실행: LLM 호출 → 도구 호출 → 다음 LLM 호출이 30번 반복. 1시간짜리 작업이 흔하다. 서버가 한 번 죽으면 50달러가 증발.
  2. 토큰 비용의 비대칭: 컴퓨트 비용보다 LLM 호출 비용이 압도적이다. 같은 호출을 반복하지 않도록 체크포인팅이 필수.
  3. 빅테크의 채택: OpenAI Codex가 Temporal로 돌고, Cloudflare·Vercel·AWS가 자체 Durable 솔루션을 GA했다. 시장 신호가 강해졌다.
  4. 프레임워크가 1급 시민으로: LangGraph·Pydantic AI·OpenAI Agents SDK가 DE를 옵션이 아니라 표준으로 채택.
  5. 개발자 경험 개선: 5년 전에 비해 Inngest·Trigger.dev·Restate·DBOS 모두 "단일 함수·단일 파일·단일 인프라"로 입문 장벽을 낮췄다.

10.2 AI 에이전트 루프 — DE가 정확히 푸는 모양

@workflow
async function researchAgent(query: string) {
  let context = []
  for (let i = 0; i < 30; i++) {
    const plan = await ctx.run('llm-plan', () => llm.plan(query, context))
    if (plan.action === 'done') return plan.answer

    const result = await ctx.run(`tool-${i}`, () => tools.run(plan.tool, plan.args))
    context.push({ plan, result })

    // 가드: 비용 폭주 방지
    if (cost(context) > 5) throw new Error('budget exceeded')
  }
}
  • LLM 호출·도구 호출 각각 체크포인팅 → 30번 째에서 죽어도 1~29번 재호출 안 함.
  • for 루프가 그대로 워크플로 의도가 된다.
  • 비용 가드가 단순 if문으로 표현된다.

이걸 큐와 크론으로 짜본 사람만 안다 — 디버깅 지옥. DE는 이걸 함수 하나로 줄인다.


11장 · 도입 의사결정 트리

워크플로가 5분을 넘기는가?
├── 아니오 → DE 필요 없음. HTTP·gRPC로 충분.
└── 예
    ├── 외부 호출이 3개 이상인가?
    │   ├── 아니오 → 큐 + 멱등키로 충분할 수도.
    │   └── 예
    │       ├── 멀티언어가 필요한가?
    │       │   ├── 예 → Temporal · Restate
    │       │   └── 아니오 (TS 중심)
    │       │       ├── 이벤트 기반 도메인? → Inngest
    │       │       ├── Next.js 풀스택? → Trigger.dev
    │       │       └── Postgres 한 개로 끝내고 싶음? → DBOS
    │       └── 자기호스팅이 의무인가?
    │           ├── 예 → Temporal · Restate · Inngest · Trigger.dev · DBOS 자기호스팅
    │           └── 아니오 → 클라우드 SaaS 모두 OK
    └── AI 에이전트 루프인가?
        ├── 예 → Temporal · Inngest · Trigger.dev (이 셋이 가장 강함)
        └── 아니오 → 일반 비교 매트릭스로 선택

추가 가지치기:

  • 이미 AWS·Azure에 깊게 묶임? → Lambda Durable Functions · Azure Durable Functions를 먼저 본다.
  • Cadence를 이미 쓰고 있음? → 신규 워크플로는 Temporal 검토.
  • 데이터 주권·규제(GDPR·HIPAA)? → 모두 자기호스팅 가능, Restate가 운영 부담 가장 작다.
  • 하루 100건 미만의 작은 워크플로? → DE 도입 비용이 이득보다 크다. DB 컬럼·간단한 큐로 충분.

12장 · 안티패턴

12.1 워크플로 안에서 직접 시간·랜덤·외부 호출 부르기

결정적 모델(Temporal·Restate) 워크플로에서 Date.now()·Math.random()·fetch()를 그대로 부르면 재생 시 다른 값이 나와 깨진다. 반드시 엔진 API(ctx.now()·ctx.random()·ctx.run())로 감싼다.

12.2 모든 함수를 활동으로 분할

활동(step)은 체크포인팅 비용이 있다. 1ms짜리 순수 함수까지 활동으로 만들면 워크플로가 100배 느려진다. 외부 부작용·외부 호출·재시도 단위가 활동의 단위다.

12.3 DE 한 개로 ETL·Kafka·DAG 다 처리

DE는 트랜잭셔널·장시간·상태 있는 워크플로용. 데이터 파이프라인은 Airflow·dbt·Spark가 적합. 두 가지를 섞으면 둘 다 어색해진다.

12.4 자기호스팅을 운영 인력 없이

Temporal 클러스터를 EC2에 띄우고 끝. 6개월 뒤 Cassandra 디스크 가득 차고 백업이 없다. DE 자기호스팅은 무료가 아니다. 운영 비용을 클라우드 SaaS 비용과 비교해야 한다.

12.5 워크플로 코드 변경에 버저닝 없이

이미 돌고 있는 워크플로의 코드를 바꾸면, 옛날 워커가 새 이력을 만나 깨진다. 모든 DE 엔진은 버저닝 API(Patched·Workflow.versioning·Side Effects)를 제공한다. 변경마다 버전을 매겨라.

12.6 AI 에이전트에 무제한 도구·예산

LLM 호출·도구 호출이 결정적이면 좋지만, 무한 루프는 결정적이라도 비싸다. 도구 화이트리스트와 비용 가드가 워크플로 안에 들어가야 한다.

12.7 멱등키 없이 외부 API 호출

활동이 재시도 가능하다는 건 외부 API도 같은 호출을 두 번 받을 수 있다는 뜻. idempotency key를 입력에 포함하고, 외부 API가 지원하면 헤더(Idempotency-Key)로 같이 보낸다. Stripe·SendGrid·Slack은 다 지원한다.


에필로그 — "엔진을 고른다"가 아니라 "워크플로 사고를 산다"

Durable Execution 엔진의 진짜 가치는 도구가 아니다. 워크플로를 코드로 보는 사고다. 큐와 크론으로 짜는 백엔드와, 함수 한 개로 짜는 백엔드는 사고 모형 자체가 다르다.

도입 체크리스트:

  • 5분 넘는 워크플로 후보 5개를 적는다.
  • 각 워크플로의 외부 호출 수·실패 빈도·비용을 추정한다.
  • 사용할 언어 SDK를 정한다 (Go·Java까지 필요? TS만? Python까지?).
  • 자기호스팅 의무 여부를 확인한다 (데이터 주권·규제).
  • 6개월 후 청구서를 시뮬레이션 — 실행 횟수·동시 실행·체크포인트.
  • 1차 엔진 1개를 골라 PoC 2주 진행, 하나의 진짜 워크플로를 옮긴다.
  • 결정적 재생 모델이면 버저닝 정책을 첫날에 정한다.
  • 활동·단계의 멱등키 표준을 팀 컨벤션으로 박는다.
  • 옵저버빌리티 한 화면(웹 UI 또는 OTel 대시보드)을 합의한다.
  • AI 에이전트가 있다면 비용 가드와 도구 화이트리스트를 워크플로 안에 넣는다.

안티패턴 짧은 목록:

  • 워크플로 안에서 직접 시간·랜덤·HTTP 부르기 — 결정적 모델이면 깨진다.
  • 1ms 함수까지 활동으로 분할 — 체크포인팅 비용이 비싸진다.
  • DE로 ETL까지 다 처리 — 데이터 파이프라인은 다른 도구.
  • 자기호스팅을 인력 없이 — 보이지 않는 운영 비용이 무겁다.
  • 워크플로 변경에 버저닝 없이 — 살아있는 실행을 깨뜨린다.
  • AI 에이전트에 무제한 권한 — 비용·보안 둘 다 폭주.
  • 외부 API에 멱등키 없이 호출 — 재시도가 부작용 두 번을 만든다.

다음 글 예고

다음 글에서는 Temporal 자기호스팅 프로덕션 가이드 — Cassandra와 Postgres 백엔드 선택, 멀티 클러스터·HA·블루-그린 워커 배포, Worker Versioning과 워크플로 마이그레이션, 비용·옵저버빌리티까지 한 회사가 6개월 동안 부순 것들을 정리한다. Temporal Cloud 청구서가 월 5,000달러를 넘기 시작했다면 그 글이 다음 결정의 기준이 된다.

Durable Execution은 단일 도구의 선택이 아니다. 함수 한 개로 시간을 다루는 사고다. 그 사고를 사면 도구는 따라온다.


참고 / References

Temporal

Restate

Inngest

Trigger.dev

DBOS

AWS · Azure · Cadence · Survey

Patterns · AI Agents · Comparisons