AWS Lambda 실습 : Simple Java application

AWS Lambda 실습 : Simple Java application

나는 몇달 전에 만들었던 ‘백발백준‘프로젝트를 aws lambda를 사용하여 serverless 아키텍처로 배포해보려 한다. 그 전에 사용법에 대해 익히기 위해서 간단한 주문 어플리케이션을 만들어볼 것이다.


요구사항

1. item을 추가할 수 있어야 한다.
2. 현재 DB에 저장된 item 목록을 조회할 수 있어야 한다.
3. item의 '이름(name)'으로 조회할 수 있어야 한다.

다음과 같은 3가지 요구사항을 만족하는 어플리케이션을 만들어보도록 하자. DB에 연결하기 전에 각각의 function이 잘 동작하는지 확인할 것이다.


Environment

== AWS ==
aws lambda java core
aws lambda java events
aws lambda java log4j2

== Spring ==
spring boot starter
spring boot starter test
spring cloud starter function web // for local testing
spring cloud function adapter aws 

== Java ==
lombok
Java 8


모르는 것 정리

일단 처음 접하는 것이기에 모르는 것 투성이다. 내가 하고 싶은 것은

  1. Restful API로 개발하고 싶다
    • HTTP Method로 통신할 것이다. → Get, Post 등등의 요청에 대한 구별이 필요
  2. DB와 연동하여 영구적인 데이터에 접근하고 싶다

그리고 내가 모르는 것은

  1. python으로 lambda application을 만들었을 때에는 def lambda_handler(event, context)와 같이 비교적 직관적인 위치에 핸들러 함수가 존재했다. 하지만 Java는 package구조가 존재하기에 어디에 핸들러 함수를 만들어야 하는지 몰랐다.
    • 핸들러 함수의 parameter는 어떤 타입인지 궁금했다.
    • 각 parameter를 직접 parsing해서 사용해야하는 것인지 궁금했다.
  2. Spring boot로 어플리케이션을 만들건데 외부 라이브러리를 어떻게 가져올지 고민이다.
    • python 으로 할 때에는 lambda layer로 해결. Java도 똑같이 하면 되는지 궁금했다.
  3. packge 구성을 어떻게 해야할지 몰랐다.
    • 어떤 의존성이 필요한지

이제 모르는 것들을 하나씩 찾아보자.


handler function

Handler function이란 무엇일까?

Lambda 함수의 핸들러는 이벤트를 처리하는 함수 코드의 메서드입니다. 함수가 호출되면 Lambda는 핸들러 메서드를 실행합니다. 핸들러가 존재하거나 응답을 반환할 때, 또 다른 이벤트를 처리하기 위해 사용할 수 있게 됩니다.

라고 aws docs에서 말하고 있다. Java로 lambda에서 발생한 요청을 처리하기 위해서 handler function을 정의해야 하는데 다행히도 예제 코드를 제공했기에 수월했다.

// Handler.java
package example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.LambdaLogger;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.util.Map;

// Handler value: example.Handler
public class Handler implements RequestHandler<Map<String,String>, String> {
  Gson gson = new GsonBuilder().setPrettyPrinting().create();
  
  @Override
  public String handleRequest(Map<String,String> event, Context context) {
    LambdaLogger logger = context.getLogger();
    String response = "200 OK";
    // log execution details
    logger.log("ENVIRONMENT VARIABLES: " + gson.toJson(System.getenv()));
    logger.log("CONTEXT: " + gson.toJson(context));
    // process event
    logger.log("EVENT: " + gson.toJson(event));
    logger.log("EVENT TYPE: " + event.getClass());
    return response;
  }
}

aws에서 제공하는 코드들 중 하나를 가져왔다(Code).


위와 같이 예제가 있었지만 좀 더 쉬운 이해를 위해서 다음과 같은 유투브 영상을 참고하였다. 이제 aws lambda로 간단한 문자열 뒤집기 함수를 만들어 보도록 하겠다.


Java : 문자열 뒤집기

쉽게 만들기 위해서 spring cloud를 사용하여 구현했다.

dependencies {
	// Handler interface 제공
	implementation group: 'com.amazonaws', name: 'aws-lambda-java-core', version: '1.2.1'
	// AWS Lambda 이벤트를 다루기 위한 의존성
	implementation group: 'com.amazonaws', name: 'aws-lambda-java-events', version: '3.9.0'
	// AWS Lambda Logging
	runtimeOnly 'com.amazonaws:aws-lambda-java-log4j2:1.5.0'
	...
	// spring cloud
	implementation 'org.springframework.cloud:spring-cloud-starter-function-web'
	implementation group: 'org.springframework.cloud', name: 'spring-cloud-function-adapter-aws', version: '3.2.7'

	...
}


awslambdasimpleapp1_1

package구성은 위와 같이 했다. Main class가 있는 위치에 아래와 같은 Handler class를 만들어준다.

@Component
public class Handler {
	@Bean
	public Function<String, String> reverse() {
		return (inputString) -> new StringBuilder(inputString).reverse().toString();
	}
}

와 같이 Handler class 안에 우리가 동작시킬 기능을 @Bean으로 등록해준다.


Spring Cloud Function이란(간단하게)

그리고 다음과 같은 특징을 가졌다.

Adapters for AWS Lambda, Microsoft Azure, Apache OpenWhisk and possibly other “serverless” service providers.

aws lambda와 그 외 serverless provider adapter이기에 영상에서도 사용한 것 같다.


Local test

spring cloud를 local환경에서 테스트하기 위해서

implementation 'org.springframework.cloud:spring-cloud-starter-function-web'

의존성을 추가해줬다.

이제 다음과 같은 명령어를 terminal에 입력하여 문자열이 뒤집어서 나오는지 확인해보자.

curl -H "Content-Type: text/plain" localhost:8080/reverse -d Hello

Error 발생

Execution failed for task ':compileJava'.
> Could not resolve all files for configuration ':compileClasspath'.
   > Could not find org.springframework.cloud:spring-cloud-starter-function-web:.
     Required by:
         project :

Possible solution:
 - Declare repository providing the artifact, see the documentation at https://docs.gradle.org/current/userguide/declaring_repositories.html

위와 같은 에러가 발생했다. 의존성은 알맞게 넣어준 것 같은데 stackoverflow에서는 spring cloud version을 명시하라고 했다. 정확하게 짚고 넘어가기 위해서 공식문서에 갔다…

awslambdasimpleapp1_2

If you an existing Spring Boot app you want to add Spring Cloud to that app, the first step is to determine the version of Spring Cloud you should use. The version you use in your app will depend on the version of Spring Boot you are using.

Spring cloud의존성을 추가하기 위해서 현재 나의 Spring Boot version에 따라 다르게 추가해줬어야 했다.

현재 나의 spring boot version

plugins {
	id 'org.springframework.boot' version '2.7.5'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
	id 'java'
}
...

따라서 Jubilee를 받아야 함을 알게 됐다. 그리고 아래에 어떤 것을 추가해야하는지 다 나와있었기에 복붙으로 해결했다.

// example build.gradle
buildscript {
  dependencies {
    classpath "io.spring.gradle:dependency-management-plugin:1.0.10.RELEASE"
  }
}

ext {
  set('springCloudVersion', "Hoxton.SR8")
}


apply plugin: "io.spring.dependency-management"

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
  }
}

나는 현재 spring cloud ‘Jubilee’버전을 받아와야 하기에

ext {
	set('springCloudVersion', "2021.0.4")
}

이렇게 바꿔줬다.


RE: Local Test

curl -H "Content-Type: text/plain" localhost:8080/reverse -d Hello

awslambdasimpleapp1_3

문자열이 olleH로 잘 뒤집어진 것을 확인할 수 있었다.


AWS Lambda에 배포하기

Create AWS Lambda Function

awslambdasimpleapp1_4

aws 에 로그인을 한 뒤에 위 사진에서 오른쪽 위 create function 버튼을 클릭한다.


awslambdasimpleapp1_5

function name은 기능에 알맞게 지어줬다. 현재 StringReverse-Function프로젝트는 Java 8이기에 runtime환경을 java8 on aws linux2 로 해줬다.

IAM role같은 경우는 실제 백발백준 프로젝트라면 직접 cloudwatch와 같은 서비스에도 접근 권한을 줬을 것이다. 그러나 현재는 연습이기 때문에 lambda생성시 자동으로 생성되는

awslambdasimpleapp1_6

AWSLambdaBasicExecutionRole권한을 줬음을 볼 수 있다.


awslambdasimpleapp1_7

오른쪽에 Upload from 을 누르면 jar 또는 zip file로 lambda에 배포할 수 있다.


Test Lambda function

awslambdasimpleapp1_8

Aws lambda에서는 테스트를 진행할 수 있는데 보다시피 에러가 발생했다. 자세히 보도록 하자.


Error : class not found

{
  "errorMessage": "Class not found: example.Hello",
  "errorType": "java.lang.ClassNotFoundException"
}

다음과 같은 오류가 나왔는데 이것은 아까 jar file을 업로드할 때 Handler class의 위치를 내가 만든 package위치로 안했기 때문에 발생한 오류이다.

내가 작성한 Handler.javame.practice.stringreverseapi에 있음을 알 수 있다.

package me.practice.stringreverseapi;

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.function.Function;

@Component
public class Handler {
	@Bean
	public Function<String, String> reverse() {
		return (inputString) -> new StringBuilder(inputString).reverse().toString();
	}
}

awslambdasimpleapp1_9

me.practice.stringreverseapi.Handler::reverse로 바꿔줬다.


RE: Test Lambda function

RE: Error : class not found

{
  "errorMessage": "Class not found: me.practice.stringreverseapi.Handler",
  "errorType": "java.lang.ClassNotFoundException"
}

이게 어떻게된 것일까…? 이 부분에 대해서 몇시간의 삽질 끝에…. 결국엔 또 공식문서로 회귀했다.

awslambdasimpleapp1_10

음… 한글이라 약간 헷갈려서 잠시 영어로 바꾼 뒤 해석해보자.

To create a deployment package with your function’s code and dependencies, use the Zip build type.

그러니깐 내 lambda function code(Handler.java)와 dependencies를 담음 package를 배포하고 싶다면 zip build type을 사용하라고 했다. 이거를…못봐서 2시간을 삽질 했다는게 믿기지가 않는다.

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

문서에서 제공해준 스크립트를 build.gradle에 추가하였다. 그리고 다음과 같은 명령어로 zip file build를 해줬다.

./gradlew buildZip

awslambdasimpleapp1_11

This build configuration produces a deployment package in the build/distributions directory.

build/distributions directory에 build결과물이 생성된다고 한다.

awslambdasimpleapp1_12

제발 잘 되길 바라면서…


Uploading a deployment package with the Lambda API

update-function-code command를 사용해서 aws cli로 zip file을 lambda function에 배포할 수 있는 코드가 있길래 사용해봤다.

aws lambda update-function-code --function-name MY_FUNCTION_NAME --zip-file fileb://MY_BUILD_ZIP_FILE.zip

형식으로 쓰면 된다. 클릭하기 귀찮았는데 편하게 배포할 수 있을 것 같다.


RE:RE: Test Lambda function

awslambdasimpleapp1_13

됐…?

awslambdasimpleapp1_14

?? olleH 가 나와야 하는데…?

awslambdasimpleapp1_15

제 output어디간지 아시는 분~?


우리는 dependency를 받을 때

implementation group: 'com.amazonaws', name: 'aws-lambda-java-core', version: '1.2.1'

를 받았던 것을 기억할 것이다. 왜 aws-lambda-java-core가 필요할까?

com.amazonaws:aws-lambda-java-core (required) – Defines handler method interfaces and the context object that the runtime passes to the handler. If you define your own input types, this is the only library that you need.

그러니깐 aws lambda handler가 어떤 입력이 들어오는지 알아야 할 것 아닌가. aws lambda runtime에 handler로 오는 객체를 처리하기 위해서 필요한 것인데 지금 우리의 코드는 그렇지 않다. 이제 해결해 보도록 하자.


현재 내가 spring cloud로 작성한 코드는 아래와 같다.

@Component
public class Handler {
	@Bean
	public Function<String, String> reverse() {
		return (inputString) -> new StringBuilder(inputString).reverse().toString();
	}
}

그러나 예제의 코드는 interface를 구현했기에 똑같이 바꾸고 해봤다.

...
import com.amazonaws.services.lambda.runtime.Context; 
import com.amazonaws.services.lambda.runtime.RequestHandler; // aws lambda 와 관련된 package name을 가졌음을 볼 수 있다.
...

public class Handler implements RequestHandler<String, String> {
	@Override
	public String handleRequest(String input, Context context) {
		return new StringBuilder(input).reverse().toString();
	}

//	@Bean
//	public Function<String, String> reverse() {
//		return (inputString) -> new StringBuilder(inputString).reverse().toString();
//	}
}

method-name이 바뀌었기에 당연히 Handler 위치로 다음과 같이 수정해줬다.

me.practice.stringreverseapi.Handler::handleRequest


RE:RE:RE: Test Lambda function

awslambdasimpleapp1_16

두근거린다… 제발… 착하게 살테니 한번만…

awslambdasimpleapp1_17

정상적으로 동작한다!!


spring-cloud는 결국…여기서는 필요없게 됐다는 사실


Spring-cloud 뭐냐? ㅋㅋㅋ

다음과 같이 쓰일 수 있다.

// LambdaHandler.java
...
import org.springframework.cloud.function.adapter.aws.SpringBootRequestHandler;
...

// 여기 클래스의 Generic 이 바로 Deserialize 해주는 역할
public class LambdaHandler extends SpringBootRequestHandler<OrderRequestDto, Object> {

}
@Slf4j
@RequiredArgsConstructor
@Component
public class APICollections {
	private final OrderService orderService;

	@Bean
	public Function<OrderRequestDto, OrderResponseDto> registerOrderItem() {
		orderService.getOrders()
				.forEach(order -> log.info(order.toString()));
		return orderService::orderItem;
	}

	@Bean
	public Supplier<List<OrderResponseDto>> getOrderList() {
		return orderService::getOrders;
	}
}

바로 위와 같이 @Bean으로 등록한 메서드를 spring cloud의 SpringBootRequestHandler<E, O> 를 사용하면 lambda에서는 그저 Handler method이름만 바꿔줌으로써 lambda function을 정의할 수 있다.


이렇게 쓰면 좋을 것 같은데…?

백발백준 프로젝트에서 API별로 @Bean을 등록한 다음에 같은 zip file, 다른 handler method이름으로 lambda function에 배포한다면 프로젝트 관리가 쉬워질 것 같다는 생각을 해봤다. 내가 생각한 것은

com.example.lambdaprac
ㄴSpringBootApplication.java
ㄴhandlers
	ㄴrecommendationHandler.java
	ㄴproblemMigrationHandler.java
	ㄴuserApiHandler.java
	...
ㄴservice
ㄴrepository
ㄴdomain
ㄴcontroller
...

와 같이 handlers package를 만들어서 각각의 lambda function이 될 handler method를 만들고, handler method는 기존에 존재하는 프로젝트의 메서드를 쓰면 되는 것이다!

물론 아직 해보지 않았기에 직접 해보는게 빠를 것 같다.


Register Order item API

몰랐던 것들

  1. python으로 lambda application을 만들었을 때에는 def lambda_handler(event, context)와 같이 비교적 직관적인 위치에 핸들러 함수가 존재했다. 하지만 Java는 package구조가 존재하기에 어디에 핸들러 함수를 만들어야 하는지 몰랐다.
    • 핸들러 함수의 parameter는 어떤 타입인지 궁금했다.
    • 각 parameter를 직접 parsing해서 사용해야하는 것인지 궁금했다.
  2. Spring boot로 어플리케이션을 만들건데 외부 라이브러리를 어떻게 가져올지 고민이다.
    • python 으로 할 때에는 lambda layer로 해결. Java도 똑같이 하면 되는지 궁금했다.
  3. packge 구성을 어떻게 해야할지 몰랐다.
    • 어떤 의존성이 필요한지


알게된 것들

  1. 공식문서를 보고 하면 된다.
  2. 의존성과 함께 배포 package를 만들기 위해서는 zip 으로 build를 해야한다. 공식문서
  3. 평소에 Spring project만들던대로 하면 된다.


알게된 것들을 바탕으로 다음 포스팅에서는 간단한 주문 어플리케이션을 만들어 볼 것이다.


Reference

aws lambda 사용법

aws lambda handler:java - aws docs

spring cloud Function

spring cloud dependency build error

spring cloud docs

spring boot aws lambda로 배포하기

aws lambda zip file deploy - aws docs