본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 19. · 5 Views
서버리스 REST API 구축 완벽 가이드
AWS Lambda와 API Gateway, DynamoDB를 활용하여 완전한 서버리스 REST API를 구축하는 실무 프로젝트입니다. SAM 프레임워크를 사용한 배포부터 테스트까지 전 과정을 다룹니다.
목차
1. 프로젝트 설계
김개발 씨는 스타트업에 입사한 첫날, 팀장님으로부터 흥미로운 미션을 받았습니다. "우리 회사 할 일 관리 API를 만들어주세요.
근데 서버 관리는 하지 말고요." 서버 없이 API를 만든다니, 무슨 말일까요?
Todo REST API Resources: # DynamoDB 테이블 TodoTable: Type: AWS::DynamoDB::Table Properties: TableName: Todos AttributeDefinitions: - AttributeName: id AttributeType: S KeySchema: - AttributeName: id KeyType: HASH BillingMode: PAY_PER_REQUEST # Lambda 함수들 CreateTodoFunction: Type: AWS::Serverless::Function Properties: Handler: handlers/create.handler Runtime: nodejs18.x Environment: Variables: TABLE_NAME: !Ref TodoTable Events: CreateApi: Type: Api Properties: Path: /todos Method: post
다음 코드를 살펴봅시다.
// template.yaml - SAM 프로젝트 구조 정의
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
김개발 씨는 팀장님의 말씀에 고개를 갸우뚱했습니다. 대학에서 배운 것은 Node.js로 Express 서버를 만들고, EC2 인스턴스에 배포하는 방식이었습니다.
그런데 서버 없이 API를 만든다니요? 팀장님은 웃으며 화이트보드에 그림을 그리기 시작했습니다.
"서버리스는 말 그대로 서버가 없다는 뜻이 아니에요. 우리가 서버를 관리하지 않아도 된다는 의미죠." 서버리스 아키텍처란 무엇일까요?
쉽게 비유하자면, 서버리스는 마치 택시를 타는 것과 같습니다. 자가용을 사면 주차비, 보험료, 수리비 등 계속 돈이 나갑니다.
하지만 택시는 탈 때만 요금을 냅니다. 서버리스도 마찬가지입니다.
코드가 실행될 때만 비용이 발생하고, 아무도 API를 호출하지 않으면 비용이 0원입니다. 기존 방식의 문제점은 무엇이었을까요?
전통적인 방식에서는 EC2 같은 서버를 24시간 켜두어야 했습니다. 새벽 3시에 사용자가 없어도 서버는 돌아가고 있고, 비용은 계속 나갑니다.
게다가 갑자기 사용자가 몰리면 서버가 다운될 수 있습니다. 스케일링 설정도 복잡하고, 보안 패치도 직접 해야 합니다.
서버리스는 이런 문제를 어떻게 해결할까요? AWS Lambda는 코드만 업로드하면 AWS가 알아서 실행해줍니다.
사용자가 API를 호출할 때만 Lambda 함수가 깨어나서 작동하고, 끝나면 다시 잠듭니다. 동시에 100명이 접속하면 Lambda가 자동으로 100개로 늘어납니다.
우리는 그저 코드만 작성하면 됩니다. API Gateway는 HTTP 요청의 관문 역할을 합니다.
클라이언트가 POST /todos 요청을 보내면, API Gateway가 받아서 적절한 Lambda 함수로 전달합니다. CORS 설정, 인증, 요청 검증 같은 기능도 제공합니다.
DynamoDB는 서버리스 데이터베이스입니다. MySQL처럼 서버를 띄울 필요가 없습니다.
데이터를 저장하고 조회하는 횟수만큼만 비용을 냅니다. 속도도 매우 빠릅니다.
프로젝트 구조는 어떻게 잡아야 할까요? 우리가 만들 Todo API는 전형적인 CRUD 구조입니다.
Create(생성), Read(조회), Update(수정), Delete(삭제) 네 가지 기능이 필요합니다. 각 기능마다 Lambda 함수를 하나씩 만들 것입니다.
김개발 씨는 메모장을 꺼내 정리했습니다. POST /todos로 할 일을 생성하고, GET /todos로 목록을 조회하고, PUT /todos/:id로 수정하고, DELETE /todos/:id로 삭제합니다.
각 엔드포인트마다 Lambda 함수가 매핑됩니다. **SAM(Serverless Application Model)**은 무엇일까요?
SAM은 서버리스 애플리케이션을 쉽게 만들고 배포할 수 있게 해주는 AWS의 프레임워크입니다. template.yaml 파일 하나에 모든 리소스를 정의하면, SAM이 알아서 Lambda, API Gateway, DynamoDB를 만들어줍니다.
마치 요리책의 레시피처럼, 재료 목록만 적으면 SAM이 요리를 완성해줍니다. 왜 여러 개의 Lambda 함수로 나누는 걸까요?
하나의 거대한 Lambda 함수에 모든 로직을 넣을 수도 있습니다. 하지만 그러면 코드가 복잡해지고, 한 부분을 수정할 때마다 전체를 다시 배포해야 합니다.
기능별로 나누면 각 함수가 단순해지고, 독립적으로 배포하고 테스트할 수 있습니다. 실제 프로젝트에서는 어떻게 시작할까요?
먼저 AWS CLI와 SAM CLI를 설치합니다. 그 다음 sam init 명령어로 프로젝트 템플릿을 생성합니다.
SAM이 기본 구조를 만들어주면, 우리는 비즈니스 로직만 작성하면 됩니다. 로컬에서 sam local start-api 명령으로 테스트도 할 수 있습니다.
김개발 씨는 설레는 마음으로 터미널을 열었습니다. 이제 본격적으로 서버리스 REST API를 만들어볼 시간입니다.
실전 팁
💡 - SAM CLI 설치 후 sam init으로 프로젝트를 시작하면 기본 구조가 자동 생성됩니다
- 로컬 테스트는
sam local start-api로 가능하며, Docker가 필요합니다 - 비용 절감을 위해 DynamoDB는 On-Demand 모드를 사용하세요
2. DynamoDB 테이블 생성
김개발 씨가 가장 먼저 마주한 질문은 "데이터를 어디에 저장하지?"였습니다. MySQL 같은 관계형 데이터베이스를 쓸까요?
아니면 MongoDB 같은 NoSQL을 쓸까요? 팀장님은 "DynamoDB를 써보세요"라고 권했습니다.
DynamoDB는 AWS의 완전관리형 NoSQL 데이터베이스입니다. 테이블은 파티션 키로 데이터를 구분하며, 필요시 정렬 키를 추가할 수 있습니다.
스키마가 자유로워서 각 항목마다 다른 필드를 가질 수 있고, 자동으로 확장되어 성능 걱정이 없습니다.
다음 코드를 살펴봅시다.
// handlers/create.js - DynamoDB에 데이터 저장
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, PutCommand } = require('@aws-sdk/lib-dynamodb');
const { v4: uuidv4 } = require('uuid');
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
exports.handler = async (event) => {
// 요청 본문 파싱
const body = JSON.parse(event.body);
// 새 할 일 항목 생성
const todo = {
id: uuidv4(), // 고유 ID 생성
title: body.title,
completed: false,
createdAt: new Date().toISOString()
};
// DynamoDB에 저장
await docClient.send(new PutCommand({
TableName: process.env.TABLE_NAME,
Item: todo
}));
return {
statusCode: 201,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(todo)
};
};
김개발 씨는 처음에 DynamoDB가 낯설었습니다. 대학에서는 MySQL만 배웠거든요.
"테이블은 알겠는데, 파티션 키가 뭐죠?" 선배 박시니어 씨가 웃으며 설명해줍니다. NoSQL과 관계형 데이터베이스의 차이는 무엇일까요?
MySQL 같은 관계형 데이터베이스는 마치 엑셀 스프레드시트와 같습니다. 미리 컬럼을 정의해야 하고, 모든 행이 같은 구조를 가져야 합니다.
반면 DynamoDB는 JSON 문서를 저장하는 서랍장과 같습니다. 각 문서가 서로 다른 필드를 가질 수 있습니다.
파티션 키는 DynamoDB의 핵심 개념입니다. 쉽게 비유하자면, 파티션 키는 도서관의 청구기호와 같습니다.
책을 찾을 때 청구기호로 바로 찾아가듯이, DynamoDB도 파티션 키로 데이터를 찾습니다. 우리 Todo 앱에서는 각 할 일의 id가 파티션 키입니다.
왜 관계형 데이터베이스 대신 DynamoDB를 쓸까요? 관계형 데이터베이스는 복잡한 조인이 필요한 경우에 좋습니다.
하지만 우리 Todo 앱은 단순합니다. 할 일을 ID로 조회하고, 목록을 가져오고, 수정하고, 삭제하면 됩니다.
조인이 필요 없습니다. 이런 경우 DynamoDB가 훨씬 빠르고 저렴합니다.
DynamoDB의 데이터 타입은 어떤 것들이 있을까요? 문자열(String), 숫자(Number), 불린(Boolean) 같은 기본 타입은 물론이고, 리스트(List)와 맵(Map)도 저장할 수 있습니다.
우리 Todo 항목을 보면 id와 title은 문자열, completed는 불린, createdAt은 ISO 문자열로 저장합니다. 코드를 하나씩 살펴볼까요?
먼저 AWS SDK를 임포트합니다. @aws-sdk/client-dynamodb는 저수준 API이고, @aws-sdk/lib-dynamodb는 JavaScript 객체를 자동으로 DynamoDB 형식으로 변환해주는 고수준 API입니다.
우리는 편리한 고수준 API를 사용합니다. uuidv4()는 고유한 ID를 생성하는 함수입니다.
MySQL에서는 auto increment를 썼지만, 분산 시스템인 DynamoDB에서는 UUID를 사용하는 것이 일반적입니다. UUID는 충돌 가능성이 거의 없는 고유 문자열입니다.
PutCommand는 DynamoDB에 항목을 저장하는 명령입니다. TableName은 환경변수에서 가져오고, Item에 저장할 객체를 넣습니다.
같은 ID로 다시 PutCommand를 실행하면 덮어쓰기됩니다. 실무에서 주의할 점이 있습니다.
DynamoDB는 **결과적 일관성(Eventual Consistency)**을 기본으로 합니다. 데이터를 저장한 직후 바로 조회하면 아직 반영되지 않았을 수 있습니다.
하지만 보통 1초 이내에 반영되므로 대부분의 경우 문제없습니다. 강한 일관성이 필요하면 ConsistentRead: true 옵션을 사용할 수 있습니다.
테이블 용량 모드는 두 가지가 있습니다. Provisioned 모드는 초당 읽기/쓰기 용량을 미리 설정합니다.
예측 가능한 트래픽에 적합합니다. On-Demand 모드는 사용한 만큼만 비용을 냅니다.
스타트업이나 트래픽 예측이 어려운 경우에 좋습니다. 우리는 On-Demand를 선택했습니다.
김개발 씨는 로컬에서 DynamoDB Local을 설치하고 테스트를 돌려봤습니다. 데이터가 정상적으로 저장되는 것을 확인하고 미소를 지었습니다.
"생각보다 간단한데요?"
실전 팁
💡 - UUID는 uuid 패키지로 생성하며, 충돌 걱정 없이 분산 환경에서 안전합니다
- DynamoDB Local을 사용하면 로컬에서 AWS 비용 없이 테스트할 수 있습니다
- 환경변수로 테이블 이름을 받아 하드코딩을 피하세요
3. CRUD Lambda 함수 작성
데이터베이스 설계를 마친 김개발 씨는 이제 본격적으로 비즈니스 로직을 작성할 차례입니다. "CREATE는 했으니까 이제 READ, UPDATE, DELETE를 만들면 되겠네요." 하지만 각 함수마다 미묘하게 다른 점들이 있었습니다.
CRUD는 Create, Read, Update, Delete의 약자로 데이터 관리의 기본 작업입니다. 각 작업마다 독립된 Lambda 함수를 만들어 단일 책임 원칙을 지킵니다.
DynamoDB의 GetCommand, ScanCommand, UpdateCommand, DeleteCommand를 사용하여 데이터를 조작합니다.
다음 코드를 살펴봅시다.
// handlers/get.js - 특정 할 일 조회
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, GetCommand } = require('@aws-sdk/lib-dynamodb');
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
exports.handler = async (event) => {
// URL 경로에서 ID 추출
const id = event.pathParameters.id;
// DynamoDB에서 항목 조회
const result = await docClient.send(new GetCommand({
TableName: process.env.TABLE_NAME,
Key: { id }
}));
// 항목이 없으면 404 반환
if (!result.Item) {
return {
statusCode: 404,
body: JSON.stringify({ message: 'Todo not found' })
};
}
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result.Item)
};
};
김개발 씨는 하나의 Lambda 함수에 모든 로직을 넣을까 고민했습니다. Express로 개발할 때는 하나의 서버에 여러 라우트를 두었으니까요.
하지만 박시니어 씨는 다른 조언을 해줬습니다. "서버리스에서는 함수를 작게 나누는 게 좋아요.
각 함수가 하나의 일만 하도록 만드세요." 단일 책임 원칙이란 무엇일까요? 쉽게 비유하자면, 식당에서 주방장, 웨이터, 계산원이 각자의 역할만 하는 것과 같습니다.
한 사람이 모든 일을 하면 복잡하고 비효율적입니다. Lambda 함수도 마찬가지입니다.
할 일을 조회하는 함수는 조회만 하고, 삭제하는 함수는 삭제만 합니다. GetCommand와 ScanCommand의 차이는 무엇일까요?
GetCommand는 파티션 키로 하나의 항목을 빠르게 가져옵니다. 시간 복잡도가 O(1)입니다.
반면 ScanCommand는 테이블 전체를 스캔합니다. 데이터가 많으면 느리고 비용도 많이 듭니다.
가능하면 GetCommand나 QueryCommand를 사용해야 합니다. 목록 조회는 어떻게 구현할까요?
Todo 목록을 가져오려면 ScanCommand를 사용할 수밖에 없습니다. 모든 할 일을 다 가져와야 하니까요.
하지만 실무에서는 페이지네이션을 구현합니다. Limit 옵션으로 한 번에 가져올 개수를 제한하고, LastEvaluatedKey로 다음 페이지를 가져옵니다.
Update는 어떻게 작동할까요? UpdateCommand는 전체를 덮어쓰는 것이 아니라 특정 필드만 수정합니다. 예를 들어 completed 필드만 true로 바꾸고 싶다면, UpdateExpression을 사용합니다.
SET completed = :completed 같은 형태로 작성합니다. 마치 SQL의 UPDATE 문과 비슷합니다.
코드를 자세히 살펴봅시다. event.pathParameters는 API Gateway가 URL 경로에서 추출한 파라미터입니다.
/todos/123 요청이 오면 id가 "123"이 됩니다. 이것을 Key로 사용해서 DynamoDB를 조회합니다.
result.Item이 undefined면 해당 ID의 할 일이 없다는 뜻입니다. 이 경우 404 Not Found를 반환합니다.
있으면 200 OK와 함께 데이터를 반환합니다. 클라이언트가 명확한 상태 코드를 받을 수 있도록 해야 합니다.
Delete는 가장 간단합니다. DeleteCommand에 Key만 전달하면 됩니다. 항목이 없어도 에러가 나지 않습니다.
삭제에 성공하면 204 No Content를 반환하는 것이 RESTful한 방식입니다. 실무에서 흔한 실수가 있습니다.
초보 개발자들은 종종 에러 처리를 빼먹습니다. DynamoDB 호출이 실패할 수도 있습니다.
네트워크 문제, 권한 문제, 용량 초과 등 여러 이유가 있습니다. try-catch로 에러를 잡고, 적절한 상태 코드를 반환해야 합니다.
환경변수 사용도 중요합니다. 테이블 이름을 코드에 하드코딩하면 안 됩니다.
개발 환경과 프로덕션 환경이 다를 수 있습니다. SAM 템플릿에서 환경변수로 주입하면, 코드 변경 없이 다른 테이블을 사용할 수 있습니다.
김개발 씨는 네 개의 Lambda 함수를 모두 작성했습니다. 각 함수는 50줄 이내로 매우 간결했습니다.
"이렇게 작게 나누니까 이해하기도 쉽고 테스트하기도 편하네요!"
실전 팁
💡 - GetCommand는 O(1)로 빠르지만, ScanCommand는 전체 스캔이므로 데이터가 많으면 느립니다
- UpdateCommand의 UpdateExpression을 사용하면 일부 필드만 효율적으로 수정할 수 있습니다
- 에러 처리는 try-catch로 감싸고 클라이언트에게 명확한 상태 코드를 반환하세요
4. API Gateway 설정
Lambda 함수들을 다 만들었지만, 아직 외부에서 호출할 수 없습니다. 김개발 씨는 "어떻게 HTTP 요청을 Lambda로 연결하지?"라는 의문이 들었습니다.
바로 이때 필요한 것이 API Gateway입니다.
API Gateway는 HTTP 요청을 받아서 Lambda 함수로 라우팅하는 관문입니다. RESTful API 엔드포인트를 정의하고, CORS 설정, 인증, 요청 검증 등을 처리합니다.
SAM 템플릿에서 Events 속성으로 간단히 설정할 수 있으며, 자동으로 API Gateway와 Lambda를 연결해줍니다.
다음 코드를 살펴봅시다.
# template.yaml - API Gateway 이벤트 설정
Resources:
# 목록 조회 함수
ListTodosFunction:
Type: AWS::Serverless::Function
Properties:
Handler: handlers/list.handler
Runtime: nodejs18.x
Environment:
Variables:
TABLE_NAME: !Ref TodoTable
Policies:
- DynamoDBReadPolicy:
TableName: !Ref TodoTable
Events:
ListApi:
Type: Api
Properties:
Path: /todos
Method: get
# 단건 조회 함수
GetTodoFunction:
Type: AWS::Serverless::Function
Properties:
Handler: handlers/get.handler
Runtime: nodejs18.x
Environment:
Variables:
TABLE_NAME: !Ref TodoTable
Policies:
- DynamoDBReadPolicy:
TableName: !Ref TodoTable
Events:
GetApi:
Type: Api
Properties:
Path: /todos/{id}
Method: get
김개발 씨는 API Gateway 콘솔을 처음 열어봤을 때 머리가 복잡해졌습니다. 리소스, 메서드, 스테이지, 배포...
생소한 용어들이 가득했습니다. 하지만 SAM을 사용하면 훨씬 간단합니다.
API Gateway의 역할은 무엇일까요? 쉽게 비유하자면, API Gateway는 호텔의 프런트 데스크와 같습니다.
손님(클라이언트)이 오면 어떤 방(Lambda 함수)으로 안내할지 결정합니다. 방 키(권한)를 확인하고, 짐(요청 데이터)을 검사합니다.
Lambda 함수는 객실처럼 외부에 직접 노출되지 않고, API Gateway를 통해서만 접근할 수 있습니다. 기존 방식의 문제점은 무엇이었을까요?
AWS 콘솔에서 직접 API Gateway를 설정하려면 클릭을 수십 번 해야 합니다. 리소스를 만들고, 메서드를 추가하고, Lambda와 통합하고, CORS를 설정하고, 스테이지에 배포해야 합니다.
실수하기도 쉽고, 같은 설정을 반복하기도 번거롭습니다. SAM의 Events 속성이 모든 것을 해결합니다.
template.yaml에 Events 섹션만 추가하면, SAM이 알아서 API Gateway를 생성하고 Lambda와 연결합니다. Path는 URL 경로, Method는 HTTP 메서드입니다.
이것만으로 끝입니다. 마치 마법처럼 간단합니다.
경로 파라미터는 어떻게 처리할까요? /todos/{id} 같은 경로를 보면 {id} 부분이 경로 파라미터입니다.
클라이언트가 /todos/abc123을 호출하면, Lambda 함수의 event.pathParameters.id에 "abc123"이 들어갑니다. API Gateway가 자동으로 파싱해서 전달해줍니다.
CORS 문제는 웹 개발자의 고민거리입니다. 브라우저에서 다른 도메인의 API를 호출하면 CORS 에러가 납니다.
예를 들어 localhost:3000에서 api.example.com을 호출하면 막힙니다. API Gateway에서 CORS를 활성화하면 해결됩니다.
SAM에서는 Cors 속성 하나로 설정할 수 있습니다. 실제 SAM 템플릿을 보면 어떤 구조일까요?
각 Lambda 함수마다 Events 섹션이 있습니다. CreateTodoFunction은 POST /todos에, GetTodoFunction은 GET /todos/{id}에 매핑됩니다.
SAM이 배포할 때 하나의 API Gateway를 만들고, 모든 함수를 연결합니다. Policies 속성도 중요합니다.
Lambda 함수가 DynamoDB를 읽거나 쓰려면 권한이 필요합니다. DynamoDBReadPolicy나 DynamoDBCrudPolicy를 추가하면, SAM이 자동으로 IAM 역할을 만들어서 Lambda에 붙여줍니다.
우리는 보안 걱정 없이 코드만 작성하면 됩니다. API Gateway의 요청 검증도 가능합니다.
요청 본문이 올바른 JSON인지, 필수 필드가 있는지 검사할 수 있습니다. 잘못된 요청은 Lambda까지 가지 않고 API Gateway에서 바로 거부됩니다.
이렇게 하면 Lambda 실행 시간을 절약하고 비용을 줄일 수 있습니다. 김개발 씨는 SAM 템플릿에 Events를 추가하고 배포해봤습니다.
API Gateway 엔드포인트 URL이 출력되었습니다. Postman으로 테스트해보니 정상적으로 작동했습니다.
"진짜 쉽네요!"
실전 팁
💡 - SAM의 Events 속성을 사용하면 API Gateway를 자동으로 생성하고 Lambda와 연결합니다
- CORS는
Cors: true한 줄로 활성화할 수 있습니다 - 요청 검증을 API Gateway에서 처리하면 Lambda 비용을 절감할 수 있습니다
5. SAM으로 배포
모든 코드를 작성한 김개발 씨는 드디어 배포할 시간이 왔습니다. "로컬에서는 잘 되는데, AWS에 올리려면 어떻게 하지?" 예전에는 파일을 FTP로 업로드했지만, 서버리스는 다른 방법이 필요합니다.
SAM 배포는 sam build로 패키징하고, sam deploy로 AWS에 업로드하는 과정입니다. SAM은 CloudFormation 스택을 생성하여 모든 리소스를 한 번에 관리합니다.
S3에 코드를 업로드하고, Lambda 함수를 생성하고, API Gateway를 설정하는 모든 과정이 자동화됩니다.
다음 코드를 살펴봅시다.
# 배포 명령어 순서
# 1. 의존성 설치 및 빌드
sam build
# 2. 가이드 모드로 첫 배포 (설정 저장)
sam deploy --guided
# 3. samconfig.toml 예시
version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "todo-api"
s3_bucket = "aws-sam-cli-managed-default-bucket"
s3_prefix = "todo-api"
region = "ap-northeast-2"
capabilities = "CAPABILITY_IAM"
confirm_changeset = true
# 4. 이후 배포는 간단히
sam build && sam deploy
# 5. 로그 확인
sam logs -n CreateTodoFunction --tail
# 6. 스택 삭제 (리소스 모두 제거)
sam delete
김개발 씨는 터미널에 sam deploy를 입력하려다가 멈칫했습니다. "잠깐, 이거 배포되면 비용이 나가는 거 아닌가?" 걱정이 앞섰지만, 팀장님은 웃으며 말했습니다.
"Lambda는 사용한 만큼만 비용이 나와요. 테스트 몇 번 하는 건 거의 무료예요." SAM 배포 프로세스는 어떻게 진행될까요?
쉽게 비유하자면, SAM 배포는 택배를 보내는 것과 같습니다. sam build는 택배를 포장하는 단계입니다.
Node.js 의존성을 설치하고, 코드를 압축합니다. sam deploy는 실제로 택배를 보내는 단계입니다.
S3에 업로드하고, CloudFormation으로 리소스를 생성합니다. sam build는 무엇을 할까요?
build 명령은 .aws-sam 폴더에 빌드 결과물을 만듭니다. 각 Lambda 함수마다 폴더가 생기고, node_modules도 함께 포함됩니다.
프로덕션 의존성만 설치하므로 패키지 크기가 최소화됩니다. TypeScript를 쓴다면 JavaScript로 컴파일도 해줍니다.
sam deploy --guided는 처음 배포할 때 사용합니다. 대화형 모드로 스택 이름, 리전, S3 버킷 등을 물어봅니다.
한 번 설정하면 samconfig.toml 파일에 저장됩니다. 다음부터는 sam deploy만 입력해도 이전 설정을 사용합니다.
매번 타이핑할 필요가 없어서 편리합니다. CloudFormation 스택이란 무엇일까요?
CloudFormation은 AWS 리소스를 코드로 관리하는 서비스입니다. 마치 레고 블록을 조립하는 설명서와 같습니다.
SAM 템플릿을 CloudFormation으로 변환하면, Lambda, API Gateway, DynamoDB, IAM 역할 등이 하나의 스택으로 묶입니다. 스택을 삭제하면 모든 리소스가 한 번에 사라집니다.
배포 중에 무슨 일이 일어날까요? 먼저 SAM이 S3 버킷을 만들거나 기존 버킷을 사용합니다.
코드 패키지를 S3에 업로드합니다. 그 다음 CloudFormation이 변경사항을 분석합니다.
"DynamoDB 테이블 1개, Lambda 함수 4개, API Gateway 1개를 만들겠습니다"라고 알려줍니다. 우리가 승인하면 실제로 생성됩니다.
Capabilities 파라미터는 왜 필요할까요? IAM 역할을 만드는 권한이 필요하기 때문입니다.
CAPABILITY_IAM을 명시하면 "나는 이 템플릿이 IAM 리소스를 만드는 것을 허용합니다"라는 의미입니다. 보안상 명시적으로 동의해야 합니다.
배포가 완료되면 무엇을 확인해야 할까요? CloudFormation 콘솔에서 스택 상태를 볼 수 있습니다.
CREATE_COMPLETE 상태면 성공입니다. Outputs 탭에는 API Gateway URL이 표시됩니다.
이 URL로 실제 API를 호출할 수 있습니다. sam logs 명령으로 로그를 실시간으로 볼 수 있습니다.
CloudWatch Logs에 쌓이는 Lambda 로그를 터미널에서 바로 확인할 수 있습니다. --tail 옵션을 붙이면 실시간으로 스트리밍됩니다.
디버깅할 때 매우 유용합니다. 실무에서 주의할 점이 있습니다.
배포 전에 sam validate로 템플릿 문법을 검증하는 것이 좋습니다. 오타나 잘못된 참조가 있으면 미리 알려줍니다.
배포 후에 에러가 나서 롤백되는 것보다 훨씬 낫습니다. 업데이트 배포는 어떻게 할까요?
코드를 수정한 후 다시 sam build && sam deploy를 실행하면 됩니다. CloudFormation이 변경사항만 적용합니다.
기존 리소스는 유지하고, 변경된 Lambda 함수만 업데이트됩니다. 무중단 배포가 가능합니다.
김개발 씨는 sam deploy를 실행하고 5분 정도 기다렸습니다. "Successfully created/updated stack - todo-api in ap-northeast-2" 메시지가 떴습니다.
API Gateway URL을 복사해서 브라우저에 붙여넣자, "Hello from Lambda!" 메시지가 보였습니다. 성공입니다!
실전 팁
💡 - sam build && sam deploy를 하나의 명령으로 체이닝하면 편리합니다
- samconfig.toml을 Git에 커밋하면 팀원들이 같은 설정으로 배포할 수 있습니다
sam delete로 스택을 삭제하면 모든 리소스가 제거되어 비용 걱정이 없습니다
6. 테스트와 문서화
배포는 끝났지만, 김개발 씨는 불안했습니다. "정말 잘 작동하는 걸까?
나중에 버그가 생기면 어떻게 찾지?" 팀장님은 "테스트 코드를 작성하세요. 그리고 API 문서도 만들어두면 좋아요"라고 조언했습니다.
테스트는 코드가 의도대로 작동하는지 자동으로 검증하는 과정입니다. 단위 테스트로 Lambda 함수 로직을 테스트하고, 통합 테스트로 실제 DynamoDB와의 연동을 확인합니다.
API 문서는 Swagger나 Postman Collection으로 작성하여 다른 개발자가 쉽게 API를 이해하고 사용할 수 있게 합니다.
다음 코드를 살펴봅시다.
// tests/unit/create.test.js - Jest를 사용한 단위 테스트
const { handler } = require('../../handlers/create');
// DynamoDB 모킹
jest.mock('@aws-sdk/lib-dynamodb', () => ({
DynamoDBDocumentClient: {
from: jest.fn(() => ({
send: jest.fn().mockResolvedValue({})
}))
},
PutCommand: jest.fn()
}));
describe('Create Todo Handler', () => {
test('새 할 일을 생성하고 201을 반환한다', async () => {
const event = {
body: JSON.stringify({
title: '테스트 할 일'
})
};
const result = await handler(event);
// 상태 코드 검증
expect(result.statusCode).toBe(201);
// 응답 본문 검증
const body = JSON.parse(result.body);
expect(body.title).toBe('테스트 할 일');
expect(body.completed).toBe(false);
expect(body.id).toBeDefined();
});
test('title이 없으면 400을 반환한다', async () => {
const event = {
body: JSON.stringify({})
};
const result = await handler(event);
expect(result.statusCode).toBe(400);
});
});
김개발 씨는 대학에서 테스트 코드를 배웠지만, 실무에서는 시간이 없어서 스킵하곤 했습니다. 하지만 박시니어 씨는 단호하게 말했습니다.
"서버리스는 테스트가 더 중요해요. 함수가 분리되어 있어서 테스트하기 쉽거든요." 왜 테스트가 중요할까요? 쉽게 비유하자면, 테스트 코드는 자동차의 안전벨트와 같습니다.
사고가 나지 않을 수도 있지만, 만약을 대비해야 합니다. 코드를 수정했을 때 기존 기능이 망가지지 않았는지 자동으로 확인할 수 있습니다.
매번 수동으로 테스트하는 것보다 훨씬 빠르고 정확합니다. 단위 테스트란 무엇일까요?
단위 테스트는 함수 하나하나를 독립적으로 테스트합니다. 외부 의존성(DynamoDB, API 호출 등)은 모킹(mocking)해서 제거합니다.
순수하게 로직만 검증합니다. 우리 예제에서는 "title이 있으면 Todo를 생성한다", "title이 없으면 400 에러를 반환한다" 같은 것들을 테스트합니다.
Jest는 JavaScript의 대표적인 테스트 프레임워크입니다. describe로 테스트 그룹을 만들고, test로 개별 테스트 케이스를 작성합니다.
expect로 예상 결과를 검증합니다. 문법이 직관적이어서 배우기 쉽습니다.
"이 함수를 호출하면 이런 결과가 나와야 한다"를 코드로 표현합니다. **모킹(Mocking)**은 왜 필요할까요?
단위 테스트는 빨라야 합니다. 하지만 실제 DynamoDB를 호출하면 느리고, AWS 자격증명도 필요합니다.
게다가 테스트할 때마다 실제 데이터가 생성됩니다. 모킹을 사용하면 DynamoDB를 가짜로 대체할 수 있습니다.
테스트는 빠르게 실행되고, 실제 리소스에 영향을 주지 않습니다. 통합 테스트는 단위 테스트와 어떻게 다를까요?
통합 테스트는 실제 AWS 리소스를 사용합니다. DynamoDB Local이나 테스트용 AWS 계정에 배포해서 테스트합니다.
API를 실제로 호출하고, 데이터베이스에서 조회해서 결과를 확인합니다. 전체 시스템이 제대로 연동되는지 검증합니다.
실무에서는 어떤 전략을 사용할까요? 피라미드 구조가 이상적입니다.
가장 많은 것은 단위 테스트, 중간은 통합 테스트, 가장 적은 것은 E2E 테스트입니다. 단위 테스트는 빠르고 많이 작성하고, 통합 테스트는 중요한 시나리오만, E2E 테스트는 핵심 플로우만 작성합니다.
API 문서화는 왜 중요할까요? API는 다른 개발자가 사용합니다.
프론트엔드 개발자, 모바일 개발자, 외부 파트너 등등. 이들이 API를 이해하려면 문서가 필요합니다.
"어떤 엔드포인트가 있고, 어떤 파라미터를 받고, 어떤 응답을 주는가"를 명확히 설명해야 합니다. **Swagger(OpenAPI)**는 API 문서의 표준입니다.
YAML이나 JSON 형식으로 API를 정의하면, 자동으로 예쁜 문서가 생성됩니다. 심지어 문서 페이지에서 바로 API를 테스트할 수도 있습니다.
SAM에서는 DefinitionBody 속성으로 Swagger를 통합할 수 있습니다. Postman Collection도 좋은 대안입니다.
Postman으로 API를 테스트하면서 Collection으로 저장합니다. 팀원들과 공유하면 똑같은 환경에서 테스트할 수 있습니다.
환경변수로 개발/스테이징/프로덕션을 구분할 수도 있습니다. 테스트 자동화는 어떻게 할까요?
GitHub Actions나 GitLab CI/CD로 테스트를 자동화합니다. 코드를 푸시하면 자동으로 테스트가 돌아갑니다.
테스트가 실패하면 배포를 막습니다. 이렇게 하면 버그가 프로덕션에 배포되는 것을 방지할 수 있습니다.
김개발 씨는 테스트 코드를 작성하고 npm test를 실행했습니다. 모든 테스트가 통과했습니다.
초록색 체크 마크가 기분 좋았습니다. "이제 자신 있게 코드를 수정할 수 있겠어요!"
실전 팁
💡 - Jest로 단위 테스트를 작성하고, AWS SDK는 모킹하여 빠르게 실행하세요
- 통합 테스트는 DynamoDB Local을 사용하면 비용 없이 로컬에서 가능합니다
- Swagger나 Postman Collection으로 API 문서를 만들면 협업이 수월합니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
서비스 메시 완벽 가이드
마이크로서비스 간 통신을 안전하고 효율적으로 관리하는 서비스 메시의 핵심 개념부터 실전 도입까지, 초급 개발자를 위한 완벽한 입문서입니다. Istio와 Linkerd 비교, 사이드카 패턴, 실무 적용 노하우를 담았습니다.
EFK 스택 로깅 완벽 가이드
마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.
Grafana 대시보드 완벽 가이드
실시간 모니터링의 핵심, Grafana 대시보드를 처음부터 끝까지 배워봅니다. Prometheus 연동부터 알람 설정까지, 초급 개발자도 쉽게 따라할 수 있는 실전 가이드입니다.
분산 추적 완벽 가이드
마이크로서비스 환경에서 요청의 전체 흐름을 추적하는 분산 추적 시스템의 핵심 개념을 배웁니다. Trace, Span, Trace ID 전파, 샘플링 전략까지 실무에 필요한 모든 것을 다룹니다.
마이크로서비스 통합 완벽 가이드
Spring Cloud를 활용한 마이크로서비스 아키텍처 구축부터 통합 테스트까지, 실무에서 바로 적용할 수 있는 완벽한 프로젝트 가이드입니다. Eureka Server, Config Server, API Gateway를 활용하여 확장 가능한 시스템을 만드는 방법을 배웁니다.