Ver3.5 DynamoDB를 활용한 서버리스 아키텍처 완전 정복 가이드

DynamoDB를 활용한 서버리스 아키텍처 완전 정복 가이드
AWS DynamoDB + Lambda + API Gateway로 진짜 서버리스 시스템 만들어보기 🚀
야 솔직히 말해서 요즘 백엔드 개발자들 사이에서 "서버리스" 얘기 안 나오면 좀 뒤처진 느낌 들지 않음? ㅋㅋㅋ
근데 막상 서버리스 하려고 하면 DB는 뭐 써야 하지? 어떻게 연결하지? 이런 고민이 생기잖아.
오늘은 그 고민 완전히 해결해줄게. AWS DynamoDB를 중심으로 서버리스 아키텍처를 어떻게 구축하는지 처음부터 끝까지 다 뜯어볼 거야. 🔥
진짜 실무에서 쓰는 패턴들, 주의해야 할 함정들, 최적화 꿀팁까지 다 담았으니까 끝까지 읽어봐. 분량 좀 되는데 그만큼 알차다고 ㅋㅋ
"서버리스"라는 단어 들으면 "서버가 없는 거야?" 이렇게 생각하는 사람들 있는데, 그건 아니야 ㅋㅋㅋ
서버는 당연히 있어. 근데 개발자가 서버를 직접 관리하지 않아도 된다는 게 핵심이야.
기존 방식이랑 비교해보면 이해가 빠름:
| 구분 | 전통적 서버 방식 | 서버리스 방식 |
|---|---|---|
| 서버 관리 | 직접 EC2 인스턴스 관리 | AWS가 알아서 관리 |
| 스케일링 | 수동 또는 Auto Scaling 설정 | 자동으로 무한 확장 |
| 비용 | 서버 켜있는 시간만큼 과금 | 실제 실행된 시간만큼만 과금 |
| 운영 부담 | OS 패치, 보안 업데이트 필요 | 코드만 신경쓰면 됨 |
| 콜드 스타트 | 없음 | 있음 (최적화 필요) |
서버리스의 핵심 철학은 "인프라 걱정은 클라우드에 맡기고, 비즈니스 로직에만 집중하자"야.
AWS에서 서버리스 아키텍처를 구성하는 주요 서비스들은 이렇게 있어:
이 중에서 오늘 우리가 집중할 건 DynamoDB야. 서버리스 아키텍처에서 DB 선택이 얼마나 중요한지, 왜 DynamoDB가 서버리스랑 찰떡궁합인지 알아볼 거야.
DynamoDB는 AWS에서 만든 완전 관리형 NoSQL 데이터베이스야.
2012년에 출시됐고, 아마존 내부에서도 쇼핑카트, 세션 관리 등 핵심 서비스에 쓰이고 있어.
근데 왜 서버리스에서 DynamoDB를 쓰냐고? 이유가 명확해:
서버 프로비저닝, 패치, 백업 전부 AWS가 해줌. 개발자는 테이블 설계와 쿼리에만 집중하면 돼.
트래픽이 갑자기 10배 뛰어도 DynamoDB는 자동으로 처리 용량을 늘려줘. Lambda랑 완벽하게 시너지.
RDS 같은 관계형 DB는 Lambda에서 쓸 때 연결 풀 고갈 문제가 생기는데, DynamoDB는 HTTP 기반 API라 이 문제가 없어.
단일 자릿수 밀리초(single-digit millisecond) 응답 속도를 보장해. 서버리스 함수의 짧은 실행 시간과 잘 맞아.
실제 읽기/쓰기 요청 수만큼만 비용 내면 돼. Lambda의 실행 횟수 과금 방식이랑 철학이 같아.
DynamoDB를 제대로 쓰려면 이 개념들은 무조건 알아야 해:
| 개념 | 설명 | RDB 비교 |
|---|---|---|
| Table | 데이터를 저장하는 기본 단위 | Table |
| Item | 테이블 내 하나의 데이터 레코드 | Row |
| Attribute | Item의 각 데이터 필드 | Column |
| Partition Key | 데이터를 분산 저장하는 기본 키 | Primary Key |
| Sort Key | 같은 파티션 내 정렬 기준 키 | Composite Key |
| GSI | 다른 속성으로 쿼리하기 위한 인덱스 | Secondary Index |
| LSI | 같은 파티션 키, 다른 정렬 키 인덱스 | Local Index |
DynamoDB 쓰기 전에 서버리스 아키텍처 자체를 어떻게 설계해야 하는지 먼저 알아야 해.
잘못 설계하면 나중에 유지보수가 지옥이 되거든 ㅋㅋㅋ
서버리스는 이벤트가 발생할 때만 함수가 실행돼. HTTP 요청, DB 변경, 파일 업로드, 스케줄 등 다양한 이벤트 소스를 활용해야 해.
Lambda 함수 하나는 하나의 일만 해야 해. "유저 생성", "주문 처리", "알림 발송" 이런 식으로 기능별로 분리. 함수가 너무 커지면 콜드 스타트도 느려지고 유지보수도 힘들어져.
Lambda 함수는 실행될 때마다 새로운 컨테이너에서 돌아갈 수 있어. 그래서 함수 내부에 상태를 저장하면 안 돼. 모든 상태는 DynamoDB, S3, ElastiCache 같은 외부 저장소에 저장해야 해.
같은 요청이 여러 번 들어와도 결과가 동일해야 해. 네트워크 오류로 재시도가 발생할 수 있거든. DynamoDB의 조건부 쓰기(Conditional Write)를 활용하면 멱등성 구현이 쉬워져.
각 Lambda 함수에는 필요한 최소한의 IAM 권한만 부여해야 해. "유저 조회" 함수에 DynamoDB 전체 권한 주는 건 보안상 위험해. 테이블별, 작업별로 세밀하게 권한 설정해야 함.
실제 서버리스 프로젝트를 어떻게 구조화하는지 보여줄게. Serverless Framework 기준이야:
# 프로젝트 디렉토리 구조
my-serverless-app/
├── serverless.yml # 서버리스 설정 파일
├── package.json
├── src/
│ ├── functions/ # Lambda 함수들
│ │ ├── users/
│ │ │ ├── create.js # 유저 생성
│ │ │ ├── get.js # 유저 조회
│ │ │ ├── update.js # 유저 수정
│ │ │ └── delete.js # 유저 삭제
│ │ ├── orders/
│ │ │ ├── create.js
│ │ │ └── list.js
│ │ └── products/
│ │ └── search.js
│ ├── lib/ # 공통 유틸리티
│ │ ├── dynamodb.js # DynamoDB 클라이언트
│ │ ├── response.js # HTTP 응답 헬퍼
│ │ └── validator.js # 입력값 검증
│ └── models/ # 데이터 모델
│ ├── user.js
│ └── order.js
└── tests/ # 테스트 파일
DynamoDB에서 제일 중요한 게 뭐냐고? 바로 테이블 설계야.
RDB처럼 나중에 ALTER TABLE 해서 컬럼 추가하는 게 자유롭지 않아.
처음에 잘 설계해야 나중에 고생 안 해 ㅋㅋㅋ
파티션 키는 데이터를 여러 파티션에 고르게 분산시키는 역할을 해.
좋은 파티션 키의 조건:
✅ 카디널리티(Cardinality)가 높아야 함 - 값의 종류가 많을수록 좋아
✅ 균등하게 분산되어야 함 - 특정 값에 요청이 몰리면 안 돼
✅ 자주 쿼리되는 패턴을 반영해야 함
❌ 나쁜 예시: 성별(M/F), 상태(active/inactive) → 값이 너무 적어서 핫 파티션 발생
✅ 좋은 예시: 사용자 ID, 주문 ID, 상품 ID → 값이 많고 고르게 분산
DynamoDB의 권장 패턴은 Single Table Design이야.
여러 엔티티를 하나의 테이블에 저장하는 방식인데, 처음엔 이상해 보여도 이게 맞아 ㅋㅋ
이유는 DynamoDB에서 JOIN이 없기 때문에, 관련 데이터를 같은 파티션에 저장해서 한 번의 쿼리로 가져오는 게 효율적이거든.
// Single Table Design 예시
// 테이블명: MyApp
// PK: pk (Partition Key), SK: sk (Sort Key)
// 유저 프로필
{
"pk": "USER#user001",
"sk": "PROFILE",
"name": "홍길동",
"email": "hong@email.com",
"createdAt": "2024-01-15T09:00:00Z"
}
// 유저의 주문 (같은 파티션에 저장!)
{
"pk": "USER#user001",
"sk": "ORDER#2024-001",
"productName": "노트북",
"amount": 1500000,
"status": "DELIVERED"
}
// 유저의 모든 주문 조회 (단 한 번의 쿼리로!)
const params = {
TableName: 'MyApp',
KeyConditionExpression: 'pk = :pk AND begins_with(sk, :prefix)',
ExpressionAttributeValues: {
':pk': 'USER#user001',
':prefix': 'ORDER#'
}
}
"이메일로 유저 찾고 싶은데?" 같은 상황에서 GSI가 필요해.
기본 파티션 키가 아닌 다른 속성으로 쿼리할 수 있게 해주는 인덱스야.
// GSI 설정 예시 (serverless.yml)
resources:
Resources:
MyAppTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: MyApp
BillingMode: PAY_PER_REQUEST # 온디맨드 과금
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
- AttributeName: sk
AttributeType: S
- AttributeName: email # GSI용 속성
AttributeType: S
- AttributeName: gsi1pk # GSI 파티션 키
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
- AttributeName: sk
KeyType: RANGE
GlobalSecondaryIndexes:
- IndexName: EmailIndex
KeySchema:
- AttributeName: email
KeyType: HASH
Projection:
ProjectionType: ALL # 모든 속성 복사
- IndexName: GSI1
KeySchema:
- AttributeName: gsi1pk
KeyType: HASH
- AttributeName: sk
KeyType: RANGE
Projection:
ProjectionType: ALL
이제 진짜 코드 레벨로 들어가보자. Lambda에서 DynamoDB를 어떻게 쓰는지 실전 예제로 보여줄게.
Node.js 기준으로 설명할 거야. (Python도 비슷한 패턴이니까 참고해봐)
AWS SDK v3 기준으로 설명할게. v2랑 문법이 좀 달라서 헷갈릴 수 있으니까 주의해!
// src/lib/dynamodb.js
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
// 클라이언트 생성 (Lambda 컨테이너 재사용을 위해 모듈 레벨에서 생성)
const client = new DynamoDBClient({
region: process.env.AWS_REGION || 'ap-northeast-2'
});
// DocumentClient: JS 객체를 자동으로 DynamoDB 타입으로 변환
export const dynamoDB = DynamoDBDocumentClient.from(client, {
marshallOptions: {
removeUndefinedValues: true, // undefined 값 자동 제거
convertEmptyValues: false
}
});
export const TABLE_NAME = process.env.TABLE_NAME || 'MyApp';
// src/functions/users/create.js - 유저 생성
import { PutCommand } from '@aws-sdk/lib-dynamodb';
import { dynamoDB, TABLE_NAME } from '../../lib/dynamodb.js';
import { v4 as uuidv4 } from 'uuid';
export const handler = async (event) => {
try {
const body = JSON.parse(event.body);
const userId = uuidv4();
const now = new Date().toISOString();
const item = {
pk: `USER#${userId}`,
sk: 'PROFILE',
userId,
name: body.name,
email: body.email,
createdAt: now,
updatedAt: now,
// GSI용 속성
gsi1pk: `EMAIL#${body.email}`
};
await dynamoDB.send(new PutCommand({
TableName: TABLE_NAME,
Item: item,
// 이미 존재하면 에러 발생 (멱등성 보장)
ConditionExpression: 'attribute_not_exists(pk)'
}));
return {
statusCode: 201,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, message: '유저 생성 완료!' })
};
} catch (error) {
if (error.name === 'ConditionalCheckFailedException') {
return { statusCode: 409, body: JSON.stringify({ message: '이미 존재하는 유저' }) };
}
return { statusCode: 500, body: JSON.stringify({ message: error.message }) };
}
};
// src/functions/users/get.js - 유저 조회
import { GetCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
import { dynamoDB, TABLE_NAME } from '../../lib/dynamodb.js';
// 유저 ID로 조회
export const getById = async (event) => {
const { userId } = event.pathParameters;
const result = await dynamoDB.send(new GetCommand({
TableName: TABLE_NAME,
Key: {
pk: `USER#${userId}`,
sk: 'PROFILE'
}
}));
if (!result.Item) {
return { statusCode: 404, body: JSON.stringify({ message: '유저 없음' }) };
}
return { statusCode: 200, body: JSON.stringify(result.Item) };
};
// 유저의 모든 주문 조회 (Sort Key 범위 쿼리)
export const getUserOrders = async (event) => {
const { userId } = event.pathParameters;
const result = await dynamoDB.send(new QueryCommand({
TableName: TABLE_NAME,
KeyConditionExpression: 'pk = :pk AND begins_with(sk, :prefix)',
ExpressionAttributeValues: {
':pk': `USER#${userId}`,
':prefix': 'ORDER#'
},
ScanIndexForward: false // 최신 주문부터 (내림차순)
}));
return { statusCode: 200, body: JSON.stringify(result.Items) };
};
// src/functions/users/update.js - 유저 수정
import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
import { dynamoDB, TABLE_NAME } from '../../lib/dynamodb.js';
export const handler = async (event) => {
const { userId } = event.pathParameters;
const body = JSON.parse(event.body);
const result = await dynamoDB.send(new UpdateCommand({
TableName: TABLE_NAME,
Key: {
pk: `USER#${userId}`,
sk: 'PROFILE'
},
// 동적으로 업데이트할 필드 설정
UpdateExpression: 'SET #name = :name, updatedAt = :updatedAt',
ExpressionAttributeNames: {
'#name': 'name' // 예약어 충돌 방지
},
ExpressionAttributeValues: {
':name': body.name,
':updatedAt': new Date().toISOString()
},
// 존재하는 아이템만 업데이트
ConditionExpression: 'attribute_exists(pk)',
ReturnValues: 'ALL_NEW' // 업데이트 후 값 반환
}));
return { statusCode: 200, body: JSON.stringify(result.Attributes) };
};
Lambda 함수를 외부에서 호출하려면 API Gateway가 필요해.
HTTP 요청을 받아서 Lambda로 전달하고, Lambda의 응답을 다시 클라이언트에게 보내주는 역할이야.
Serverless Framework로 설정하는 방법 보여줄게:
# serverless.yml
service: my-serverless-app
provider:
name: aws
runtime: nodejs20.x
region: ap-northeast-2
stage: ${opt:stage, 'dev'}
environment:
TABLE_NAME: ${self:service}-${self:provider.stage}
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
- dynamodb:BatchGetItem
- dynamodb:BatchWriteItem
Resource:
- !GetAtt MyAppTable.Arn
- !Sub '${MyAppTable.Arn}/index/*' # GSI 포함
functions:
createUser:
handler: src/functions/users/create.handler
events:
- httpApi:
path: /users
method: POST
getUser:
handler: src/functions/users/get.getById
events:
- httpApi:
path: /users/{userId}
method: GET
getUserOrders:
handler: src/functions/users/get.getUserOrders
events:
- httpApi:
path: /users/{userId}/orders
method: GET
updateUser:
handler: src/functions/users/update.handler
events:
- httpApi:
path: /users/{userId}
method: PUT
실제 서비스에서는 인증이 필수야. AWS Cognito나 JWT 토큰으로 인증을 구현할 수 있어:
# JWT Authorizer 설정 (serverless.yml)
provider:
httpApi:
authorizers:
jwtAuthorizer:
type: jwt
identitySource: $request.header.Authorization
issuerUrl: https://cognito-idp.ap-northeast-2.amazonaws.com/{userPoolId}
audience:
- {clientId}
functions:
getUser:
handler: src/functions/users/get.getById
events:
- httpApi:
path: /users/{userId}
method: GET
authorizer:
name: jwtAuthorizer # 인증 적용!
서버리스의 고질적인 문제인 콜드 스타트. Lambda 함수가 처음 실행될 때 컨테이너를 새로 띄우는 시간이 걸리는데, 이게 수백 ms에서 수 초까지 걸릴 수 있어.
DynamoDB 연동에서 콜드 스타트를 줄이는 방법들:
Lambda 패키지가 클수록 콜드 스타트가 느려져. AWS SDK v3는 모듈별로 import할 수 있어서 필요한 것만 가져오면 돼.
// ❌ 나쁜 예 - 전체 SDK importimport AWS from 'aws-sdk';// ✅ 좋은 예 - 필요한 것만 importimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';import { PutCommand } from '@aws-sdk/lib-dynamodb';
중요한 함수에는 Provisioned Concurrency를 설정해서 미리 컨테이너를 띄워놓을 수 있어.
비용이 추가되지만 콜드 스타트가 완전히 없어져. 결제 API 같은 지연에 민감한 기능에 적합해.
Java Lambda의 경우 SnapStart를 활용하면 초기화 스냅샷을 저장해서 콜드 스타트를 90% 이상 줄일 수 있어.
// ❌ 나쁜 예 - Scan 사용 (전체 테이블 스캔, 비용 폭탄!)
const badQuery = await dynamoDB.send(new ScanCommand({
TableName: TABLE_NAME,
FilterExpression: 'email = :email',
ExpressionAttributeValues: { ':email': 'user@email.com' }
}));
// ✅ 좋은 예 - GSI를 활용한 Query (특정 파티션만 조회)
const goodQuery = await dynamoDB.send(new QueryCommand({
TableName: TABLE_NAME,
IndexName: 'EmailIndex', // GSI 사용!
KeyConditionExpression: 'email = :email',
ExpressionAttributeValues: { ':email': 'user@email.com' }
}));
// ✅ 프로젝션으로 필요한 속성만 가져오기
const projectedQuery = await dynamoDB.send(new GetCommand({
TableName: TABLE_NAME,
Key: { pk: 'USER#user001', sk: 'PROFILE' },
ProjectionExpression: 'userId, #name, email', // 필요한 것만!
ExpressionAttributeNames: { '#name': 'name' }
}));
DynamoDB Streams는 테이블의 변경 사항을 실시간으로 캡처해서 Lambda를 트리거할 수 있어.
이걸 활용하면 이벤트 드리븐 아키텍처를 완성할 수 있어:
// DynamoDB Streams 트리거 Lambda 함수
export const streamHandler = async (event) => {
for (const record of event.Records) {
const { eventName, dynamodb } = record;
if (eventName === 'INSERT') {
// 새 유저 생성 시 환영 이메일 발송
const newItem = dynamodb.NewImage;
if (newItem.sk.S === 'PROFILE') {
await sendWelcomeEmail(newItem.email.S);
}
}
if (eventName === 'MODIFY') {
// 주문 상태 변경 시 알림 발송
const oldItem = dynamodb.OldImage;
const newItem = dynamodb.NewImage;
if (newItem.sk.S.startsWith('ORDER#') &&
oldItem.status?.S !== newItem.status?.S) {
await sendOrderStatusNotification(newItem);
}
}
if (eventName === 'REMOVE') {
// 데이터 삭제 시 감사 로그 기록
await logDeletion(dynamodb.OldImage);
}
}
};
# serverless.yml에 Streams 트리거 설정
functions:
streamProcessor:
handler: src/functions/stream.streamHandler
events:
- stream:
type: dynamodb
arn: !GetAtt MyAppTable.StreamArn
batchSize: 100
startingPosition: LATEST
bisectBatchOnFunctionError: true
✅ 실시간 알림 발송 (이메일, 푸시 알림)
✅ 검색 인덱스 동기화 (Elasticsearch, OpenSearch)
✅ 캐시 무효화 (Redis, ElastiCache)
✅ 감사 로그 (Audit Log) 기록
✅ 다른 서비스로 데이터 복제
서버리스의 장점 중 하나가 비용 효율성인데, 잘못 쓰면 오히려 비용이 폭발할 수 있어 ㅋㅋㅋ
DynamoDB 비용 구조를 이해하고 최적화하는 방법을 알아보자.
| 항목 | 온디맨드 모드 | 프로비저닝 모드 |
|---|---|---|
| 읽기 (RCU) | 100만 요청당 약 $0.25 | RCU당 시간당 $0.00013 |
| 쓰기 (WCU) | 100만 요청당 약 $1.25 | WCU당 시간당 $0.00065 |
| 스토리지 | GB당 월 $0.25 | GB당 월 $0.25 |
| 적합한 경우 | 트래픽 예측 불가, 초기 서비스 | 트래픽 예측 가능, 안정적 서비스 |
※ 위 가격은 서울 리전(ap-northeast-2) 기준 대략적인 수치야. 실제 가격은 AWS 공식 사이트에서 확인해봐.
만료된 데이터를 자동으로 삭제해주는 TTL 기능. 세션 데이터, 임시 토큰, 캐시 데이터 등에 활용하면 스토리지 비용을 크게 줄일 수 있어:
// TTL 설정 예시 - 세션 데이터 (24시간 후 자동 삭제)
const sessionItem = {
pk: `SESSION#${sessionId}`,
sk: 'DATA',
userId: userId,
token: accessToken,
createdAt: new Date().toISOString(),
// TTL: Unix timestamp (초 단위)
// 현재 시간 + 24시간
ttl: Math.floor(Date.now() / 1000) + (24 * 60 * 60)
};
// serverless.yml에서 TTL 속성 지정
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
FilterExpression: 'attribute_not_exists(ttl) OR ttl > :now'
프로비저닝 모드를 쓸 때는 Auto Scaling을 설정해서 트래픽에 따라 자동으로 용량을 조절할 수 있어:
# serverless.yml - Auto Scaling 설정
resources:
Resources:
MyAppTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PROVISIONED
ProvisionedThroughput:
ReadCapacityUnits: 5 # 최소값
WriteCapacityUnits: 5 # 최소값
ReadScalingTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MaxCapacity: 100 # 최대 RCU
MinCapacity: 5 # 최소 RCU
ResourceId: !Sub 'table/${MyAppTable}'
ScalableDimension: dynamodb:table:ReadCapacityUnits
ServiceNamespace: dynamodb
ReadScalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyType: TargetTrackingScaling
TargetTrackingScalingPolicyConfiguration:
TargetValue: 70 # 70% 사용률 유지
PredefinedMetricSpecification:
PredefinedMetricType: DynamoDBReadCapacityUtilization
서버리스라고 보안을 소홀히 하면 안 돼! 오히려 더 신경 써야 할 부분들이 있어.
# 함수별 세밀한 권한 설정 예시
functions:
readOnlyFunction:
handler: src/functions/users/get.handler
iamRoleStatements: # 함수별 IAM 역할
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:Query
Resource:
- !GetAtt MyAppTable.Arn
- !Sub '${MyAppTable.Arn}/index/*'
writeFunction:
handler: src/functions/users/create.handler
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:UpdateItem
Resource: !GetAtt MyAppTable.Arn
API 키, DB 연결 정보 같은 민감한 정보는 절대 코드에 하드코딩하면 안 돼!
AWS Systems Manager Parameter Store나 Secrets Manager를 활용해:
# serverless.yml - SSM Parameter Store 활용
provider:
environment:
TABLE_NAME: ${self:service}-${self:provider.stage}
# SSM에서 값 가져오기
JWT_SECRET: ${ssm:/myapp/${self:provider.stage}/jwt-secret}
SMTP_PASSWORD: ${ssm:/myapp/${self:provider.stage}/smtp-password~true} # ~true: 암호화된 값
// Lambda 코드에서 Secrets Manager 사용
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const secretsClient = new SecretsManagerClient({ region: 'ap-northeast-2' });
// 캐싱으로 매 요청마다 Secrets Manager 호출 방지
let cachedSecret = null;
const getSecret = async () => {
if (cachedSecret) return cachedSecret;
const response = await secretsClient.send(new GetSecretValueCommand({
SecretId: 'myapp/production/api-keys'
}));
cachedSecret = JSON.parse(response.SecretString);
return cachedSecret;
};
DynamoDB는 기본적으로 인터넷을 통해 접근하지만, 보안이 중요한 경우 VPC Endpoint를 설정해서 트래픽이 AWS 내부 네트워크를 통해서만 흐르게 할 수 있어.
단, Lambda를 VPC에 넣으면 콜드 스타트가 느려질 수 있으니 트레이드오프를 고려해야 해.
서버리스 환경에서 문제가 생기면 어디서 뭐가 잘못됐는지 찾기가 쉽지 않아.
제대로 된 모니터링 체계를 갖추는 게 진짜 중요해 ㅋㅋ
// src/lib/logger.js - 구조화된 로깅 유틸리티
export const logger = {
info: (message, data = {}) => {
console.log(JSON.stringify({
level: 'INFO',
message,
timestamp: new Date().toISOString(),
requestId: process.env.AWS_REQUEST_ID,
...data
}));
},
error: (message, error, data = {}) => {
console.error(JSON.stringify({
level: 'ERROR',
message,
errorMessage: error.message,
errorStack: error.stack,
timestamp: new Date().toISOString(),
...data
}));
}
};
// 사용 예시
logger.info('유저 생성 완료', { userId, email });
logger.error('DynamoDB 쓰기 실패', error, { userId, operation: 'createUser' });
DynamoDB와 Lambda 모두 CloudWatch에 자동으로 메트릭을 보내줘.
꼭 모니터링해야 할 핵심 메트릭들:
| 서비스 | 메트릭 | 알람 기준 |
|---|---|---|
| Lambda | Duration | 타임아웃의 80% 초과 시 |
| Lambda | Errors | 에러율 1% 초과 시 |
| Lambda | Throttles | 스로틀링 발생 시 |
| DynamoDB | ConsumedReadCapacityUnits | 프로비저닝의 80% 초과 시 |
| DynamoDB | SystemErrors | 0보다 클 때 |
| DynamoDB | SuccessfulRequestLatency | P99 > 100ms 시 |
| API Gateway | 5XXError | 에러율 0.1% 초과 시 |
여러 Lambda 함수와 DynamoDB가 연결된 복잡한 아키텍처에서 성능 병목을 찾으려면 X-Ray가 필수야:
# serverless.yml - X-Ray 활성화
provider:
tracing:
lambda: true # Lambda X-Ray 활성화
apiGateway: true # API Gateway X-Ray 활성화
// Lambda 코드에서 X-Ray SDK 사용
import AWSXRay from 'aws-xray-sdk-core';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
// X-Ray로 DynamoDB 호출 추적
const client = AWSXRay.captureAWSv3Client(new DynamoDBClient({
region: 'ap-northeast-2'
}));
서버리스 환경에서 테스트하는 게 좀 까다롭긴 한데, 제대로 된 테스트 전략이 있으면 훨씬 편해져.
// tests/users/create.test.js
import { mockClient } from 'aws-sdk-client-mock';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
import { handler } from '../../src/functions/users/create.js';
// DynamoDB 클라이언트 모킹
const ddbMock = mockClient(DynamoDBDocumentClient);
describe('유저 생성 Lambda', () => {
beforeEach(() => {
ddbMock.reset();
});
it('유저를 성공적으로 생성해야 함', async () => {
ddbMock.on(PutCommand).resolves({});
const event = {
body: JSON.stringify({ name: '홍길동', email: 'hong@test.com' })
};
const result = await handler(event);
expect(result.statusCode).toBe(201);
const body = JSON.parse(result.body);
expect(body.userId).toBeDefined();
});
it('중복 유저 생성 시 409 반환해야 함', async () => {
ddbMock.on(PutCommand).rejects({
name: 'ConditionalCheckFailedException'
});
const result = await handler({ body: JSON.stringify({ name: '홍길동', email: 'hong@test.com' }) });
expect(result.statusCode).toBe(409);
});
});
매번 AWS에 배포해서 테스트하면 시간도 오래 걸리고 비용도 나와.
로컬에서 개발하는 방법들:
AWS에서 제공하는 로컬 DynamoDB. Docker로 쉽게 실행 가능해:
docker run -p 8000:8000 amazon/dynamodb-local코드에서 엔드포인트만 바꿔주면 돼:
const client = new DynamoDBClient({ endpoint: 'http://localhost:8000' });
Serverless Framework 플러그인으로 Lambda와 API Gateway를 로컬에서 실행:
npm install --save-dev serverless-offline serverless-dynamodb-localserverless offline start
AWS에서 공식 제공하는 로컬 테스트 도구. Docker 기반으로 실제 Lambda 환경과 유사하게 테스트 가능:
sam local start-apisam local invoke FunctionName --event event.json
이론은 충분히 배웠으니까 실제로 많이 쓰이는 아키텍처 패턴들을 알아보자.
이런 패턴들은 재능넷 같은 플랫폼에서도 실제로 활용할 수 있는 것들이야.
// 주문 생성 - 트랜잭션으로 원자성 보장
import { TransactWriteCommand } from '@aws-sdk/lib-dynamodb';
export const createOrder = async (userId, productId, quantity) => {
const orderId = uuidv4();
const now = new Date().toISOString();
// 트랜잭션: 주문 생성 + 재고 감소를 원자적으로 처리
await dynamoDB.send(new TransactWriteCommand({
TransactItems: [
{
// 1. 주문 아이템 생성
Put: {
TableName: TABLE_NAME,
Item: {
pk: `USER#${userId}`,
sk: `ORDER#${orderId}`,
orderId, productId, quantity,
status: 'PENDING',
createdAt: now
},
ConditionExpression: 'attribute_not_exists(pk)'
}
},
{
// 2. 재고 감소 (재고 부족 시 트랜잭션 전체 롤백)
Update: {
TableName: TABLE_NAME,
Key: { pk: `PRODUCT#${productId}`, sk: 'INFO' },
UpdateExpression: 'SET stock = stock - :qty',
ConditionExpression: 'stock >= :qty', // 재고 체크!
ExpressionAttributeValues: { ':qty': quantity }
}
}
]
}));
return orderId;
};
// DynamoDB Streams → Lambda → SNS → 사용자 알림
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
const snsClient = new SNSClient({ region: 'ap-northeast-2' });
export const notificationHandler = async (event) => {
for (const record of event.Records) {
if (record.eventName !== 'MODIFY') continue;
const newImage = record.dynamodb.NewImage;
const oldImage = record.dynamodb.OldImage;
// 주문 상태 변경 감지
if (newImage.sk?.S?.startsWith('ORDER#') &&
oldImage.status?.S !== newImage.status?.S) {
const message = {
userId: newImage.pk.S.replace('USER#', ''),
orderId: newImage.sk.S.replace('ORDER#', ''),
oldStatus: oldImage.status?.S,
newStatus: newImage.status.S,
timestamp: new Date().toISOString()
};
// SNS로 알림 발행
await snsClient.send(new PublishCommand({
TopicArn: process.env.ORDER_NOTIFICATION_TOPIC_ARN,
Message: JSON.stringify(message),
MessageAttributes: {
eventType: {
DataType: 'String',
StringValue: 'ORDER_STATUS_CHANGED'
}
}
}));
}
}
};
Command Query Responsibility Segregation - 읽기와 쓰기를 분리하는 패턴이야.
DynamoDB Streams를 활용해서 쓰기 모델과 읽기 모델을 분리할 수 있어:
1️⃣ Command (쓰기): API Gateway → Lambda → DynamoDB (메인 테이블)
2️⃣ DynamoDB Streams: 변경 이벤트 캡처
3️⃣ 프로젝션 Lambda: 이벤트를 받아서 읽기 최적화 모델로 변환
4️⃣ Query (읽기): 읽기 최적화된 GSI나 별도 테이블에서 조회
이 패턴을 쓰면 복잡한 집계 쿼리도 미리 계산해서 저장해놓을 수 있어서 읽기 성능이 극적으로 향상돼.
DynamoDB + 서버리스 조합에서 많이들 하는 실수들 정리해줄게.
나도 처음에 이것들 때문에 많이 고생했거든 ㅋㅋㅋ
DynamoDB에서 Scan은 전체 테이블을 읽어. 데이터가 많아질수록 비용이 폭발하고 느려져.
항상 Query를 사용하고, 필요하면 GSI를 추가해야 해.
날짜를 파티션 키로 쓰면 특정 날짜에 모든 트래픽이 몰려서 핫 파티션이 생겨.
날짜는 Sort Key로 쓰고, 파티션 키는 사용자 ID나 카테고리 같은 걸 써야 해.
DynamoDB 클라이언트를 핸들러 함수 안에서 생성하면 매 요청마다 새로 만들어져.
모듈 레벨에서 한 번만 생성하면 Lambda 컨테이너 재사용 시 클라이언트도 재사용돼.
DynamoDB 호출은 언제든 실패할 수 있어. ProvisionedThroughputExceededException, ThrottlingException 등
지수 백오프(Exponential Backoff)로 재시도 로직을 구현해야 해.
DynamoDB 아이템 하나의 최대 크기는 400KB야. 이미지나 큰 파일은 S3에 저장하고 DynamoDB에는 S3 URL만 저장해야 해.
기본 Lambda 타임아웃은 3초야. DynamoDB 쿼리가 복잡하거나 재시도가 필요하면 타임아웃에 걸릴 수 있어.
작업 특성에 맞게 타임아웃을 설정해야 해 (최대 15분).
// 지수 백오프 재시도 유틸리티
export const withRetry = async (fn, maxRetries = 3) => {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// 재시도 가능한 에러인지 확인
const retryableErrors = [
'ProvisionedThroughputExceededException',
'ThrottlingException',
'RequestLimitExceeded',
'ServiceUnavailable'
];
if (!retryableErrors.includes(error.name)) {
throw error; // 재시도 불가능한 에러는 즉시 throw
}
// 지수 백오프: 100ms, 200ms, 400ms...
const delay = Math.pow(2, attempt) * 100;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
};
// 사용 예시
const result = await withRetry(() =>
dynamoDB.send(new PutCommand({ TableName, Item }))
);
PartiQL은 SQL과 유사한 쿼리 언어로 DynamoDB를 조회할 수 있게 해줘.
SQL에 익숙한 개발자들이 DynamoDB를 더 쉽게 쓸 수 있도록 AWS가 2020년에 도입했어:
import { ExecuteStatementCommand } from '@aws-sdk/client-dynamodb';
// PartiQL로 조회
const result = await dynamoDB.send(new ExecuteStatementCommand({
Statement: "SELECT * FROM MyApp WHERE pk = 'USER#user001'"
}));
// 조건부 업데이트
await dynamoDB.send(new ExecuteStatementCommand({
Statement: "UPDATE MyApp SET status = 'SHIPPED' WHERE pk = 'USER#user001' AND sk = 'ORDER#001'"
}));
DynamoDB 데이터를 S3로 내보내서 Athena로 분석하거나 백업할 수 있어.
Point-in-Time Recovery(PITR)가 활성화된 테이블에서 사용 가능해:
// DynamoDB Export 설정 (AWS CLI)
aws dynamodb export-table-to-point-in-time \
--table-arn arn:aws:dynamodb:ap-northeast-2:123456789:table/MyApp \
--s3-bucket my-backup-bucket \
--s3-prefix dynamodb-exports/ \
--export-format DYNAMODB_JSON
# 내보낸 데이터를 Athena로 분석
CREATE EXTERNAL TABLE dynamodb_export (
Item struct<pk:struct<S:string>, sk:struct<S:string>, name:struct<S:string>>
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://my-backup-bucket/dynamodb-exports/';
전 세계 사용자를 대상으로 하는 서비스라면 DynamoDB Global Tables를 활용해서 여러 리전에 데이터를 자동으로 복제할 수 있어.
각 리전에서 읽기/쓰기가 가능하고, 충돌은 "last writer wins" 방식으로 해결돼.
- ✅ 멀티 리전 자동 복제 (보통 1초 이내)
- ✅ 각 리전에서 로컬 읽기/쓰기 가능
- ✅ 리전 장애 시 자동 페일오버
- ✅ 온디맨드 모드에서만 사용 가능 (2023년 이후)
- ⚠️ 비용이 일반 테이블보다 높음 (복제 비용 추가)
서버리스 앱을 어떻게 배포하고 자동화할지도 중요해.
GitHub Actions로 CI/CD 파이프라인 구축하는 방법 보여줄게:
# .github/workflows/deploy.yml
name: Deploy Serverless App
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- run: npm test
deploy-dev:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-2
- name: Deploy to dev
run: npx serverless deploy --stage dev
deploy-prod:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-2
- name: Deploy to production
run: npx serverless deploy --stage prod
와 진짜 길었다 ㅋㅋㅋ 여기까지 읽어줬으면 진짜 고마워!
DynamoDB + Lambda + API Gateway 조합으로 서버리스 아키텍처를 구축하는 것, 이제 어느 정도 감이 왔지?
핵심을 다시 정리하면:
1️⃣ DynamoDB는 서버리스의 베스트 파트너 - 완전 관리형, 자동 스케일링, HTTP 기반 API로 연결 풀 문제 없음
2️⃣ 테이블 설계가 전부 - Single Table Design, 올바른 파티션 키 선택, 쿼리 패턴 기반 인덱스 설계
3️⃣ Scan은 금지 - 항상 Query를 사용하고, 필요하면 GSI 추가
4️⃣ 이벤트 드리븐으로 설계 - DynamoDB Streams를 활용한 비동기 처리
5️⃣ 보안과 모니터링은 필수 - IAM 최소 권한, CloudWatch 알람, X-Ray 추적
6️⃣ 비용 최적화 항상 고려 - TTL, 적절한 과금 모드 선택, 프로젝션 최소화
서버리스는 처음엔 낯설게 느껴질 수 있어. 근데 한번 익숙해지면 진짜 생산성이 폭발해.
서버 관리에 쓰던 시간을 전부 비즈니스 로직 개발에 쏟을 수 있거든.
혹시 서버리스 개발 관련해서 더 배우고 싶거나, 실제 프로젝트에 도움이 필요하다면 재능넷에서 AWS 서버리스 전문가들의 도움을 받아볼 수도 있어. 다양한 개발 재능을 가진 분들이 활동하고 있으니까 참고해봐! 😊
DynamoDB 설계 관련해서 더 깊이 파고 싶다면 Alex DeBrie의 "The DynamoDB Book"을 강력 추천해. 서버리스 아키텍처 전반에 대해서는 AWS Well-Architected Framework의 서버리스 렌즈 문서도 꼭 읽어봐. 공식 문서인데 진짜 알차거든 ㅋㅋ
자, 이제 직접 만들어봐! 코드 짜다가 막히는 거 있으면 AWS 공식 문서랑 이 글 다시 보면서 해결해봐. 화이팅! 🚀
📚 AWS DynamoDB 공식 문서: docs.aws.amazon.com/dynamodb
📚 The DynamoDB Book - Alex DeBrie
📚 AWS Well-Architected Framework - Serverless Lens
📚 Serverless Framework 공식 문서: serverless.com/framework/docs
🎥 AWS re:Invent DynamoDB 세션 (YouTube에서 무료로 볼 수 있어!)
관련 키워드
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.