

# Lambda 지속성 함수 모범 사례
<a name="durable-best-practices"></a>

지속성 함수는 기존 Lambda 함수와 다른 패턴이 필요한 재생 기반 실행 모델을 사용합니다. 다음 모범 사례에 따라 안정적이면서 비용 효율적인 워크플로를 빌드합니다.

## 결정론적 코드 작성
<a name="durable-determinism"></a>

재생 도중에 함수는 처음부터 실행되며 원래 실행과 동일한 실행 경로를 따라야 합니다. 지속성 작업 외부의 코드는 결정론적이어야 하며, 동일한 입력이 주어지면 동일한 결과를 생성해야 합니다.

**비결정론적 작업을 단계로 래핑합니다.**
+ 난수 생성 및 UUID
+ 현재 시간 또는 타임스탬프
+ 외부 API 직접 호출 및 데이터베이스 쿼리
+ 파일 시스템 작업

------
#### [ TypeScript ]

```
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js';
import { randomUUID } from 'crypto';

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // Generate transaction ID inside a step
    const transactionId = await context.step('generate-transaction-id', async () => {
      return randomUUID();
    });
    
    // Use the same ID throughout execution, even during replay
    const payment = await context.step('process-payment', async () => {
      return processPayment(event.amount, transactionId);
    });
    
    return { statusCode: 200, transactionId, payment };
  }
);
```

------
#### [ Python ]

```
from aws_durable_execution_sdk_python import durable_execution, DurableContext
import uuid

@durable_execution
def handler(event, context: DurableContext):
    # Generate transaction ID inside a step
    transaction_id = context.step(
        lambda _: str(uuid.uuid4()),
        name='generate-transaction-id'
    )
    
    # Use the same ID throughout execution, even during replay
    payment = context.step(
        lambda _: process_payment(event['amount'], transaction_id),
        name='process-payment'
    )
    
    return {'statusCode': 200, 'transactionId': transaction_id, 'payment': payment}
```

------

**중요**  
전역 변수 또는 클로저를 사용하여 단계 간에 상태를 공유하지 말고, 반환 값을 통해 데이터를 전달해야 합니다. 단계가 캐시된 결과를 반환하지만 전역 변수가 재설정되므로 재생 도중 전역 상태가 중단됩니다.

**클로저 변형 지양:** 클로저에 캡처된 변수는 재생 도중 변형(mutation)이 손실될 수 있습니다. 단계는 캐시된 결과를 반환하지만 단계 외부의 변수 업데이트는 재생되지 않습니다.

------
#### [ TypeScript ]

```
// ❌ WRONG: Mutations lost on replay
export const handler = withDurableExecution(async (event, context) => {
  let total = 0;
  
  for (const item of items) {
    await context.step(async () => {
      total += item.price; // ⚠️ Mutation lost on replay!
      return saveItem(item);
    });
  }
  
  return { total }; // Inconsistent value!
});

// ✅ CORRECT: Accumulate with return values
export const handler = withDurableExecution(async (event, context) => {
  let total = 0;
  
  for (const item of items) {
    total = await context.step(async () => {
      const newTotal = total + item.price;
      await saveItem(item);
      return newTotal; // Return updated value
    });
  }
  
  return { total }; // Consistent!
});

// ✅ EVEN BETTER: Use map for parallel processing
export const handler = withDurableExecution(async (event, context) => {
  const results = await context.map(
    items,
    async (ctx, item) => {
      await ctx.step(async () => saveItem(item));
      return item.price;
    }
  );
  
  const total = results.getResults().reduce((sum, price) => sum + price, 0);
  return { total };
});
```

------
#### [ Python ]

```
# ❌ WRONG: Mutations lost on replay
@durable_execution
def handler(event, context: DurableContext):
    total = 0
    
    for item in items:
        context.step(
            lambda _: save_item_and_mutate(item, total),  # ⚠️ Mutation lost on replay!
            name=f'save-item-{item["id"]}'
        )
    
    return {'total': total}  # Inconsistent value!

# ✅ CORRECT: Accumulate with return values
@durable_execution
def handler(event, context: DurableContext):
    total = 0
    
    for item in items:
        total = context.step(
            lambda _: save_item_and_return_total(item, total),
            name=f'save-item-{item["id"]}'
        )
    
    return {'total': total}  # Consistent!

# ✅ EVEN BETTER: Use map for parallel processing
@durable_execution
def handler(event, context: DurableContext):
    def process_item(ctx, item):
        ctx.step(lambda _: save_item(item))
        return item['price']
    
    results = context.map(items, process_item)
    total = sum(results.get_results())
    
    return {'total': total}
```

------

## 멱등성을 고려한 설계
<a name="durable-idempotency"></a>

재시도 또는 재생으로 인해 작업이 여러 번 실행될 수 있습니다. 비멱등성 작업으로 인해 고객에게 두 번 요금을 부과하거나 여러 번 이메일을 보내는 등의 중복과 관련된 부작용이 발생합니다.

**멱등성 토큰 사용:** 단계 내에 토큰을 생성하고 외부 API 직접 호출에 포함하여 중복 작업을 방지합니다.

------
#### [ TypeScript ]

```
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js';

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // Generate idempotency token once
    const idempotencyToken = await context.step('generate-idempotency-token', async () => {
      return crypto.randomUUID();
    });
    
    // Use token to prevent duplicate charges
    const charge = await context.step('charge-payment', async () => {
      return paymentService.charge({
        amount: event.amount,
        cardToken: event.cardToken,
        idempotencyKey: idempotencyToken
      });
    });
    
    return { statusCode: 200, charge };
  }
);
```

------
#### [ Python ]

```
from aws_durable_execution_sdk_python import durable_execution, DurableContext
import uuid

@durable_execution
def handler(event, context: DurableContext):
    # Generate idempotency token once
    idempotency_token = context.step(
        lambda _: str(uuid.uuid4()),
        name='generate-idempotency-token'
    )
    
    # Use token to prevent duplicate charges
    def charge_payment(_):
        return payment_service.charge(
            amount=event['amount'],
            card_token=event['cardToken'],
            idempotency_key=idempotency_token
        )
    
    charge = context.step(charge_payment, name='charge-payment')
    
    return {'statusCode': 200, 'charge': charge}
```

------

**최대 1회 시맨틱 사용:** 복제해서는 안 되는 중요 작업(재무 거래, 재고 차감)의 경우 최대 1회 실행 모드를 구성합니다.

------
#### [ TypeScript ]

```
// Critical operation that must not duplicate
await context.step('deduct-inventory', async () => {
  return inventoryService.deduct(event.productId, event.quantity);
}, {
  executionMode: 'AT_MOST_ONCE_PER_RETRY'
});
```

------
#### [ Python ]

```
# Critical operation that must not duplicate
context.step(
    lambda _: inventory_service.deduct(event['productId'], event['quantity']),
    name='deduct-inventory',
    config=StepConfig(execution_mode='AT_MOST_ONCE_PER_RETRY')
)
```

------

**데이터베이스 멱등성:** check-before-write 패턴, 조건부 업데이트 또는 업서트 작업을 사용하여 레코드 중복을 방지합니다.

## 효율적으로 상태 관리
<a name="durable-state-management"></a>

모든 체크포인트는 영구 스토리지에 상태를 저장합니다. 상태 객체의 크기가 크면 비용이 증가하고 체크포인트 속도가 느려지며 성능에 영향을 미칩니다. 필수 워크플로 조정 데이터만 저장해야 합니다.

**상태를 최소한으로 유지합니다.**
+ 전체 객체가 아닌 ID와 참조 저장
+ 필요에 따라 단계 내에서 세부 데이터 가져오기
+ 대용량 데이터에 Amazon S3 또는 DynamoDB 사용, 상태의 참조 전달
+ 단계 사이에 큰 페이로드 전달 방지

------
#### [ TypeScript ]

```
import { withDurableExecution, DurableContext } from '@aws/durable-execution-sdk-js';

export const handler = withDurableExecution(
  async (event: any, context: DurableContext) => {
    // Store only the order ID, not the full order object
    const orderId = event.orderId;
    
    // Fetch data within each step as needed
    await context.step('validate-order', async () => {
      const order = await orderService.getOrder(orderId);
      return validateOrder(order);
    });
    
    await context.step('process-payment', async () => {
      const order = await orderService.getOrder(orderId);
      return processPayment(order);
    });
    
    return { statusCode: 200, orderId };
  }
);
```

------
#### [ Python ]

```
from aws_durable_execution_sdk_python import durable_execution, DurableContext

@durable_execution
def handler(event, context: DurableContext):
    # Store only the order ID, not the full order object
    order_id = event['orderId']
    
    # Fetch data within each step as needed
    context.step(
        lambda _: validate_order(order_service.get_order(order_id)),
        name='validate-order'
    )
    
    context.step(
        lambda _: process_payment(order_service.get_order(order_id)),
        name='process-payment'
    )
    
    return {'statusCode': 200, 'orderId': order_id}
```

------

## 효과적인 단계 설계
<a name="durable-step-design"></a>

단계는 지속성 함수의 기본 작업 단위입니다. 잘 설계된 단계를 통해 워크플로를 더 쉽게 이해하고 디버깅하고 유지 관리할 수 있습니다.

**단계 설계 원칙:**
+ **설명이 포함된 이름 사용** - 로그와 오류를 쉽게 이해할 수 있도록 `step1` 대신 `validate-order` 같은 이름을 사용합니다.
+ **이름을 정적 상태로 유지** - 타임스탬프 또는 임의 값이 포함된 동적 이름은 사용하지 않습니다. 재생을 위해 단계 이름은 결정론적이어야 합니다.
+ **세분화 균형 조정** - 복잡한 작업을 보다 집중된 여러 단계로 나누되 체크포인트 오버헤드를 높일 수 있으므로 지나치게 작은 단계는 생성하지 않습니다.
+ **그룹 관련 작업** - 함께 성공 또는 실패해야 하는 작업은 동일한 단계에 속합니다.

## 효율적으로 대기 작업 사용
<a name="durable-wait-operations"></a>

대기 작업은 리소스를 소비하거나 비용을 발생시키지 않고 실행을 일시 중지합니다. Lambda를 계속 실행하는 대신 대기 작업을 사용하세요.

**시간 기반 대기:** `setTimeout` 또는 `sleep` 대신 지연에 `context.wait()`을 사용합니다.

**외부 콜백:** 외부 시스템을 기다릴 때 `context.waitForCallback()`을 사용합니다. 무기한 대기를 방지하려면 항상 제한 시간을 설정합니다.

**폴링:** 지수 백오프와 함께 `context.waitForCondition()`을 사용하여 과부하 없이 외부 서비스를 폴링합니다.

------
#### [ TypeScript ]

```
// Wait 24 hours without cost
await context.wait({ seconds: 86400 });

// Wait for external callback with timeout
const result = await context.waitForCallback(
  'external-job',
  async (callbackId) => {
    await externalService.submitJob({
      data: event.data,
      webhookUrl: `https://api.example.com/callbacks/${callbackId}`
    });
  },
  { timeout: { seconds: 3600 } }
);
```

------
#### [ Python ]

```
# Wait 24 hours without cost
context.wait(86400)

# Wait for external callback with timeout
result = context.wait_for_callback(
    lambda callback_id: external_service.submit_job(
        data=event['data'],
        webhook_url=f'https://api.example.com/callbacks/{callback_id}'
    ),
    name='external-job',
    config=WaitForCallbackConfig(timeout_seconds=3600)
)
```

------

## 추가 고려 사항
<a name="durable-additional-considerations"></a>

**오류 처리:** 네트워크 제한 시간 및 속도 제한과 같은 일시적 실패가 발생하면 재시도합니다. 잘못된 입력 또는 인증 오류와 같은 영구적 실패는 재시도하지 않습니다. 적절한 최대 시도 횟수 및 백오프 속도로 재시도 전략을 구성합니다. 자세한 예제는 [오류 처리 및 재시도](durable-execution-sdk-retries.md)를 참조하세요.

**성능:** 전체 페이로드 대신 참조를 저장하여 체크포인트 크기를 최소화합니다. `context.parallel()` 및 `context.map()`을 사용하여 독립적인 작업을 동시에 실행합니다. 관련 작업을 배치로 처리하여 체크포인트 오버헤드를 줄이세요.

**버전 관리:** 버전 번호 또는 별칭이 있는 함수를 간접 호출하여 실행을 특정 코드 버전에 고정합니다. 새 코드 버전이 이전 버전의 상태를 처리할 수 있어야 합니다. 재생이 중단되는 방식으로 단계의 이름을 바꾸거나 동작을 변경하지 않습니다.

**직렬화:** 작업 입력 및 결과에 JSON 호환 유형을 사용합니다. 지속성 작업에 전달하기 전에 날짜를 ISO 문자열로 변환하고 사용자 지정 객체를 일반 객체로 변환합니다.

**모니터링:** 실행 ID 및 단계 이름을 포함하여 구조화된 로깅을 활성화합니다. 오류 발생률 및 실행 기간에 대한 CloudWatch 경보를 설정합니다. 추적을 사용하여 병목 현상을 식별합니다. 자세한 지침은 [모니터링 및 디버깅](durable-monitoring.md)을 참조하세요.

**테스트:** 해피 패스, 오류 처리 및 재생 동작을 테스트합니다. 콜백 및 대기에 대한 제한 시간 시나리오를 테스트합니다. 로컬 테스트를 사용하여 반복 시간을 줄입니다. 자세한 지침은 [지속성 함수 테스트](durable-testing.md)를 참조하세요.

**피해야 할 일반적인 실수:** `context.step()` 호출을 중첩하지 않고, 대신 하위 컨텍스트를 사용합니다. 비결정론적 작업을 단계로 래핑합니다. 항상 콜백에 대한 제한 시간을 설정합니다. 단계 세분성과 체크포인트 오버헤드 사이의 균형을 맞춥니다. 큰 객체 대신 참조를 상태로 저장합니다.

## 추가 리소스
<a name="durable-additional-resources"></a>
+ [Python SDK 설명서](https://github.com/aws/aws-durable-execution-sdk-python/tree/main/docs) - 전체 API 참조, 테스트 패턴 및 고급 예제
+ [TypeScript SDK 설명서](https://github.com/aws/aws-durable-execution-sdk-js/tree/main/docs) - 전체 API 참조, 테스트 패턴 및 고급 예제