시나리오: 현장에서 바로 신고하고 싶다
2편에서 FastAPI로 점검 요청 관리 API를 만들었습니다. 그런데 현장 근무자들의 새로운 요구가 들어옵니다:
"정식 점검 요청은 PC에서 천천히 올리면 되는데, 현장에서 갑자기 이상한 소리가 나거나 연기가 보이면 즉시 신고해야 합니다. 핸드폰에서 사진 한 장과 한 줄 메모만으로 바로 접수할 수 있게 해 주세요. 그리고 야간에는 거의 신고가 없는데 서버 비용이 계속 나가는 것도 아까워요."
이 요구사항에는 2편의 EC2 기반 API와는 다른 접근이 필요합니다:
- 즉시 접수 — 최소한의 입력으로 빠르게 신고
- 24시간 대기하되 비용은 0원 — 사용한 만큼만 과금
- 갑자기 신고가 폭주해도 버틸 것 — 자동 확장
이 모든 조건을 만족하는 것이 서버리스 아키텍처입니다.
이 실습을 마치면 여러분은:
- API Gateway + Lambda + DynamoDB로 서버리스 API를 구축할 수 있습니다
- 2편의 EC2 기반 API와 직접 비교하여 각 방식의 장단점을 이해합니다
- AWS 프리 티어 안에서 완전히 무료로 운영할 수 있습니다
이 프로젝트는 스마트 설비 운영 지원 플랫폼 시리즈의 세 번째입니다. 2편과 같은 도메인(설비 이상 접수)을 서버리스로 구현하여 아키텍처 접근법을 비교합니다. 2편을 완료하지 않았더라도 독립적으로 진행 가능합니다.
실습을 시작하기 전에 AWS 콘솔에 로그인되어 있는지 확인하세요. 리전은 ap-northeast-2 (서울) 을 사용합니다. 모든 서비스가 프리 티어 범위 안에서 동작합니다.
아키텍처 개요

서비스 흐름
비용 예측
비용 계산기
1,000 요청당 $3.50
요청당 + 실행시간 과금
읽기/쓰기 요청 과금
* 실제 비용은 AWS 요금 정책에 따라 달라질 수 있습니다.
프리 티어 안내: 실습 규모에서는 API Gateway 월 100만 건, Lambda 월 100만 건, DynamoDB 25GB 프리 티어 내에서 비용이 거의 $0입니다. 위 계산기는 프리 티어 초과 시 예상 비용을 보여줍니다.
이 실습의 모든 서비스는 AWS 프리 티어 범위 안에서 동작합니다. 프리 티어는 계정 생성 후 12개월간 제공되며, 그 안에서는 과금이 0원입니다.
Step 1: DynamoDB 테이블 생성

DynamoDB는 서버리스 NoSQL 데이터베이스입니다. 테이블을 만들기만 하면 용량, 확장, 백업을 AWS가 알아서 관리합니다.
- 1AWS 콘솔 → DynamoDB 검색 → 테이블 생성 클릭
- 2테이블 이름: IncidentReports 입력
- 3파티션 키: incident_id (문자열)
- 4정렬 키: created_at (문자열)
- 5테이블 설정: 기본 설정 선택 (온디맨드 용량 모드가 프리 티어에 적합)
- 6테이블 생성 클릭 → 상태가 활성(Active)이 되기까지 약 30초 대기
aws dynamodb create-table \
--table-name IncidentReports \
--attribute-definitions \
AttributeName=incident_id,AttributeType=S \
AttributeName=created_at,AttributeType=S \
--key-schema \
AttributeName=incident_id,KeyType=HASH \
AttributeName=created_at,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST \
--region ap-northeast-2DynamoDB 테이블에 저장할 데이터 구조:
| 필드 | 타입 | 설명 |
|---|---|---|
incident_id | String (PK) | UUID 형식 고유 식별자 |
created_at | String (SK) | ISO 8601 형식 생성 시간 |
reporter_name | String | 신고자 이름 |
location | String | 발생 장소 |
description | String | 이상징후 설명 |
severity | String | 심각도 (LOW/MEDIUM/HIGH/CRITICAL) |
status | String | 처리 상태 (REPORTED/INVESTIGATING/RESOLVED) |
DynamoDB는 RDB와 다르게 스키마가 유연합니다. 파티션 키와 정렬 키만 미리 정의하고, 나머지 필드는 데이터를 넣을 때 자유롭게 추가할 수 있습니다. 단, 쿼리 패턴을 미리 설계하지 않으면 나중에 비효율적인 Scan이 발생합니다.
Step 2: Lambda 함수 생성 — 신고 접수
Lambda는 요청이 올 때만 실행되고, 실행이 끝나면 자동으로 종료됩니다. 서버를 관리할 필요가 전혀 없습니다.
- 1AWS 콘솔 → Lambda 검색 → 함수 생성
- 2새로 작성 선택
- 3함수 이름: CreateIncidentReport
- 4런타임: Python 3.12
- 5아키텍처: arm64 선택
- 6기본 실행 역할: 기본 Lambda 권한을 가진 새 역할 생성 선택
- 7함수 생성 클릭
- 8코드 소스에 아래 코드를 붙여넣습니다
- 9Deploy 버튼 클릭
x86_64 vs arm64, 어떤 걸 선택해야 할까요?
Lambda는 두 가지 CPU 아키텍처를 지원합니다:
- x86_64: 인텔/AMD 기반. 전통적인 서버 CPU와 동일. 모든 라이브러리가 호환됩니다.
- arm64 (Graviton2): AWS가 자체 설계한 ARM 기반 칩. x86보다 최대 20% 저렴하고, 성능도 동등하거나 더 빠릅니다.
Python, Node.js 같은 인터프리터 언어에서는 arm64를 선택하면 동일한 코드로 비용만 20% 절감됩니다. C/C++ 네이티브 바이너리를 포함하는 특수 라이브러리를 사용하는 경우에만 x86_64 호환성을 확인하면 됩니다.
이 실습에서는 순수 Python + boto3만 사용하므로 arm64가 최적의 선택입니다.
import json
import uuid
import boto3
from datetime import datetime, timezone
dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-2")
table = dynamodb.Table("IncidentReports")
def lambda_handler(event, context):
try:
# API Gateway에서 전달된 요청 Body 파싱
body = json.loads(event.get("body", "{}"))
# 필수 필드 검증
required = ["reporter_name", "location", "description", "severity"]
missing = [f for f in required if f not in body]
if missing:
return {
"statusCode": 400,
"headers": {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"},
"body": json.dumps({"error": f"필수 필드 누락: {', '.join(missing)}"})
}
# 심각도 검증
valid_severity = ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
if body["severity"] not in valid_severity:
return {
"statusCode": 400,
"headers": {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"},
"body": json.dumps({"error": f"유효하지 않은 심각도. 가능한 값: {valid_severity}"})
}
# DynamoDB에 저장
item = {
"incident_id": str(uuid.uuid4()),
"created_at": datetime.now(timezone.utc).isoformat(),
"reporter_name": body["reporter_name"],
"location": body["location"],
"description": body["description"],
"severity": body["severity"],
"status": "REPORTED",
}
table.put_item(Item=item)
return {
"statusCode": 201,
"headers": {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"},
"body": json.dumps({"message": "이상징후 신고가 접수되었습니다", "incident": item})
}
except Exception as e:
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"},
"body": json.dumps({"error": f"서버 오류: {str(e)}"})
}Lambda IAM 권한 설정
Lambda가 DynamoDB에 접근할 수 있도록 IAM 역할에 권한을 추가합니다.
- 1Lambda 콘솔 → CreateIncidentReport 함수 → 구성 탭 → 권한
- 2실행 역할 링크를 클릭 → IAM 콘솔이 열립니다
- 3권한 추가 → 정책 연결 클릭
- 4AmazonDynamoDBFullAccess 검색 → 체크 → 정책 연결 클릭
- 5Lambda 콘솔로 돌아갑니다
학습 목적으로 AmazonDynamoDBFullAccess를 사용합니다.
프로덕션에서는 특정 테이블에 대한 최소 권한 정책을 직접 만들어야 합니다.
예: dynamodb:PutItem, dynamodb:Scan만 IncidentReports 테이블에 허용.
Step 3: Lambda 함수 생성 — 신고 목록 조회
- 1Lambda 콘솔 → 함수 생성 → 이름: ListIncidentReports
- 2런타임: Python 3.12
- 3실행 역할: 기존 역할 사용 → Step 2에서 생성된 역할 선택
- 4함수 생성 → 아래 코드 붙여넣기 → Deploy
import json
import boto3
dynamodb = boto3.resource("dynamodb", region_name="ap-northeast-2")
table = dynamodb.Table("IncidentReports")
def lambda_handler(event, context):
try:
# 쿼리 파라미터 추출
params = event.get("queryStringParameters") or {}
severity_filter = params.get("severity")
status_filter = params.get("status")
# DynamoDB Scan (소규모 데이터에 적합)
response = table.scan()
items = response.get("Items", [])
# 필터링
if severity_filter:
items = [i for i in items if i.get("severity") == severity_filter]
if status_filter:
items = [i for i in items if i.get("status") == status_filter]
# 최신순 정렬
items.sort(key=lambda x: x.get("created_at", ""), reverse=True)
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"},
"body": json.dumps({"count": len(items), "incidents": items})
}
except Exception as e:
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"},
"body": json.dumps({"error": f"서버 오류: {str(e)}"})
}Access-Control-Allow-Origin: * 헤더는 브라우저에서 직접 API를 호출할 때 필요한 CORS 설정입니다.
모바일 앱에서 이 API를 호출할 때 CORS 에러가 발생하지 않도록 미리 추가합니다.
Step 4: API Gateway 설정

API Gateway는 Lambda 앞에서 HTTP 요청을 받아 적절한 Lambda 함수로 라우팅합니다.
- 1AWS 콘솔 → API Gateway 검색 → API 생성
- 2HTTP API 선택 → 구축 클릭
- 3API 이름: IncidentReportAPI
- 4통합 추가: Lambda → 리전: ap-northeast-2 → CreateIncidentReport 선택
- 5경로 구성: 메서드: POST, 경로: /incidents, 통합 대상: CreateIncidentReport 경로 추가 클릭 메서드: GET, 경로: /incidents, 통합 대상: ListIncidentReports
- 6스테이지: $default (자동 배포) → 다음 → 생성
- 7생성된 API의 호출 URL을 메모합니다 (https://xxxxxxxxxx.execute-api.ap-northeast-2.amazonaws.com)
HTTP API와 REST API는 다릅니다. HTTP API가 더 저렴하고 빠르며, 대부분의 사용 사례에 충분합니다. REST API는 API 키 관리, 요청 검증, 캐싱 등 고급 기능이 필요할 때 사용합니다.
Step 5: curl로 테스트
API Gateway URL을 사용하여 실제로 이상징후를 신고해 봅니다.
# API Gateway URL (자신의 URL로 교체)
API_URL="https://xxxxxxxxxx.execute-api.ap-northeast-2.amazonaws.com"
# 1. 이상징후 신고 (현장 근무자가 모바일에서 전송)
curl -X POST "$API_URL/incidents" \
-H "Content-Type: application/json" \
-d '{
"reporter_name": "박현장",
"location": "A동 1층 1라인",
"description": "컨베이어 벨트에서 심한 진동과 소음 발생. 즉시 확인 필요.",
"severity": "HIGH"
}'
# 2. 추가 신고
curl -X POST "$API_URL/incidents" \
-H "Content-Type: application/json" \
-d '{
"reporter_name": "이반장",
"location": "B동 지하 냉각실",
"description": "냉각수 온도가 비정상적으로 높음. 40도 이상.",
"severity": "CRITICAL"
}'
# 3. 전체 신고 목록 조회
curl "$API_URL/incidents" | python3 -m json.tool
# 4. 심각도 필터링
curl "$API_URL/incidents?severity=CRITICAL" | python3 -m json.tool
# 5. 잘못된 요청 테스트 (필수 필드 누락)
curl -X POST "$API_URL/incidents" \
-H "Content-Type: application/json" \
-d '{"reporter_name": "테스트"}'EC2 기반 API vs 서버리스 API 비교
이 시리즈의 핵심입니다. 같은 도메인을 두 가지 방식으로 구현했으니 직접 비교해 봅시다.
직접 설명해 보기

본인의 말로 설명해 보세요
공장장에게 '왜 점검 요청 API는 EC2에, 이상징후 신고는 서버리스에 올렸는지' 설명해 보세요.
💡 비용, 사용 패턴, 복잡도의 차이를 중심으로 설명하세요.
본인의 말로 설명해 보세요
DynamoDB의 파티션 키와 정렬 키 개념을 비전공자에게 설명해 보세요.
💡 도서관의 서가 배치에 비유해 보세요.
완성 후 테스트 가이드
- 1신고 접수 테스트: curl로 POST /incidents 요청 → 201 응답과 incident_id 확인
- 2복수 신고: 3개 이상의 신고를 서로 다른 severity로 접수
- 3목록 조회: GET /incidents → 접수한 모든 신고가 최신순으로 나오는지 확인
- 4필터링: GET /incidents?severity=CRITICAL → 해당 심각도만 반환되는지 확인
- 5검증 테스트: 필수 필드 누락 요청 → 400 에러와 누락 필드 안내 메시지 확인
- 6DynamoDB 확인: DynamoDB 콘솔 → IncidentReports 테이블 → 항목 탐색 → 데이터가 저장된 것 확인
- 7Lambda 로그: CloudWatch Logs에서 Lambda 실행 로그를 확인하여 실행 시간(Duration)과 메모리 사용량 확인
- 8콜드 스타트 확인: 10분 이상 대기 후 요청을 보내 응답 시간이 평소보다 느린지 확인
트러블슈팅
API Gateway URL로 요청했는데 {"message":"Internal Server Error"} 반환:
- CloudWatch Logs에서 Lambda 에러 로그를 확인합니다
- 대부분 DynamoDB 권한 문제입니다 — Lambda 실행 역할에 DynamoDB 권한이 있는지 확인
- Lambda 함수의 리전과 DynamoDB 테이블의 리전이 같은지 확인합니다
AccessDeniedException 에러:
Lambda 실행 역할에 DynamoDB 접근 권한이 없습니다.
IAM 콘솔에서 해당 역할에 AmazonDynamoDBFullAccess 정책을 연결하세요.
API Gateway에서 CORS 에러:
Lambda 응답에 Access-Control-Allow-Origin 헤더가 포함되어 있는지 확인합니다.
HTTP API를 사용하면 API Gateway 설정에서도 CORS를 활성화할 수 있습니다:
API 선택 → CORS → 허용 오리진에 * 추가
Lambda 실행 시간이 15초 이상 걸리는 경우:
- DynamoDB 테이블 상태가 ACTIVE인지 확인합니다
- Lambda 함수의 메모리를 256MB로 늘려봅니다 (메모리가 크면 CPU도 함께 증가)
- VPC에 연결된 Lambda는 ENI 생성으로 콜드 스타트가 길어질 수 있습니다 — VPC 연결을 제거하세요
비용 분석: EC2 vs 서버리스
실제 숫자로 비교해 봅시다. 한 달(30일) 기준입니다.
결론: 트래픽이 적고 불규칙한 서비스에는 서버리스가 압도적으로 저렴합니다. 트래픽이 일정하고 높은 서비스에는 EC2 예약 인스턴스가 더 경제적일 수 있습니다.
확장 아이디어
- S3 이미지 업로드: 현장 사진을 S3에 업로드하고 URL을 DynamoDB에 저장하는 기능을 추가해 보세요. Pre-signed URL 패턴을 활용합니다.
- SNS 알림 연동: CRITICAL 심각도의 신고가 접수되면 SNS를 통해 관리자에게 즉시 이메일/SMS를 보내 보세요.
- DynamoDB Streams: 데이터 변경 이벤트를 Lambda로 처리하여, 신고가 접수될 때마다 2편의 FastAPI에 자동으로 정식 점검 요청을 생성하는 연동을 만들어 보세요.
- Cognito 인증: API Gateway에 Cognito User Pool을 연동하여 인증된 사용자만 API를 사용하도록 제한해 보세요.
- SAM으로 IaC: AWS SAM(Serverless Application Model)을 사용하여 Lambda, API Gateway, DynamoDB를 코드로 관리해 보세요.
다음 단계 미리보기
마지막 미니 프로젝트 "스마트 설비 운영 지원 플랫폼"에서는 1편(인프라) + 2편(API) + 3편(서버리스)을 하나의 통합 플랫폼으로 엮습니다. CloudWatch 모니터링과 Bedrock AI Q&A를 추가하여 최종 발표 가능한 포트폴리오를 완성합니다.
학습 정리
핵심 치트시트
이 프로젝트에서는 API Gateway + Lambda + DynamoDB로 서버리스 이상징후 신고 서비스를 구축하고, EC2 기반 API와의 차이를 직접 비교했습니다. 핵심은 서버리스의 비용 효율성, 자동 확장, 그리고 적합한 사용 시나리오의 판단 능력입니다.
리소스 정리
이 실습의 모든 리소스는 프리 티어 범위 안이므로 즉시 삭제하지 않아도 괜찮습니다. 하지만 4편을 바로 진행하지 않는다면 아래 순서대로 정리하세요.
- API Gateway → IncidentReportAPI 삭제
- Lambda 함수 삭제 (CreateIncidentReport, ListIncidentReports)
- DynamoDB 테이블 삭제 (IncidentReports)
- IAM 역할 삭제 (Lambda 실행 역할)
- CloudWatch Logs → Lambda 로그 그룹 삭제