안녕하세요. 오랜만입니다. 최근에 서버리스 아키텍처, 자동화, 그리고 MVP(최소 기능 제품) 개발에 관심을 가지게 되면서, 두 가지 프로젝트를 진행하게 되었습니다. 바로 Lambda-trigger-pipeline과 Spring-lambda-MVP입니다.
이 두 프로젝트를 통해 자동화를 더욱 쉽고 효율적으로 구현하고자 했는데요, 이번 글에서는 제가 왜 이 프로젝트들을 시작하게 되었는지, 어떤 문제를 겪었고 어떻게 해결했는지, 그리고 그 과정에서 무엇을 배웠는지 여러분과 공유하고자 합니다. 자동화와 서버리스 기술에 관심이 있으신 분들이라면 제 경험이 도움이 되길 바랍니다.
처음에는 기존 시스템들이 유연성이 부족하다고 느꼈습니다. 인프라를 직접 관리하다 보니 반복적인 작업이 많아지고, 빠르게 개발하고 배포하는 데 어려움이 있었습니다. 특히 작은 변경 사항에도 많은 시간을 소모하게 되어 효율성을 높일 방법을 찾고 있었습니다.
이때 서버리스 아키텍처가 눈에 들어왔습니다. 서버 관리를 하지 않아도 되며, 필요한 시점에 필요한 만큼의 리소스를 사용할 수 있다는 점이 매력적이었습니다. AWS Lambda를 사용하면 인프라 관리에 대한 부담을 줄이고 비즈니스 로직에만 집중할 수 있을 것 같았습니다.
AWS Lambda가 다양한 AWS 서비스와 유기적으로 연결될 수 있다는 점을 알게 되면서, 자동으로 파이프라인을 트리거하는 Lambda-trigger-pipeline 프로젝트를 시작하게 되었습니다. 또한, Spring-lambda-MVP 프로젝트를 통해 Spring Boot와 AWS Lambda를 결합하여 빠르게 MVP를 구축하고 배포할 수 있겠다는 생각이 들었습니다.
Spring-lambda-MVP는 Spring Boot와 AWS Lambda를 결합하여 빠르게 MVP를 구축하고 배포하는 데 중점을 둔 프로젝트입니다. 서버 설정의 복잡함 없이 비즈니스 로직에만 집중할 수 있어 개발 효율성을 높일 수 있었습니다.
spring-lambda-mvp/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com.example.demo/
│ │ │ ├── handler/
│ │ │ │ └── StreamLambdaHandler.java
│ │ │ └── user/
│ │ │ ├── User.java
│ │ │ ├── UserController.java
│ │ │ ├── UserRepository.java
│ │ │ └── UserService.java
│ │ └── resources/
│ │ └── application.yml
├── build.gradle
└── gradlew
src/main/java/com.example.demo/handler/StreamLambdaHandler.java
AWS Lambda와의 통합을 처리하는 핸들러 클래스입니다. Lambda 함수가 실행될 때 요청을 받아 Spring 애플리케이션으로 전달하고, 응답을 반환하는 역할을 합니다.src/main/java/com.example.demo/user/
사용자 관리와 관련된 모든 클래스들이 위치합니다.User.java
: 사용자 정보를 담는 엔티티 클래스입니다.UserController.java
: 사용자 관련 API 엔드포인트를 제공하는 컨트롤러 클래스입니다.UserService.java
: 사용자 비즈니스 로직을 처리하는 서비스 클래스입니다.UserRepository.java
: 사용자 데이터를 데이터베이스와 연동하는 리포지토리 인터페이스입니다.src/main/resources/application.yml
애플리케이션 설정 파일로, 데이터베이스 연결 정보 및 기타 설정들이 포함되어 있습니다.build.gradle
Gradle 빌드 설정 파일입니다.gradlew
Gradle Wrapper 실행 스크립트로, 시스템에 Gradle이 설치되어 있지 않아도 빌드를 가능하게 합니다.public class StreamLambdaHandler implements RequestStreamHandler {
private static final SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
static {
try {
handler = SpringLambdaContainerHandler.getAwsProxyHandler(Application.class);
} catch (ContainerInitializationException e) {
e.printStackTrace();
throw new RuntimeException("Could not initialize Spring framework", e);
}
}
@Override
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
throws IOException {
handler.proxyStream(inputStream, outputStream, context);
}
}
번호 | 메소드 | 엔드포인트 | 설명 | 요청 데이터 (JSON) |
---|---|---|---|---|
1 | GET | /users/list | 모든 사용자 목록 조회 | 없음 |
2 | GET | /users/{username} | 특정 사용자 정보 조회 | 경로 변수 username 필요 |
3 | GET | /users/search | 사용자 이름으로 검색 | 쿼리 파라미터 username 필요 |
4 | POST | /users/register | 새로운 사용자 등록 | { "username": "string", "password": "string" } |
5 | POST | /users/login | 사용자 로그인 | { "username": "string", "password": "string" } |
Lambda-trigger-pipeline은 AWS Lambda를 트리거로 사용하는 CI/CD(지속적 통합 및 배포) 자동화 시스템입니다. 특정 이벤트나 조건이 충족되면 Lambda 함수가 자동으로 파이프라인을 시작하여 코드를 빌드하고 배포합니다.
프로젝트 자동화를 위해 세 개의 GitHub Actions 워크플로우 파일을 작성했어요. 각각의 파일이 어떤 역할을 하는지 자세히 설명해드릴게요.
build-and-generate-openapi.yml
역할: Spring Boot 애플리케이션을 빌드하고, OpenAPI 스펙을 생성하여 S3에 업로드해요.
주요 내용:
spring-lambda-mvp
레포지토리의 코드를 가져와요./v3/api-docs
엔드포인트를 호출하여 OpenAPI 스펙을 가져와요.특이 사항:
nohup
과 &
를 사용하여 애플리케이션을 백그라운드에서 실행하고, sleep
으로 애플리케이션이 시작될 때까지 대기해요.deploy-lambda.yml
역할: 빌드된 JAR 파일을 사용하여 Lambda 함수를 생성하거나 업데이트해요.
주요 내용:
update-function-code
로 코드를 업데이트해요.create-function
으로 함수를 생성해요.특이 사항:
ROLE_ARN
)과 핸들러 정보를 지정해요.api-gateway-setup.yml
역할: S3에 업로드된 OpenAPI 스펙을 기반으로 API Gateway를 설정하고, Lambda 함수와 연동해요.
주요 내용:
dev
or prod
)로 배포해요.특이 사항:
aws apigateway
와 aws lambda
CLI 명령어를 조합하여 API와 Lambda를 연동해요.이 파이프라인을 사용하려면 레포지토리에 다음과 같은 시크릿을 설정해야 합니다:
PERSONAL_ACCESS_TOKEN
: spring-lambda-mvp
레포지토리에 접근할 수 있는 GitHub Personal Access Token.AWS_ACCESS_KEY_ID
: AWS 계정에 대한 접근 키 ID.AWS_SECRET_ACCESS_KEY
: AWS 계정에 대한 시크릿 접근 키.AWS_REGION
: AWS 리전 정보.LAMBDA_FUNCTION_NAME
: 배포할 Lambda 함수의 이름.S3_BUCKET_NAME
: 빌드 아티팩트를 저장할 S3 버킷 이름.DB_URL
: Spring Boot 애플리케이션의 데이터베이스 URL.DB_USERNAME
: 데이터베이스 사용자 이름.DB_PASSWORD
: 데이터베이스 비밀번호.DB_DRIVER
: 데이터베이스 드라이버.AWS API Gateway를 통해 Lambda 함수와 HTTP 엔드포인트를 연결하여 RESTful API를 구축했습니다. 아래는 설정한 API 리소스와 메서드의 상세 정보입니다:
리소스 ID | 메서드 | 경로 | 상태 | Lambda 권한 ARN |
---|---|---|---|---|
6mbp7t | GET | /users/list | 성공 | arn:aws:execute-api:...pezn0se8t1/*/GET/users/list |
dy3qm2 | GET(Path Parameters) | /users/{username} | 성공 | arn:aws:execute-api:...pezn0se8t1/*/GET/users/{username} |
xfvg26 | GET(Query String Parameters) | /users/search | 성공 | arn:aws:execute-api:...pezn0se8t1/*/GET/users/search |
m2ahn1 | POST | /users/register | 성공 | arn:aws:execute-api:...pezn0se8t1/*/POST/users/register |
suqx3s | POST | /users/login | 성공 | arn:aws:execute-api:...pezn0se8t1/*/POST/users/login |
아래는 프로젝트에서 사용된 API Gateway 리소스 설정과 관련된 로그를 정리한 것입니다.
각 리소스 ID에 해당하는 로그를 통해 API 메서드가 AWS Lambda 함수와 어떻게 통합되었는지 확인할 수 있습니다.
리소스 ID: 6mbp7t
메서드: GET
경로: /users/list
처리 로그:
Processing METHOD: GET on RESOURCE_ID: 6mbp7t
{
"type": "AWS_PROXY",
"httpMethod": "POST",
"uri": "arn:aws:apigateway:[지역]:lambda:path/2015-03-31/functions/arn:aws:lambda:[지역]:[계정 ID]:function:[함수 이름]/invocations",
"passthroughBehavior": "WHEN_NO_MATCH",
"timeoutInMillis": 29000,
"cacheNamespace": "6mbp7t",
"cacheKeyParameters": []
}
리소스 ID: dy3qm2
메서드: GET (경로 파라미터)
경로: /users/{username}
처리 로그:
Processing METHOD: GET on RESOURCE_ID: dy3qm2
{
"type": "AWS_PROXY",
"httpMethod": "POST",
"uri": "arn:aws:apigateway:[지역]:lambda:path/2015-03-31/functions/arn:aws:lambda:[지역]:[계정 ID]:function:[함수 이름]/invocations",
"passthroughBehavior": "WHEN_NO_MATCH",
"timeoutInMillis": 29000,
"cacheNamespace": "dy3qm2",
"cacheKeyParameters": []
}
리소스 ID: xfvg26
메서드: GET (쿼리 스트링 파라미터)
경로: /users/search
처리 로그:
Processing METHOD: GET on RESOURCE_ID: xfvg26
{
"type": "AWS_PROXY",
"httpMethod": "POST",
"uri": "arn:aws:apigateway:[지역]:lambda:path/2015-03-31/functions/arn:aws:lambda:[지역]:[계정 ID]:function:[함수 이름]/invocations",
"passthroughBehavior": "WHEN_NO_MATCH",
"timeoutInMillis": 29000,
"cacheNamespace": "xfvg26",
"cacheKeyParameters": []
}
리소스 ID: m2ahn1
메서드: POST
경로: /users/register
처리 로그:
Processing METHOD: POST on RESOURCE_ID: m2ahn1
{
"type": "AWS_PROXY",
"httpMethod": "POST",
"uri": "arn:aws:apigateway:[지역]:lambda:path/2015-03-31/functions/arn:aws:lambda:[지역]:[계정 ID]:function:[함수 이름]/invocations",
"passthroughBehavior": "WHEN_NO_MATCH",
"timeoutInMillis": 29000,
"cacheNamespace": "m2ahn1",
"cacheKeyParameters": []
}
리소스 ID: suqx3s
메서드: POST
경로: /users/login
처리 로그:
Processing METHOD: POST on RESOURCE_ID: suqx3s
{
"type": "AWS_PROXY",
"httpMethod": "POST",
"uri": "arn:aws:apigateway:[지역]:lambda:path/2015-03-31/functions/arn:aws:lambda:[지역]:[계정 ID]:function:[함수 이름]/invocations",
"passthroughBehavior": "WHEN_NO_MATCH",
"timeoutInMillis": 29000,
"cacheNamespace": "suqx3s",
"cacheKeyParameters": []
}
Spring Boot 애플리케이션을 AWS Lambda에 배포하고, API Gateway를 통해 HTTP 요청을 처리하는 전체적인 과정을 그림으로 표현해보았습니다:
Lambda-trigger-pipeline과 Spring-lambda-MVP 프로젝트를 통해 서버리스 아키텍처와 자동화의 성능을 느낄 수 있었습니다.
프로젝트의 상세 내용은 아래 링크를 통해 확인하실 수 있습니다:
읽어주셔서 감사합니다!