Java Lambda 함수 핸들러 정의하기

Java Lambda 함수 핸들러 정의하기 (Define Lambda function handler in Java)

Lambda 함수 핸들러(handler)는 함수 코드에서 이벤트를 처리하는 메서드예요. 함수가 호출되면 Lambda가 핸들러 메서드를 실행하고, 핸들러가 응답을 반환하거나 종료하거나 타임아웃될 때까지 함수가 실행돼요. 이 페이지는 프로젝트 설정, 명명 규칙, 모범 사례를 포함해 Java에서 Lambda 함수 핸들러를 다루는 방법을 설명하고, 주문 정보를 받아 텍스트 파일 영수증을 만들어 Amazon Simple Storage Service(Amazon S3) 버킷에 넣는 Java Lambda 함수 예제도 함께 보여줘요.

출처: AWS Lambda 개발자 안내서

본문

Java 핸들러 프로젝트 설정

Java에서 Lambda 함수를 작업할 때 코드를 작성하고, 컴파일하고, 컴파일된 아티팩트를 Lambda에 배포하는 과정을 거쳐요. Java Lambda 프로젝트는 다양한 방식으로 초기화할 수 있어요. 예를 들어 Lambda 함수용 Maven Archetype, AWS SAM CLI sam init 명령, 또는 IntelliJ IDEA나 Visual Studio Code 같은 선호 IDE의 표준 Java 프로젝트 설정을 사용할 수 있어요. 또는 필요한 파일 구조를 수동으로 만들 수 있어요.

일반적인 Java Lambda 함수 프로젝트 구조는 다음과 같아요.

/project-root
    └ src
        └ main
            └ java
                └ example
                    └ OrderHandler.java (메인 핸들러 포함)
                    └ <other_supporting_classes>
     └ build.gradle OR pom.xml

Maven이나 Gradle을 사용해 프로젝트를 빌드하고 의존성을 관리할 수 있어요. 함수의 메인 핸들러 로직은 src/main/java/example 디렉토리의 Java 파일에 있어요. 이 페이지의 예시에서 파일 이름은 OrderHandler.java예요. 이 파일 외에도 필요에 따라 추가 Java 클래스를 포함할 수 있어요. 함수를 Lambda에 배포할 때 호출 중에 Lambda가 호출할 메인 핸들러 메서드가 있는 Java 클래스를 지정해야 해요.

Java Lambda 함수 코드 예제

다음 예제 Java 21 Lambda 함수는 주문 정보를 받아 텍스트 파일 영수증을 만들고 그 파일을 Amazon S3 버킷에 넣어요.

예제 OrderHandler.java Lambda 함수

package example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;

import java.nio.charset.StandardCharsets;

/**
 * Lambda handler for processing orders and storing receipts in S3.
 */
public class OrderHandler implements RequestHandler<OrderHandler.Order, String> {

    private static final S3Client S3_CLIENT = S3Client.builder().build();

    /**
     * Record to model the input event.
     */
    public record Order(String orderId, double amount, String item) {}

    @Override
    public String handleRequest(Order event, Context context) {
        try {
            // Access environment variables
            String bucketName = System.getenv("RECEIPT_BUCKET");
            if (bucketName == null || bucketName.isEmpty()) {
                throw new IllegalArgumentException("RECEIPT_BUCKET environment variable is not set");
            }

            // Create the receipt content and key destination
            String receiptContent = String.format("OrderID: %s\nAmount: $%.2f\nItem: %s",
                    event.orderId(), event.amount(), event.item());
            String key = "receipts/" + event.orderId() + ".txt";

            // Upload the receipt to S3
            uploadReceiptToS3(bucketName, key, receiptContent);

            context.getLogger().log("Successfully processed order " + event.orderId() +
                    " and stored receipt in S3 bucket " + bucketName);
            return "Success";

        } catch (Exception e) {
            context.getLogger().log("Failed to process order: " + e.getMessage());
            throw new RuntimeException(e);
        }
    }

    private void uploadReceiptToS3(String bucketName, String key, String receiptContent) {
        try {
            PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(key)
                    .build();

            // Convert the receipt content to bytes and upload to S3
            S3_CLIENT.putObject(putObjectRequest, RequestBody.fromBytes(receiptContent.getBytes(StandardCharsets.UTF_8)));
        } catch (S3Exception e) {
            throw new RuntimeException("Failed to upload receipt to S3: " + e.awsErrorDetails().errorMessage(), e);
        }
    }
}

이 OrderHandler.java 파일은 다음 코드 섹션으로 구성돼요.

  • package example: Java에서 이 값은 무엇이든 될 수 있지만 프로젝트의 디렉토리 구조와 일치해야 합니다. 여기서는 디렉토리 구조가 src/main/java/example이므로 package example을 사용합니다.
  • import 문: Lambda 함수가 필요로 하는 Java 클래스를 import 합니다.
  • public class OrderHandler ...: Java 클래스를 정의하며, 유효한 클래스 정의여야 합니다.
  • private static final S3Client S3_CLIENT ...: 클래스의 어떤 메서드 밖에서 S3 클라이언트를 초기화합니다. Lambda는 초기화 단계에서 이 코드를 실행합니다.
  • public record Order ...: 이 커스텀 Java record에서 예상 입력 이벤트의 구조를 정의합니다.
  • public String handleRequest(Order event, Context context): 메인 애플리케이션 로직이 있는 메인 핸들러 메서드입니다.
  • private void uploadReceiptToS3(...) {}: 메인 handleRequest 핸들러 메서드가 참조하는 헬퍼 메서드입니다.

build.gradle

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.amazonaws:aws-lambda-java-core:1.2.3'
    implementation 'software.amazon.awssdk:s3:2.28.29'
    implementation 'org.slf4j:slf4j-nop:2.0.16'
}

task buildZip(type: Zip) {
    from compileJava
    from processResources
    into('lib') {
        from configurations.runtimeClasspath
    }
}

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

build.dependsOn buildZip

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>example-java</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>example-java-function</name>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>s3</artifactId>
            <version>2.28.29</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-nop</artifactId>
            <version>2.0.16</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.2</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.4.1</version>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                    <filters>
                        <filter>
                            <artifact>*:*</artifact>
                            <excludes>
                                <exclude>META-INF/*</exclude>
                                <exclude>META-INF/versions/**</exclude>
                            </excludes>
                        </filter>
                    </filters>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <release>21</release>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

이 함수가 제대로 작동하려면 실행 역할이 s3:PutObject 액션을 허용해야 하고, RECEIPT_BUCKET 환경 변수를 정의해야 해요.

참고

이 함수는 타임아웃 없이 성공적으로 실행되려면 추가 구성 설정이 필요할 수 있어요. 256MB 메모리와 10초 타임아웃을 구성할 것을 권장합니다. 첫 호출은 콜드 스타트 때문에 추가 시간이 걸릴 수 있어요. 이후 호출은 실행 환경 재사용으로 훨씬 빠르게 실행될 거예요.

Java 핸들러의 유효한 클래스 정의

클래스를 정의하려면 aws-lambda-java-core 라이브러리가 핸들러 메서드용 두 인터페이스를 정의해요. 제공된 인터페이스를 사용하면 핸들러 구성을 단순화하고 컴파일 시점에 메서드 시그니처를 검증할 수 있어요.

  • com.amazonaws.services.lambda.runtime.RequestHandler
  • com.amazonaws.services.lambda.runtime.RequestStreamHandler

RequestHandler 인터페이스는 입력 타입과 출력 타입 두 파라미터를 받는 제네릭 타입이에요. 두 타입 모두 객체여야 해요. 이 예시에서 OrderHandler 클래스는 RequestHandler<OrderHandler.Order, String>을 구현해요. 입력 타입은 클래스 안에 정의한 Order record이고, 출력 타입은 String이에요.

public class OrderHandler implements RequestHandler<OrderHandler.Order, String> {
    ...
}

이 인터페이스를 사용하면 Java 런타임이 이벤트를 입력 타입의 객체로 역직렬화하고 출력을 텍스트로 직렬화해요. 내장 직렬화가 입력·출력 타입과 작동할 때 이 인터페이스를 사용하세요.

자체 직렬화를 사용하려면 RequestStreamHandler 인터페이스를 구현할 수 있어요. 이 인터페이스에서 Lambda는 핸들러에 입력 스트림과 출력 스트림을 전달해요. 핸들러는 입력 스트림에서 바이트를 읽고 출력 스트림에 쓰며 void를 반환해요.

Java 함수에서 기본·제네릭 타입(String, Integer, List, Map)만 다룬다면 인터페이스를 구현할 필요가 없어요. 예를 들어 함수가 Map<String, String> 입력을 받고 String을 반환한다면 클래스 정의와 핸들러 시그니처는 다음과 같을 수 있어요.

public class ExampleHandler {
    public String handleRequest(Map<String, String> input, Context context) {
        ...
    }
}

또한 인터페이스를 구현하지 않으면 context 객체는 선택 사항이에요. 예를 들어 클래스 정의와 핸들러 시그니처는 다음과 같을 수 있어요.

public class NoContextHandler {
   public String handleRequest(Map<String, String> input) {
        ...
   }
}

핸들러 명명 규칙

Java Lambda 함수에서 RequestHandler나 RequestStreamHandler 인터페이스 중 하나를 구현한다면 메인 핸들러 메서드는 handleRequest라고 이름을 지어야 해요. 또한 handleRequest 메서드 위에 @Override 태그를 포함하세요. 함수를 Lambda에 배포할 때 함수 구성에서 메인 핸들러를 다음 형식으로 지정해요.

  • <package>.<Class> – 예: example.OrderHandler.

RequestHandler나 RequestStreamHandler 인터페이스를 구현하지 않는 Java Lambda 함수는 핸들러 이름으로 어떤 이름이든 사용할 수 있어요. 함수를 Lambda에 배포할 때 함수 구성에서 메인 핸들러를 다음 형식으로 지정해요.

  • <package>.<Class>::<handler_method_name> – 예: example.Handler::mainHandler.

입력 이벤트 객체 정의와 접근

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

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

Java 17 이상에서 Lambda 함수를 다룰 때 Java record로 예상 입력 이벤트의 구조를 정의할 수 있어요. 이 예시에서는 OrderHandler 클래스 안에 Order 객체를 나타내는 record를 정의해요.

public record Order(String orderId, double amount, String item) {}

이 record는 예상 입력 구조와 일치해요. record를 정의한 후 record 정의에 맞는 JSON 입력을 받는 핸들러 시그니처를 작성할 수 있어요. Java 런타임이 이 JSON을 Java 객체로 자동 역직렬화해요. 그런 다음 객체의 필드에 접근할 수 있어요. 예를 들어 event.orderId는 원래 입력에서 orderId 값을 가져와요.

참고

Java record는 Java 17 런타임 이상에서만 사용할 수 있는 기능이에요. 모든 Java 런타임에서 클래스를 사용해 이벤트 데이터를 나타낼 수 있어요. 이런 경우 jackson 같은 라이브러리로 JSON 입력을 역직렬화할 수 있어요.

기타 입력 이벤트 유형

Java Lambda 함수에는 가능한 입력 이벤트가 많아요.

  • Integer, Long, Double 등 숫자 타입 – 이벤트는 추가 포맷 없이 숫자입니다(예: 3.5). Java 런타임이 값을 지정된 타입의 객체로 변환합니다.
  • String – 이벤트는 따옴표를 포함한 JSON 문자열입니다(예: "My string"). 런타임은 따옴표 없이 String 객체로 변환합니다.
  • List, List, List 등 – 이벤트는 JSON 배열입니다. 런타임이 지정된 타입·인터페이스의 객체로 역직렬화합니다.
  • InputStream – 이벤트는 어떤 JSON 타입입니다. 런타임이 문서의 바이트 스트림을 수정 없이 핸들러에 전달합니다. 입력을 역직렬화하고 출력 스트림에 출력을 씁니다.
  • 라이브러리 타입 – 다른 AWS 서비스가 보낸 이벤트에는 aws-lambda-java-events 라이브러리의 타입을 사용합니다. 예를 들어 Lambda 함수가 Amazon Simple Queue Service(SQS)에 의해 호출되면 SQSEvent 객체를 입력으로 사용합니다.
  • Lambda context 객체 접근·사용

    Lambda context 객체는 호출, 함수, 실행 환경에 대한 정보를 포함해요. 이 예시에서 context 객체는 com.amazonaws.services.lambda.runtime.Context 타입이며 메인 핸들러 함수의 두 번째 인수예요.

    public String handleRequest(Order event, Context context) {
        ...
    }
    

    클래스가 RequestHandler 또는 RequestStreamHandler 인터페이스 중 하나를 구현하면 context 객체는 필수 인수예요. 그렇지 않으면 context 객체는 선택 사항이에요.

    AWS SDK로 다른 서비스에 호출하면 context 객체는 몇 가지 핵심 영역에서 필요해요. 예를 들어 Amazon CloudWatch용 함수 로그를 만들려면 context.getLogger() 메서드로 로깅용 LambdaLogger 객체를 구할 수 있어요. 이 예시에서 로거를 사용해 처리 실패 시 오류 메시지를 로깅할 수 있어요.

    context.getLogger().log("Failed to process order: " + e.getMessage());
    

    로깅 외에도 context 객체를 함수 모니터링에 사용할 수 있어요.

    핸들러에서 AWS SDK for Java v2 사용하기

    Lambda 함수로 다른 AWS 리소스와 상호작용하거나 리소스를 업데이트하는 경우가 많죠. 이 리소스와 인터페이스하는 가장 간단한 방법은 AWS SDK for Java v2를 사용하는 것이에요.

    참고

    AWS SDK for Java(v1)는 유지보수 모드이며 2025년 12월 31일에 지원이 종료돼요. 앞으로는 AWS SDK for Java v2만 사용할 것을 권장합니다.

    함수에 SDK 의존성을 추가하려면 Gradle의 build.gradle 또는 Maven의 pom.xml 파일에 추가해요. 함수에 필요한 라이브러리만 추가할 것을 권장해요. 앞선 예시 코드에서 software.amazon.awssdk.services.s3 라이브러리를 사용했어요. Gradle에서는 build.gradle의 dependencies 섹션에 다음 줄을 추가해 이 의존성을 추가할 수 있어요.

    implementation 'software.amazon.awssdk:s3:2.28.29'
    

    Maven에서는 pom.xml의 <dependencies> 섹션에 다음 줄을 추가해요.

        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>s3</artifactId>
            <version>2.28.29</version>
        </dependency>
    

    참고

    이 버전이 SDK의 가장 최신 버전이 아닐 수 있어요. 애플리케이션에 적절한 SDK 버전을 선택하세요.

    그런 다음 Java 클래스에서 의존성을 직접 import 해요.

    import software.amazon.awssdk.services.s3.S3Client;
    import software.amazon.awssdk.services.s3.model.PutObjectRequest;
    import software.amazon.awssdk.services.s3.model.S3Exception;
    

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

    private static final S3Client S3_CLIENT = S3Client.builder().build();
    

    함수를 호출할 때마다 초기화하지 않도록 Amazon S3 클라이언트를 메인 핸들러 함수 밖에서 초기화했어요. SDK 클라이언트를 초기화한 후엔 다른 AWS 서비스와 상호작용하는 데 사용할 수 있어요. 예시 코드는 Amazon S3 PutObject API를 다음과 같이 호출해요.

    PutObjectRequest putObjectRequest = PutObjectRequest.builder()
        .bucket(bucketName)
        .key(key)
        .build();
    
    // Convert the receipt content to bytes and upload to S3
    S3_CLIENT.putObject(putObjectRequest, RequestBody.fromBytes(receiptContent.getBytes(StandardCharsets.UTF_8)));
    

    환경 변수 접근하기

    핸들러 코드에서 System.getenv() 메서드로 어떤 환경 변수든 참조할 수 있어요.

    String bucketName = System.getenv("RECEIPT_BUCKET");
    if (bucketName == null || bucketName.isEmpty()) {
        throw new IllegalArgumentException("RECEIPT_BUCKET environment variable is not set");
    }
    

    전역 상태 사용하기

    Lambda는 함수를 처음 호출하기 전에 초기화 단계에서 정적 코드와 클래스 생성자를 실행해요. 초기화 중에 만들어진 리소스는 호출 사이에 메모리에 남아, 함수를 호출할 때마다 만들 필요가 없어요. 예시 코드에서 S3 클라이언트 초기화 코드는 메인 핸들러 메서드 밖에 있어요.

    Java Lambda 함수 코드 모범 사례

    • Lambda 핸들러를 핵심 로직과 분리하세요. 이렇게 하면 더 단위 테스트가 쉬운 함수를 만들 수 있어요.
    • 함수 배포 패키지의 의존성을 제어하세요. AWS Lambda 실행 환경에는 여러 라이브러리가 포함돼 있어요. Lambda는 이 라이브러리를 주기적으로 업데이트하며, 이 업데이트로 함수 동작에 미묘한 변화가 생길 수 있어요. 모든 의존성을 배포 패키지에 패키징하세요.
    • 의존성의 복잡성을 최소화하세요. 실행 환경 시작 시 빠르게 로드되는 더 단순한 프레임워크를 선호해요. 예를 들어 Spring Framework처럼 복잡한 것보다 Dagger나 Guice 같은 더 단순한 Java 의존성 주입(IoC) 프레임워크를 선호해요.
    • 배포 패키지 크기를 런타임 필수 요소로 최소화하세요. 호출 전에 배포 패키지를 다운로드·압축 해제하는 시간이 줄어들어요. Java로 작성한 함수에서는 전체 AWS SDK 라이브러리를 배포 패키지의 일부로 업로드하지 마세요. 대신 필요한 SDK 구성 요소를 제공하는 모듈을 선택적으로 의존하세요(예: DynamoDB, Amazon S3 SDK 모듈과 Lambda 코어 라이브러리).

    실행 환경 재사용을 활용해 함수 성능을 개선하세요. SDK 클라이언트와 데이터베이스 연결을 함수 핸들러 밖에서 초기화하고, 정적 자산을 /tmp 디렉토리에 로컬로 캐시하세요. 이후 호출은 이 리소스를 재사용할 수 있어요. 함수 실행 시간을 줄여 비용을 절약해요.

    호출 간 잠재적 데이터 누출을 피하기 위해 실행 환경에 사용자 데이터, 이벤트, 보안에 영향이 있는 기타 정보를 저장하지 마세요.

    Keep-alive 지시어를 사용해 영구 연결을 유지하세요. Lambda는 시간이 지나며 유휴 연결을 정리해요. 영구 연결을 유지하려면 런타임에 연결된 keep-alive 지시어를 사용하세요.

    환경 변수로 운영 파라미터를 함수에 전달하세요. 예를 들어 Amazon S3 버킷에 쓴다면 버킷 이름을 하드코딩하지 말고 환경 변수로 구성하세요.

    Lambda 함수에서 재귀 호출을 피하세요. 함수가 자신을 호출하면 의도치 않은 호출량과 비용 증가로 이어질 수 있어요.

    Lambda 함수 코드에서 문서화되지 않은 비공개 API를 사용하지 마세요. Lambda 관리 런타임의 내부 API 업데이트는 하위 호환되지 않을 수 있어, 비공개 API에 의존하면 호출 실패 같은 의도치 않은 결과가 생길 수 있어요.

    멱등(idempotent) 코드를 작성하세요. 중복 이벤트가 같은 방식으로 처리되게 보장할 수 있어요.

    • Java DNS 캐시 사용을 피하세요. Lambda 함수는 이미 DNS 응답을 캐시해요. 다른 DNS 캐시를 사용하면 연결 타임아웃을 겪을 수 있어요. java.util.logging.Logger 클래스는 간접적으로 JVM DNS 캐시를 활성화할 수 있어요. 기본 설정을 덮어쓰려면 로거를 초기화하기 전에 networkaddress.cache.ttl을 0으로 설정하세요. 예시:
      public class MyHandler {
        // first set TTL property
        static{
         java.security.Security.setProperty("networkaddress.cache.ttl" , "0");
        }
       // then instantiate logger
        var logger = org.apache.logging.log4j.LogManager.getLogger(MyHandler.class);
      }
      
    • 의존성 .jar 파일을 별도의 /lib 디렉토리에 넣어 Java로 작성한 배포 패키지를 Lambda가 푸는 시간을 줄이세요. 이는 모든 함수 코드를 많은 .class 파일이 있는 단일 jar에 넣는 것보다 빠르답니다.

    더 알아보기 (Learn more)