Rust에서 Lambda 함수 핸들러 정의

Rust에서 Lambda 함수 핸들러 정의 (Define Lambda function handlers in Rust)

Lambda 함수 핸들러(handler)는 이벤트를 처리하는 함수 코드의 메서드예요. 함수가 호출되면 Lambda는 핸들러 메서드를 실행해요. 함수는 핸들러가 응답을 반환하거나, 종료되거나, 타임아웃될 때까지 실행돼요.

출처: AWS Lambda 개발자 안내서

본문

이 페이지는 프로젝트 초기화, 이름 규칙, 모범 사례를 포함해 Rust에서 Lambda 함수 핸들러로 작업하는 방법을 설명해요. 이 페이지에는 주문 정보를 받아 텍스트 파일 영수증을 생성하고 이 파일을 Amazon Simple Storage Service(S3) 버킷에 넣는 Rust Lambda 함수 예제도 포함돼 있어요. 함수를 작성한 후 배포하는 방법에 대한 자세한 내용은 .zip 파일 아카이브로 Rust Lambda 함수 배포를 참고하세요.

Rust 핸들러 프로젝트 설정 (Setting up your Rust handler project)

Rust에서 Lambda 함수로 작업할 때는 코드를 작성하고, 컴파일하고, 컴파일된 산출물을 Lambda에 배포하는 과정이 포함돼요. Rust에서 Lambda 핸들러 프로젝트를 설정하는 가장 간단한 방법은 AWS Lambda Runtime for Rust를 사용하는 것이에요. 이름에도 불구하고 AWS Lambda Runtime for Rust는 Python, Java, Node.js에서와 같은 의미의 관리형 런타임이 아니에요. 대신 AWS Lambda Runtime for Rust는 Rust에서 Lambda 함수를 작성하고 AWS Lambda의 실행 환경과 인터페이스하는 것을 지원하는 크레이트(lambda_runtime)예요.

Cargo 명령줄 도구의 타사 오픈소스 확장으로, Rust Lambda 함수를 빌드하고 배포하는 것을 단순화하는 Cargo Lambda를 설치하려면 다음 명령을 사용해요.

cargo install cargo-lambda

cargo-lambda 를 성공적으로 설치한 후 다음 명령을 사용해 새 Rust Lambda 함수 핸들러 프로젝트를 초기화해요.

cargo lambda new example-rust

이 명령을 실행하면 CLI(명령줄 인터페이스)가 Lambda 함수에 대해 몇 가지 질문을 해요.

  • HTTP 함수(HTTP function) – API Gateway나 함수 URL을 통해 함수를 호출하려면 Yes 로 답해요. 그렇지 않으면 No 로 답해요. 이 페이지의 예제 코드에서는 커스텀 JSON 이벤트로 함수를 호출하므로 No 로 답해요.
  • 이벤트 유형(Event type) – 미리 정의된 이벤트 형태로 함수를 호출하려면 올바른 예상 이벤트 유형을 선택해요. 그렇지 않으면 이 옵션을 비워 두어요. 이 페이지의 예제 코드에서는 커스텀 JSON 이벤트로 함수를 호출하므로 이 옵션을 비워 둬요.

명령이 성공적으로 실행된 후 프로젝트의 기본 디렉터리로 들어가요.

cd example-rust

이 명령은 src 디렉터리에 generic_handler.rs 파일과 main.rs 파일을 생성해요. generic_handler.rs 는 일반 이벤트 핸들러를 커스터마이즈하는 데 사용할 수 있어요. main.rs 파일에는 기본 애플리케이션 로직이 포함돼요. Cargo.toml 파일에는 패키지에 대한 메타데이터와 외부 종속성 목록이 포함돼요.

예제 Rust Lambda 함수 코드 (Example Rust Lambda function code)

다음 예제 Rust Lambda 함수 코드는 주문 정보를 받아 텍스트 파일 영수증을 생성하고 이 파일을 Amazon S3 버킷에 넣어요.

예제 main.rs Lambda 함수

use aws_sdk_s3::{Client, primitives::ByteStream};
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::env;

#[derive(Deserialize, Serialize)]
struct Order {
    order_id: String,
    amount: f64,
    item: String,
}

async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> {
    let payload = event.payload;

    // Deserialize the incoming event into Order struct
    let order: Order = serde_json::from_value(payload)?;

    let bucket_name = env::var("RECEIPT_BUCKET")
        .map_err(|_| "RECEIPT_BUCKET environment variable is not set")?;

    let receipt_content = format!(
        "OrderID: {}\nAmount: ${:.2}\nItem: {}",
        order.order_id, order.amount, order.item
    );
    let key = format!("receipts/{}.txt", order.order_id);

    let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
    let s3_client = Client::new(&config);

    upload_receipt_to_s3(&s3_client, &bucket_name, &key, &receipt_content).await?;

    Ok("Success".to_string())
}

async fn upload_receipt_to_s3(
    client: &Client,
    bucket_name: &str,
    key: &str,
    content: &str,
) -> Result<(), Error> {
    client
        .put_object()
        .bucket(bucket_name)
        .key(key)
        .body(ByteStream::from(content.as_bytes().to_vec()))  // Fixed conversion
        .content_type("text/plain")
        .send()
        .await?;

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    run(service_fn(function_handler)).await
}

이 main.rs 파일에는 다음 코드 섹션이 포함돼 있어요.

  • use 문: Lambda 함수에 필요한 Rust 크레이트와 메서드를 import 하는 데 사용해요.
  • #[derive(Deserialize, Serialize)] : 이 Rust 구조체에서 예상 입력 이벤트의 형태를 정의해요.
  • async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error>: 기본 핸들러 메서드 로, 기본 애플리케이션 로직을 담고 있어요.
  • async fn upload_receipt_to_s3 (...) : 기본 function_handler 메서드가 참조하는 도우미 메서드예요.
  • #[tokio::main] : Rust 프로그램의 진입점을 표시하는 매크로예요. 또한 main() 메서드가 async/await 를 사용하고 비동기로 실행할 수 있게 해주는 Tokio 런타임도 설정해요.
  • async fn main() -> Result<(), Error>: main() 함수는 코드의 진입점이에요. 그 안에서 function_handler 를 기본 핸들러 메서드로 지정해요.

이 함수와 함께 제공되는 Cargo.toml 파일은 다음과 같아요.

[package]
name = "example-rust"
version = "0.1.0"
edition = "2024"

[dependencies]
aws-config = "1.5.18"
aws-sdk-s3 = "1.78.0"
lambda_runtime = "0.13.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

이 함수가 올바르게 작동하려면 실행 역할이 s3:PutObject 작업을 허용해야 해요. 또한 RECEIPT_BUCKET 환경 변수를 정의해야 해요. 호출이 성공하면 Amazon S3 버킷에 영수증 파일이 있어야 해요.

Rust 핸들러의 유효한 클래스 정의 (Valid class definitions for Rust handlers)

대부분의 경우 Rust에서 정의하는 Lambda 핸들러 시그니처는 다음 형식을 가져요.

async fn function_handler(event: LambdaEvent<T>) -> Result<U, Error>

이 핸들러에 대해:

  • 이 핸들러의 이름은 function_handler 예요.
  • 핸들러의 단일 입력은 event이고 타입은 LambdaEvent<T> 예요.
  • LambdaEvent 는 lambda_runtime 크레이트에서 제공하는 래퍼예요. 이 래퍼를 사용하면 호출의 요청 ID 같은 Lambda 특정 메타데이터를 포함하는 context 객체에 접근할 수 있어요.
  • T 는 역직렬화된 이벤트 타입이에요. 예를 들어 serde_json::Value 일 수 있으며, 핸들러가 일반 JSON 입력을 받을 수 있게 해줘요. 또는 함수가 특정, 미리 정의된 입력 타입을 기대한다면 ApiGatewayProxyRequest 같은 타입일 수도 있어요.
  • 핸들러의 반환 타입은 Result<U, Error> 예요.
  • U 는 역직렬화된 출력 타입이에요. U 는 Lambda가 반환 값을 JSON으로 변환할 수 있도록 serde::Serialize 트레이트를 구현해야 해요. 예를 들어 U 는 String, serde_json::Value 같은 단순한 타입이거나 Serialize 를 구현하는 한 커스텀 구조체일 수 있어요. 코드가 Ok(U) 문에 도달하면 성공적인 실행을 나타내며 함수는 타입 U 의 값을 반환해요.
  • 코드가 오류(즉 Err(Error))를 만나면 함수는 오류를 Amazon CloudWatch에 기록하고 타입 Error 의 오류 응답을 반환해요.

이 예제에서 핸들러 시그니처는 다음과 같아요.

async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error>

다른 유효한 핸들러 시그니처에는 다음이 포함될 수 있어요.

  • LambdaEvent 래퍼 생략 – LambdaEvent 를 생략하면 함수 내에서 Lambda context 객체에 접근할 수 없어요. 다음은 이런 유형의 시그니처 예시예요.
    async fn handler(event: serde_json::Value) -> Result<String, Error>
    
  • 입력으로 단위(unit) 타입 사용 – Rust에서는 빈 입력을 나타내는 데 단위 타입을 사용할 수 있어요. 이는 주기적, 예약된 호출이 있는 함수에서 일반적으로 사용돼요. 다음은 이런 유형의 시그니처 예시예요.
    async fn handler(_: ()) -> Result<Value, Error>
    

핸들러 이름 규칙 (Handler naming conventions)

Rust의 Lambda 핸들러는 엄격한 이름 제한이 없어요. 핸들러에 어떤 이름이든 사용할 수 있지만, Rust의 함수 이름은 일반적으로 snake_case 예요.

이 예제 같은 작은 애플리케이션에서는 단일 main.rs 파일에 모든 코드를 포함할 수 있어요. 더 큰 프로젝트의 경우 main.rs 에 함수의 진입점을 포함해야 하지만, 코드를 논리적 모듈로 나누는 추가 파일을 가질 수 있어요. 예를 들어 다음과 같은 파일 구조를 가질 수 있어요.

/example-rust
│── src/
│   ├── main.rs        # Entry point
│   ├── handler.rs     # Contains main handler
│   ├── services.rs    # [Optional] Back-end service calls
│   ├── models.rs      # [Optional] Data models
│── Cargo.toml

입력 이벤트 객체 정의 및 접근 (Defining and accessing the input event object)

JSON은 Lambda 함수의 가장 일반적이고 표준적인 입력 형식이에요. 이 예제에서 함수는 다음과 같은 입력을 기대해요.

{
    "order_id": "12345",
    "amount": 199.99,
    "item": "Wireless Headphones"
}

Rust에서는 구조체에서 예상 입력 이벤트의 형태를 정의할 수 있어요. 이 예제에서는 Order 를 나타내는 다음 구조체를 정의해요.

#[derive(Deserialize, Serialize)]
struct Order {
    order_id: String,
    amount: f64,
    item: String,
}

이 구조체는 예상 입력 형태와 일치해요. 이 예제에서 #[derive(Deserialize, Serialize)] 매크로는 직렬화와 역직렬화를 위한 코드를 자동으로 생성해요. 이는 serde_json::from_value() 메서드를 사용해 일반 입력 JSON 타입을 구조체로 역직렬화할 수 있다는 뜻이에요. 핸들러의 처음 몇 줄에 나와 있어요.

async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> {
    let payload = event.payload;

    // Deserialize the incoming event into Order struct
    let order: Order = serde_json::from_value(payload)?;
    ...
}

그런 다음 객체의 필드에 접근할 수 있어요. 예를 들어 order.order_id 는 원래 입력에서 order_id 의 값을 검색해요.

미리 정의된 입력 이벤트 타입 (Pre-defined input event types)

aws_lambda_events 크레이트에는 미리 정의된 입력 이벤트 타입이 많이 있어요. 예를 들어 API Gateway로 함수를 호출하려면 다음 import 를 포함해요.

use aws_lambda_events::event::apigw::ApiGatewayProxyRequest;

그런 다음 기본 핸들러가 다음 시그니처를 사용하는지 확인해요.

async fn handler(event: LambdaEvent<ApiGatewayProxyRequest>) -> Result<String, Error> {
    let body = event.payload.body.unwrap_or_default();
    ...
}

다른 미리 정의된 입력 이벤트 타입에 대한 자세한 내용은 aws_lambda_events 크레이트를 참고하세요.

Lambda context 객체 접근 및 사용 (Accessing and using the Lambda context object)

Lambda context 객체는 호출, 함수, 실행 환경에 대한 정보를 담고 있어요. Rust에서 LambdaEvent 래퍼에는 context 객체가 포함돼 있어요. 예를 들어 context 객체를 사용해 다음 코드로 현재 호출의 요청 ID를 검색할 수 있어요.

async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> {
    let request_id = event.context.request_id;
    ...
}

context 객체에 대한 자세한 내용은 Lambda context 객체를 사용해 Rust 함수 정보 검색을 참고하세요.

핸들러에서 AWS SDK for Rust 사용 (Using the AWS SDK for Rust in your handler)

자주 Lambda 함수를 사용해 다른 AWS 리소스와 상호작용하거나 업데이트하게 돼요. 이러한 리소스와 인터페이스하는 가장 간단한 방법은 AWS SDK for Rust를 사용하는 것이에요.

함수에 SDK 종속성을 추가하려면 Cargo.toml 파일에 추가해요. 함수에 필요한 라이브러리만 추가하는 것을 권장해요. 앞선 예제 코드에서 aws_sdk_s3::Client 를 사용했어요. Cargo.toml 파일에서 [dependencies] 섹션 아래에 다음 줄을 추가해 이 종속성을 추가할 수 있어요.

aws-sdk-s3 = "1.78.0"

참고

이것이 최신 버전이 아닐 수 있어요. 애플리케이션에 적합한 버전을 선택하세요.

그런 다음 코드에서 종속성을 직접 import 해요.

use aws_sdk_s3::{Client, primitives::ByteStream};

예제 코드는 Amazon S3 클라이언트를 다음과 같이 초기화해요.

let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
let s3_client = Client::new(&config);

SDK 클라이언트를 초기화한 후 다른 AWS 서비스와 상호작용하는 데 사용할 수 있어요. 예제 코드는 upload_receipt_to_s3 도우미 함수에서 Amazon S3 PutObject API를 호출해요.

환경 변수 접근 (Accessing environment variables)

핸들러 코드에서 env::var 메서드를 사용해 어떤 환경 변수든 참조할 수 있어요. 이 예제에서는 다음 코드 줄을 사용해 정의된 RECEIPT_BUCKET 환경 변수를 참조해요.

let bucket_name = env::var("RECEIPT_BUCKET")
    .map_err(|_| "RECEIPT_BUCKET environment variable is not set")?;

공유 상태 사용 (Using shared state)

Lambda 함수의 핸들러 코드와 독립적인 공유 변수를 선언할 수 있어요. 이 변수는 함수가 이벤트를 받기 전인 Init 단계 동안 상태 정보를 로드하는 데 도움이 될 수 있어요. 예를 들어 이 페이지의 코드를 수정해 main 함수와 핸들러 시그니처를 업데이트함으로써 Amazon S3 클라이언트를 초기화할 때 공유 상태를 사용할 수 있어요.

async fn function_handler(client: &Client, event: LambdaEvent<Value>) -> Result<String, Error> {
    ...
    upload_receipt_to_s3(client, &bucket_name, &key, &receipt_content).await?;
    ...
}

...

#[tokio::main]
async fn main() -> Result<(), Error> {
    let shared_config = aws_config::from_env().load().await;
    let client = Client::new(&shared_config);
    let shared_client = &client;
    lambda_runtime::run(service_fn(move |event: LambdaEvent<Request>| async move {
        handler(&shared_client, event).await
    }))
    .await

Rust Lambda 함수 코드 모범 사례 (Code best practices for Rust Lambda functions)

Lambda 함수를 구축할 때 다음 목록의 지침을 준수해 최상의 코딩 방식을 사용하세요.

  • Lambda 핸들러를 핵심 로직과 분리하세요. 이렇게 하면 더 단위 테스트하기 쉬운 함수를 만들 수 있어요.
  • 종속성의 복잡성을 최소화하세요. 실행 환경 시작 시 빠르게 로드되는 더 간단한 프레임워크를 선호하세요.
  • 배포 패키지 크기를 런타임 요구 사항으로 최소화하세요. 이렇게 하면 호출 전에 배포 패키지를 다운로드하고 압축을 푸는 데 걸리는 시간이 줄어들어요.
  • 실행 환경 재사용을 활용해 함수 성능을 개선하세요. SDK 클라이언트와 데이터베이스 연결을 함수 핸들러 밖에서 초기화하고, 정적 자산은 /tmp 디렉터리에 로컬로 캐시하세요. 함수의 같은 인스턴스가 처리하는 후속 호출은 이러한 리소스를 재사용할 수 있어요. 이렇게 하면 함수 실행 시간을 줄여 비용을 절약할 수 있어요. 호출 간 잠재적인 데이터 누출을 피하려면 실행 환경을 사용해 사용자 데이터, 이벤트, 보안 영향을 주는 기타 정보를 저장하지 마세요. 함수가 핸들러 내 메모리에 저장할 수 없는 변경 가능한 상태에 의존한다면 각 사용자에 대해 별도의 함수 또는 함수 버전을 만드는 것을 고려하세요.
  • 지속 연결을 유지하려면 keep-alive 지시문을 사용하세요. Lambda는 시간이 지나면 유휴 연결을 정리해요. 함수를 호출할 때 유휴 연결을 재사용하려고 하면 연결 오류가 발생해요. 지속 연결을 유지하려면 런타임과 연결된 keep-alive 지시문을 사용해요. 예제는 Node.js에서 keep-alive로 연결 재사용을 참고하세요.
  • 환경 변수를 사용해 운영 매개 변수를 함수에 전달하세요. 예를 들어 Amazon S3 버킷에 쓰는 경우 쓸 버킷 이름을 하드코딩하는 대신 버킷 이름을 환경 변수로 구성해요.
  • Lambda 함수에서 재귀적 호출을 피하세요. 함수가 자신을 호출하거나 함수를 다시 호출할 수 있는 프로세스를 시작하는 경우를 말해요. 이로 인해 의도하지 않은 함수 호출 양과 비용 상승이 발생할 수 있어요. 의도하지 않은 호출 양이 보이면 코드를 업데이트하는 동안 함수 예약 동시성을 즉시 0 으로 설정해 함수에 대한 모든 호출을 제한하세요.
  • Lambda 함수 코드에서 문서화되지 않은, 공개되지 않은 API를 사용하지 마세요. AWS Lambda 관리 런타임의 경우 Lambda는 Lambda 내부 API에 보안 및 기능 업데이트를 주기적으로 적용해요. 이러한 내부 API 업데이트는 이전 버전과 호환되지 않을 수 있어요. 함수가 이러한 비공개 API에 의존하면 호출 오류와 같은 의도하지 않은 결과가 발생할 수 있어요. 공개적으로 사용 가능한 API 목록은 API 참조를 참고하세요.
  • 멱등성 코드를 작성하세요. 함수에 멱등성 코드를 작성하면 중복 이벤트가 동일한 방식으로 처리되도록 보장해요. 코드는 이벤트를 올바르게 검증하고 중복 이벤트를 우아하게 처리해야 해요. 자세한 내용은 Lambda 함수를 멱등성으로 만드는 방법을 참고하세요.

더 알아보기 (Learn more)