예시와 사용 사례
예시와 사용 사례 (Examples and use cases)
Lambda durable functions는 steps, waits 같은 내구성 있는 연산(durable operations)을 사용해 장애 허용이 있는 다단계 애플리케이션을 구축할 수 있게 해줘요. 자동 체크포인트와 체크포인트-재생(checkpoint-replay) 모델(실패 시 처음부터 다시 시작하되 완료된 체크포인트는 건너뜀) 덕분에 함수는 진행 상황을 잃지 않고 실패에서 복구해 실행을 재개할 수 있어요.
참고 SDK API 레퍼런스, 퀵스타트 튜토리얼, 언어별 가이드, 추가 워크플로 패턴은 AWS Durable Execution SDK Developer Guide를 참고하세요.
본문
짧게 끝나는 장애 허용 프로세스
durable functions로 보통 몇 분 안에 완료되는 신뢰할 수 있는 작업을 구축할 수 있어요. 이런 프로세스는 오래 걸리는 워크플로보다 짧지만, 분산 시스템 전반에 걸친 자동 체크포인트와 장애 허용의 이점은 여전히 누릴 수 있어요. durable functions는 개별 서비스 호출이 실패해도 복잡한 오류 처리나 상태 관리 코드 없이 다단계 프로세스가 성공적으로 완료되도록 도와줘요.
흔한 시나리오로는 호텔 예약 시스템, 레스토랑 예약 플랫폼, 라이드셰어링 차량 호출, 이벤트 티켓 구매, SaaS 구독 업그레이드가 있어요. 이 시나리오들은 공통된 특성을 공유해요. — 함께 완료되어야 하는 여러 서비스 호출, 일시적 실패에 대한 자동 재시도 필요성, 분산 시스템 전반에 걸친 일관된 상태 유지 요구죠.
마이크로서비스 간 분산 트랜잭션
실패 시 자동 롤백을 지원하면서 여러 서비스에 걸쳐 결제, 재고, 배송을 조정해요. 각 서비스 연산이 단계(step)로 감싸져 있어서 서비스가 실패해도 트랜잭션이 어느 지점에서든 복구할 수 있어요.
TypeScript:
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
export const handler = withDurableExecution(
async (event: any, context: DurableContext) => {
const { orderId, amount, items } = event;
// Reserve inventory across multiple warehouses
const inventory = await context.step("reserve-inventory", async () => {
return await inventoryService.reserve(items);
});
// Process payment
const payment = await context.step("process-payment", async () => {
return await paymentService.charge(amount);
});
// Create shipment
const shipment = await context.step("create-shipment", async () => {
return await shippingService.createShipment(orderId, inventory);
});
return { orderId, status: 'completed', shipment };
}
);
Python:
from aws_durable_execution_sdk_python import DurableContext, durable_execution
@durable_execution
def lambda_handler(event, context: DurableContext):
order_id = event['orderId']
amount = event['amount']
items = event['items']
# Reserve inventory across multiple warehouses
inventory = context.step(
lambda _: inventory_service.reserve(items),
name='reserve-inventory'
)
# Process payment
payment = context.step(
lambda _: payment_service.charge(amount),
name='process-payment'
)
# Create shipment
shipment = context.step(
lambda _: shipping_service.create_shipment(order_id, inventory),
name='create-shipment'
)
return {
'orderId': order_id,
'status': 'completed',
'shipment': shipment
}
여러 단계로 구성된 주문 처리
서점 API가 여러 단계를 조합해 주문을 처리해요. 이 예시는 durable function에서 context.step()으로 다단계 주문 흐름을 오케스트레이션하는 방법을 보여주며, 각 단계가 추적되고 체크포인트화돼요. 전체 예시는 하나의 오케스트레이션 흐름 안에서 waits, callbacks, conditionals를 사용하는 방법을 보여줘요.
오래 걸리는 프로세스
표준 Lambda 15분 제한을 넘어서는 확장 실행이 필요한 워크로드에는 durable functions를 사용하세요. durable functions는 진행 상황을 보존하는 자동 체크포인트, 실행을 일시 중지하는 waits, 이전 상태에서 재개하는 체크포인트-재생을 통해 오래 걸리는 프로세스를 지원해요. 이로써 실행 중인 프로세스 제어, 사례별 로직이 있는 의사 결정 트리, 몇 시간·며칠·최대 1년까지 걸리는 인간 개입(human-in-the-loop) 워크플로를 실행할 수 있어요.
인간 개입 승인 (Human-in-the-loop approvals)
이 예시는 고객 서비스 승인 워크플로를 보여줘요. 워크플로는 콜백 ID가 포함된 검증 URL을 담은 이메일을 고객에게 보낸 다음, 고객이 링크를 클릭할 때까지 기다려요. 체크포인트 덕분에 워크플로는 중단된 지점에서 재개할 수 있어요.
다단계 데이터 파이프라인
데이터 파이프라인은 추출(extract), 변환(transform), 적재(load)(ETL) 같은 여러 단계를 순차적으로 실행하는 경우가 많아요. 파이프라인은 보통 대량의 데이터를 이동하며 오랫동안 실행될 수 있어요. 이 예시는 데이터가 이용 가능해지면 실시간으로 처리하고, 피크가 아닌 시간에만 데이터를 처리하며, 작업을 예약하고, 실패한 단계를 자동으로 재시도하는 wait-for-callback과 조건부 로직이 있는 ETL 파이프라인을 보여줘요. 다음 TypeScript 코드 예시는 단계 사이에 5분 대기, 컴퓨팅 작업의 예약 타이밍, 오류 처리가 있는 이 다단계 프로세스를 보여줘요. 다음 단계를 사용해요. 1단계 — 소스에서 데이터 추출. 2단계 — 데이터를 필요한 형식으로 변환. 3단계 — 데이터를 적재하려고 오전 2시(하드코딩)까지 대기. 4단계 — 검증 및 마무리. 각 단계가 체크포인트화되도록 step으로 감싸져 있고, 추출과 변환 사이에 5분 대기가 있어요.
TypeScript:
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
export const handler = withDurableExecution(
async (event: any, context: DurableContext) => {
const { datasetId, batchSize = 100 } = event;
// Stage 1: Extract data from source
const extracted = await context.step("extract-data", async () => {
const records = await sourceDatabase.extractRecords(datasetId);
return { recordCount: records.length, records };
});
// Wait 5 minutes to respect source system rate limits
await context.wait({ seconds: 300 });
// Stage 2: Transform data in batches
const transformed = await context.step("transform-data", async () => {
const batches = chunkArray(extracted.records, batchSize);
const results = [];
for (const batch of batches) {
const transformed = await transformService.processBatch(batch);
results.push(transformed);
}
return { batchCount: batches.length, results };
});
// Wait until off-peak hours (e.g., 2 AM)
const now = new Date();
const targetHour = 2;
const msUntilTarget = calculateMsUntilHour(now, targetHour);
await context.wait({ seconds: Math.floor(msUntilTarget / 1000) });
// Stage 3: Load data to destination
const loaded = await context.step("load-data", async () => {
let loadedCount = 0;
for (const result of transformed.results) {
await destinationDatabase.loadBatch(result);
loadedCount += result.length;
}
return { loadedCount };
});
// Stage 4: Verify and finalize
const verified = await context.step("verify-pipeline", async () => {
const verification = await destinationDatabase.verifyRecords(datasetId);
await pipelineService.markComplete(datasetId, verification);
return verification;
});
return {
datasetId,
recordsProcessed: extracted.recordCount,
batchesProcessed: transformed.batchCount,
recordsLoaded: loaded.loadedCount,
verified: verified.success
};
}
);
각 단계는 step으로 감싸져 체크포인트를 만들기 때문에, 중단되면 파이프라인이 어느 단계에서든 재개할 수 있어요. 추출과 변환 사이의 5분 대기는 컴퓨팅 리소스를 소모하지 않으면서 소스 시스템의 속도 제한을 존중하고, 오전 2시까지의 대기는 비용이 많이 드는 적재 작업을 피크 시간이 아닐 때 예약해요.
참고
new Date()호출과calculateMsUntilHour()함수는 step 밖에 있으므로 재생 중에 다시 실행돼요. 재생 사이에 일관적이어야 하는 시간 기반 작업은 step 안에서 타임스탬프를 계산하거나 대기 시간(체크포인트화됨)에만 사용하세요.
고급 패턴
durable functions로 여러 내구성 연산, 병렬 실행, 배열 처리, 조건부 로직, 폴링을 결합한 복잡한 다단계 애플리케이션을 구축할 수 있어요. 이런 패턴들은 장애 허용과 자동 복구를 유지하면서 많은 작업을 조정하는 정교한 애플리케이션을 구축할 수 있게 해줘요.
고급 패턴은 단순한 순차적 step을 넘어서요. parallel()로 연산을 동시에 실행하고, map()으로 배열을 처리하고, waitForCondition()으로 외부 조건을 기다리고, 이런 원시형들을 결합해 신뢰할 수 있는 애플리케이션을 만들 수 있어요. 각 내구성 연산은 자체 체크포인트를 만들기 때문에 중단되어도 애플리케이션이 어느 지점에서든 복구할 수 있어요.
사용자 온보딩 프로세스
재시도 처리가 있는 등록, 이메일 검증, 프로필 설정, 초기 구성을 사용자에게 안내해요. 이 예시는 순차적 step, 콜백, 조건부 로직을 결합해 완전한 온보딩 프로세스를 만들어요.
TypeScript:
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
export const handler = withDurableExecution(
async (event: any, context: DurableContext) => {
const { userId, email } = event;
// Step 1: Create user account
const user = await context.step("create-account", async () => {
return await userService.createAccount(userId, email);
});
// Step 2: Send verification email
await context.step("send-verification", async () => {
return await emailService.sendVerification(email);
});
// Step 3: Wait for email verification (up to 48 hours)
const verified = await context.waitForCallback(
"email-verification",
async (callbackId) => {
await notificationService.sendVerificationLink({
email,
callbackId,
expiresIn: 172800
});
},
{
timeout: { seconds: 172800 }
}
);
if (!verified) {
await context.step("send-reminder", async () => {
await emailService.sendReminder(email);
});
return {
status: "verification_timeout",
userId,
message: "Email verification not completed within 48 hours"
};
}
// Step 4: Initialize user profile in parallel
const setupResults = await context.parallel("profile-setup", [
async (ctx: DurableContext) => {
return await ctx.step("create-preferences", async () => {
return await preferencesService.createDefaults(userId);
});
},
async (ctx: DurableContext) => {
return await ctx.step("setup-notifications", async () => {
return await notificationService.setupDefaults(userId);
});
},
async (ctx: DurableContext) => {
return await ctx.step("create-welcome-content", async () => {
return await contentService.createWelcome(userId);
});
}
]);
// Step 5: Send welcome email
await context.step("send-welcome", async () => {
const [preferences, notifications, content] = setupResults.getResults();
return await emailService.sendWelcome({
email,
preferences,
notifications,
content
});
});
return {
status: "onboarding_complete",
userId,
completedAt: new Date().toISOString()
};
}
);
이 프로세스는 계정 생성과 이메일 전송에 대한 체크포인트가 있는 순차적 step을 결합하고, 리소스를 소모하지 않으면서 이메일 검증을 최대 48시간 기다렸다가 일시 중지해요. 조건부 로직이 검증 완료 여부에 따라 다른 경로를 처리하죠. 프로필 설정 작업은 병렬 연산으로 동시에 실행되어 총 실행 시간을 줄이고, 각 step은 일시적 실패 시 자동으로 재시도되어 온보딩이 신뢰할 수 있게 완료되도록 해요.
함수 간 체인 호출
durable function 안에서 context.invoke()로 다른 Lambda 함수를 호출할 수 있어요. 호출하는 함수는 호출된 함수가 완료될 때까지 일시 중지되며, 결과를 보존하는 체크포인트를 만들어요. 호출된 함수가 완료된 후 호출 함수가 중단되면 함수를 다시 호출하지 않고 저장된 결과로 재개해요.
고객 검증, 결제 처리, 재고 관리 같은 특정 도메인을 처리하는 전문화된 함수가 있고 이들을 워크플로에서 조정해야 할 때 이 패턴을 사용하세요. 각 함수는 자체 로직을 유지하고 여러 오케스트레이터 함수가 호출할 수 있어서 코드 중복을 피할 수 있어요.
TypeScript:
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
// Main orchestrator function
export const handler = withDurableExecution(
async (event: any, context: DurableContext) => {
const { orderId, customerId } = event;
// Step 1: Validate customer by invoking customer service function
const customer = await context.invoke(
"validate-customer",
"arn:aws:lambda:us-east-1:123456789012:function:customer-service:1",
{ customerId }
);
if (!customer.isValid) {
return { orderId, status: "rejected", reason: "invalid_customer" };
}
// Step 2: Check inventory by invoking inventory service function
const inventory = await context.invoke(
"check-inventory",
"arn:aws:lambda:us-east-1:123456789012:function:inventory-service:1",
{ orderId, items: event.items }
);
if (!inventory.available) {
return { orderId, status: "rejected", reason: "insufficient_inventory" };
}
// Step 3: Process payment by invoking payment service function
const payment = await context.invoke(
"process-payment",
"arn:aws:lambda:us-east-1:123456789012:function:payment-service:1",
{
customerId,
amount: inventory.totalAmount,
paymentMethod: customer.paymentMethod
}
);
// Step 4: Create shipment by invoking fulfillment service function
const shipment = await context.invoke(
"create-shipment",
"arn:aws:lambda:us-east-1:123456789012:function:fulfillment-service:1",
{
orderId,
items: inventory.allocatedItems,
address: customer.shippingAddress
}
);
return {
orderId,
status: "completed",
trackingNumber: shipment.trackingNumber,
estimatedDelivery: shipment.estimatedDelivery
};
}
);
각 호출은 오케스트레이터 함수에 체크포인트를 만들어요. 고객 검증이 완료된 후 오케스트레이터가 중단되면 저장된 고객 데이터로 해당 체크포인트에서 재개해서 검증 호출을 건너뛰어요. 이는 다운스트림 서비스에 대한 중복 호출을 막고 중단 사이에 일관된 실행을 보장해요.
호출되는 함수는 durable 또는 표준 Lambda 함수일 수 있어요. durable 함수를 호출하면 그 함수는 waits와 체크포인트가 있는 자체 다단계 워크플로를 가질 수 있어요. 오케스트레이터는 완전한 durable 실행이 끝날 때까지 기다렸다가 최종 결과를 받기만 하면 돼요.
참고 크로스 계정 호출은 지원되지 않아요. 호출되는 모든 함수는 호출 함수와 같은 AWS 계정에 있어야 해요.
체크포인트가 있는 배치 처리
실패 후 마지막 성공 체크포인트에서 자동 복구하면서 수백만 개의 레코드를 처리해요. 이 예시는 durable functions가 map() 연산을 청킹(chunking)과 속도 제한과 결합해 대규모 데이터 처리를 다루는 방법을 보여줘요.
TypeScript:
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
interface Batch {
batchIndex: number;
recordIds: string[];
}
export const handler = withDurableExecution(
async (event: any, context: DurableContext) => {
const { datasetId, batchSize = 1000 } = event;
// Step 1: Get all record IDs to process
const recordIds = await context.step("fetch-record-ids", async () => {
return await dataService.getRecordIds(datasetId);
});
// Step 2: Split into batches
const batches: Batch[] = [];
for (let i = 0; i < recordIds.length; i += batchSize) {
batches.push({
batchIndex: Math.floor(i / batchSize),
recordIds: recordIds.slice(i, i + batchSize)
});
}
// Step 3: Process batches with controlled concurrency
const batchResults = await context.map(
"process-batches",
batches,
async (ctx: DurableContext, batch: Batch, index: number) => {
const processed = await ctx.step(`batch-${batch.batchIndex}`, async () => {
const results = [];
for (const recordId of batch.recordIds) {
const result = await recordService.process(recordId);
results.push(result);
}
return results;
});
const validated = await ctx.step(`validate-${batch.batchIndex}`, async () => {
return await validationService.validateBatch(processed);
});
return {
batchIndex: batch.batchIndex,
recordCount: batch.recordIds.length,
successCount: validated.successCount,
failureCount: validated.failureCount
};
},
{
maxConcurrency: 5
}
);
// Step 4: Aggregate results
const summary = await context.step("aggregate-results", async () => {
const results = batchResults.getResults();
const totalSuccess = results.reduce((sum, r) => sum + r.successCount, 0);
const totalFailure = results.reduce((sum, r) => sum + r.failureCount, 0);
return {
datasetId,
totalRecords: recordIds.length,
batchesProcessed: batches.length,
successCount: totalSuccess,
failureCount: totalFailure,
completedAt: new Date().toISOString()
};
});
return summary;
}
);
레코드는 메모리나 다운스트림 서비스를 압도하지 않도록 관리 가능한 배치로 나뉘고, 여러 배치가 maxConcurrency로 병렬도를 제어하며 동시에 처리돼요. 각 배치에는 자체 체크포인트가 있어 실패 시 모든 레코드를 다시 처리하는 대신 실패한 배치만 재시도해요. 이 패턴은 처리에 몇 시간이 걸릴 수 있는 ETL 작업, 데이터 마이그레이션, 또는 대량 작업에 이상적이에요.
다음 단계
- 기본 개념을 살펴보고 DurableContext, steps, waits를 이해하세요.
- 결정적(deterministic) 코드 작성과 성능 최적화를 위한 모범 사례를 검토하세요.
- 로컬과 클라우드에서 durable functions 테스트에 대해 알아보세요.
- durable functions와 Step Functions를 비교해 어느 접근 방식이 언제 가장 효과적인지 이해하세요. Durable functions or Step Functions 참고하세요.