리플레이 모델: AWS Lambda Durable Functions가 실제로 작동하는 방식

발행: (2025년 12월 3일 오전 09:27 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

핵심 원칙

핸들러 함수는 매 호출 시 처음부터 다시 실행되지만, 완료된 작업은 체크포인트에서 캐시된 결과를 반환하므로 다시 실행되지 않습니다.

async function processOrder(event: any, ctx: DurableContext) {
  const order = await ctx.step('create-order', async () => {
    console.log('Creating order...');
    return { orderId: '123', total: 50 };
  });

  const payment = await ctx.step('process-payment', async () => {
    console.log('Processing payment...');
    return { transactionId: 'txn-456', status: 'success' };
  });

  await ctx.wait({ seconds: 300 }); // Wait 5 minutes

  const notification = await ctx.step('send-notification', async () => {
    console.log('Sending notification...');
    return { sent: true };
  });

  return { order, payment, notification };
}

호출 흐름

Invocation 1 (t = 0 s)

Creating order...
Processing payment...
[Checkpoint: create-order completed]
[Checkpoint: process-payment completed]
[Function terminates – waiting 5 minutes]

Invocation 2 (t = 300 s, after wait completes)

[REPLAY MODE: Skipping create-order – returning cached result]
[REPLAY MODE: Skipping process-payment – returning cached result]
[EXECUTION MODE: Running send-notification]
Sending notification...
[Checkpoint: send-notification completed]
[Function completes]

함수는 매번 처음부터 시작하지만, 이전에 완료된 단계는 건너뛰므로 로그는 한 번만 나타납니다.

실행 모드: 비밀 소스

SDK는 두 가지 자동 모드로 동작합니다:

  • ExecutionMode – 작업을 처음 실행할 때; 결과가 체크포인트에 저장되고, 로그가 출력되며, 부수 효과가 발생합니다.
  • ReplayMode – 이전에 완료된 작업을 재생할 때; 캐시된 결과가 즉시 반환되고, 로그는 억제되며, 부수 효과가 발생하지 않습니다.

SDK는 아직 완료되지 않은 작업에 도달하면 ReplayMode에서 ExecutionMode로 전환합니다.

체크포인트 작동 방식

각 작업은 메타데이터와 결과를 저장하는 체크포인트를 생성합니다:

{
  "operationId": "2",
  "operationType": "STEP",
  "operationName": "process-payment",
  "status": "SUCCEEDED",
  "result": {
    "transactionId": "txn-456",
    "status": "success"
  }
}

함수가 재시작될 때 SDK는 모든 체크포인트를 로드하고, 작업 ID별로 인덱싱한 뒤, 완료된 작업에 대해 캐시된 결과를 반환하고 새로운 작업은 정상적으로 실행합니다.

결정론성 요구 사항

Replay를 사용하려면 코드가 결정론적이어야 합니다 – 매 호출마다 동일한 순서로 동일한 작업 흐름이 발생해야 합니다.

결정론성을 깨뜨리는 경우

// ❌ 랜덤 제어 흐름
if (Math.random() > 0.5) {
  await ctx.step('optional-step', async () => doSomething());
}

// ❌ 시간 기반 분기
const isWeekend = new Date().getDay() >= 5;
if (isWeekend) {
  await ctx.step('weekend-task', async () => doWeekendWork());
}

// ❌ 외부 가변 상태
let counter = 0;
await ctx.step('step1', async () => {
  counter++; // Replay 중에는 증가하지 않음!
  return counter;
});

이러한 패턴은 실행 간 작업 순서가 일치하지 않게 만들어 Replay 일관성 위반을 초래합니다.

결정론적인 코드 작성 방법

비결정론적 값은 모두 단계 내부에서 캡처합니다:

// ✅ 단계 안에서 랜덤 값 캡처
const randomId = await ctx.step('generate-id', async () => {
  return crypto.randomUUID(); // 한 번만 실행되고 Replay에서 캐시됨
});

// ✅ 단계 안에서 타임스탬프 캡처
const timestamp = await ctx.step('get-timestamp', async () => {
  return Date.now(); // 모든 Replay에서 동일한 타임스탬프
});

// ✅ 이벤트 데이터를 이용한 제어 흐름 (결정론적)
if (event.shouldProcess) {
  await ctx.step('process', async () => doWork());
}

// ✅ 시간 기반 결정도 단계 안에서 캡처
const isWeekend = await ctx.step('check-day', async () => {
  return new Date().getDay() >= 5;
});
if (isWeekend) {
  await ctx.step('weekend-task', async () => doWeekendWork());
}

Replay 일관성 검증

SDK는 각 호출이 동일한 작업 순서를 따르는지 검증합니다:

  • 작업 유형 (STEP, WAIT, INVOKE)
  • 작업 이름 (사용자 지정 식별자)
  • 작업 위치 (연속 순서)

예시 검증 오류:

Replay consistency violation: Expected operation 'process-payment' 
of type STEP at position 2, but found operation 'send-email' of type STEP

이러한 조기 감지는 비결정론적 버그를 빠르게 찾아낼 수 있게 도와줍니다.

전체 예시: Replay가 적용된 주문 처리

async function processOrder(event: any, ctx: DurableContext) {
  ctx.logger.info('Order processing started', { orderId: event.orderId });

  // Step 1: 재고 검증
  const inventory = await ctx.step('check-inventory', async () => {
    ctx.logger.info('Checking inventory');
    const response = await fetch(`https://api.inventory.com/check`, {
      method: 'POST',
      body: JSON.stringify({ items: event.items })
    });
    return response.json();
  });

  if (!inventory.available) {
    ctx.logger.warn('Out of stock', { missing: inventory.missing });
    return { status: 'out-of-stock' };
  }

  // Step 2: 결제 처리
  const payment = await ctx.step('process-payment', async () => {
    ctx.logger.info('Processing payment', { amount: inventory.total });
    const response = await fetch(`https://api.payments.com/charge`, {
      method: 'POST',
      body: JSON.stringify({
        customerId: event.customerId,
        amount: inventory.total
      })
    });
    return response.json();
  });

  // Step 3: 창고 확인 대기 (5분 제한)
  ctx.logger.info('Waiting for warehouse confirmation');
  const confirmation = await ctx.waitForCallback(
    'warehouse-confirm',
    async (callbackId) => {
      // 콜백 ID를 창고 시스템에 전송
      await fetch(`https://api.warehouse.com/notify`, {
        method: 'POST',
        body: JSON.stringify({ orderId: event.orderId, callbackId })
      });
    },
    { timeout: { seconds: 300 } }
  );

  // Step 4: 알림 전송
  const notification = await ctx.step('send-notification', async () => {
    ctx.logger.info('Sending notification');
    // 예: 이메일 서비스 호출
    await fetch(`https://api.email.com/send`, {
      method: 'POST',
      body: JSON.stringify({
        to: event.customerEmail,
        subject: 'Your order is confirmed',
        body: `Order ${event.orderId} is confirmed.`
      })
    });
    return { sent: true };
  });

  return { inventory, payment, confirmation, notification };
}

이 워크플로에서는 각 단계가 체크포인트에 저장됩니다. 함수가 일시 중지(예: 창고 콜백 대기)되면 이후 호출은 이전 단계들을 즉시 재생하여 부수 효과가 정확히 한 번만 실행되도록 보장합니다.

Back to Blog

관련 글

더 보기 »

TypeScript에서 AWS Lambda Durable Functions 테스트

테스팅 라이브러리 SDK는 다양한 시나리오에 대한 테스트 도구를 제공합니다: LocalDurableTestRunner는 함수를 인‑프로세스 방식으로 시뮬레이션된 체크포인트와 함께 실행합니다.

core.async: 심층 탐구 — 온라인 밋업

이벤트 개요: 12월 10일 GMT+1 기준 18:00에 Health Samurai가 온라인 밋업 “core.async: Deep Dive”를 주최합니다. 이번 강연은 clojure.core의 내부를 파헤칩니다....

모뎀의 복수

첫 번째 연결 1994년 겨울, 홍콩의 작은 아파트에서, 14세 소년이 US Robotics Sportster 14,400 Fax Modem을 연결했다.