본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 19. · 9 Views
Lambda에서 DynamoDB 사용 완벽 가이드
AWS Lambda에서 DynamoDB를 활용하는 방법을 처음부터 끝까지 배웁니다. boto3 클라이언트 설정부터 CRUD 작업, 에러 처리, 배치 작업, 트랜잭션까지 실무에 필요한 모든 것을 담았습니다.
목차
1. boto3 DynamoDB 클라이언트
입사 한 달 차인 김개발 씨는 오늘 처음으로 AWS Lambda 함수를 작성하게 되었습니다. 선배 개발자 박시니어 씨가 다가와 말합니다.
"Lambda에서 DynamoDB에 데이터를 저장하는 함수를 만들어야 하는데, 먼저 boto3 클라이언트부터 설정해볼까요?"
boto3는 AWS 서비스를 Python으로 제어할 수 있게 해주는 공식 SDK입니다. 마치 리모컨으로 TV를 조작하듯이, boto3를 통해 DynamoDB를 제어할 수 있습니다.
Lambda에서 DynamoDB를 사용하려면 먼저 이 클라이언트를 제대로 설정하는 것이 첫 단추입니다.
다음 코드를 살펴봅시다.
import boto3
from botocore.exceptions import ClientError
# DynamoDB 리소스 생성 (높은 수준의 추상화)
dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-2')
table = dynamodb.Table('Users')
# 또는 클라이언트 생성 (낮은 수준의 제어)
dynamodb_client = boto3.client('dynamodb', region_name='ap-northeast-2')
def lambda_handler(event, context):
# 테이블 상태 확인
response = table.table_status
print(f"테이블 상태: {response}")
return {"statusCode": 200, "body": "클라이언트 설정 완료"}
김개발 씨는 선배의 모니터를 바라보며 궁금증이 생겼습니다. "boto3가 정확히 뭔가요?" 박시니어 씨가 친절하게 설명을 시작합니다.
boto3는 AWS가 공식적으로 제공하는 Python용 소프트웨어 개발 키트입니다. 쉽게 말해서, Python 코드로 AWS 서비스들을 마음대로 조작할 수 있게 해주는 도구입니다.
리모컨을 떠올려 보세요. TV를 켜고 끄고, 채널을 돌리고, 볼륨을 조절하는 모든 동작을 버튼 하나로 할 수 있습니다.
boto3도 마찬가지입니다. DynamoDB에 데이터를 넣고, 조회하고, 수정하고, 삭제하는 모든 작업을 Python 함수 호출만으로 처리할 수 있습니다.
Lambda 함수에서 DynamoDB를 사용하려면 가장 먼저 해야 할 일이 있습니다. 바로 boto3 클라이언트를 설정하는 것입니다.
boto3는 두 가지 방식을 제공합니다. resource와 client입니다.
이 둘의 차이를 이해하는 것이 중요합니다. resource는 높은 수준의 추상화를 제공합니다.
마치 자동차의 자동 변속기처럼, 복잡한 내부 동작을 숨기고 간편하게 사용할 수 있게 해줍니다. 대부분의 일반적인 작업은 resource로 충분합니다.
반면 client는 낮은 수준의 제어를 가능하게 합니다. 수동 변속기처럼 세밀한 제어가 필요할 때 사용합니다.
AWS API를 거의 그대로 호출하는 방식이라 더 많은 옵션을 사용할 수 있습니다. 코드를 다시 살펴봅시다.
먼저 boto3.resource('dynamodb')를 호출해서 DynamoDB 리소스 객체를 생성합니다. 여기서 region_name을 지정하는 것이 중요합니다.
서울 리전을 사용한다면 'ap-northeast-2'를 입력합니다. 다음으로 dynamodb.Table('Users')로 특정 테이블에 대한 참조를 얻습니다.
이제 이 table 객체를 통해 Users 테이블에 데이터를 읽고 쓸 수 있습니다. Lambda 환경에서는 특별히 신경 써야 할 점이 있습니다.
Lambda 함수는 요청이 올 때마다 실행되는데, 매번 새로운 boto3 클라이언트를 생성하면 비효율적입니다. 그래서 실무에서는 클라이언트 생성 코드를 lambda_handler 함수 밖에 작성합니다.
Lambda는 컨테이너를 재사용하기 때문에, 한 번 생성된 클라이언트 객체가 여러 요청에 걸쳐 재사용됩니다. 이렇게 하면 성능이 크게 향상됩니다.
김개발 씨가 물었습니다. "그럼 region_name을 항상 지정해야 하나요?" 박시니어 씨가 답합니다.
"Lambda 함수가 실행되는 리전과 DynamoDB 테이블이 있는 리전이 같다면 생략해도 됩니다. 하지만 명시적으로 적어주는 게 나중에 혼란을 방지할 수 있어요." 실제 프로젝트에서는 환경 변수를 활용하는 경우가 많습니다.
테이블 이름이나 리전을 환경 변수로 관리하면, 개발/스테이징/프로덕션 환경을 쉽게 구분할 수 있습니다. boto3를 사용할 때 주의할 점도 있습니다.
DynamoDB 테이블이 실제로 존재하지 않거나, 권한이 없는 경우 에러가 발생합니다. 따라서 항상 예외 처리를 염두에 두어야 합니다.
박시니어 씨가 마무리하며 말합니다. "boto3 클라이언트 설정은 Lambda에서 DynamoDB를 사용하는 모든 작업의 시작점입니다.
이 부분을 제대로 이해하면 나머지는 훨씬 쉬워질 거예요."
실전 팁
💡 - boto3 클라이언트는 lambda_handler 밖에서 생성해서 재사용하세요 (성능 향상)
- 일반적인 CRUD 작업은 resource를, 세밀한 제어가 필요하면 client를 사용하세요
- region_name은 명시적으로 지정하는 것이 좋습니다
2. Lambda IAM 역할 설정
김개발 씨가 코드를 완성하고 실행해봤더니 AccessDeniedException 에러가 발생했습니다. "왜 안 되는 거죠?" 박시니어 씨가 웃으며 말합니다.
"아, Lambda 함수에 DynamoDB 접근 권한을 안 줬네요. IAM 역할 설정부터 해봅시다."
IAM 역할은 Lambda 함수가 다른 AWS 서비스에 접근할 수 있는 권한을 정의합니다. 마치 회사 출입증처럼, 어느 방에 들어갈 수 있는지를 결정합니다.
Lambda에서 DynamoDB를 사용하려면 반드시 적절한 권한을 부여해야 합니다.
다음 코드를 살펴봅시다.
# IAM 정책 JSON (AWS Console 또는 Terraform에서 설정)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query",
"dynamodb:Scan"
],
"Resource": "arn:aws:dynamodb:ap-northeast-2:123456789012:table/Users"
}
]
}
# Lambda 함수에서 권한 확인하는 방법
import os
print(f"Lambda 실행 역할: {os.environ.get('AWS_EXECUTION_ENV')}")
김개발 씨는 에러 메시지를 보며 당황했습니다. 분명히 코드는 맞게 작성한 것 같은데, AccessDeniedException이라는 에러가 계속 발생합니다.
박시니어 씨가 설명을 시작합니다. "AWS에서는 보안이 아주 중요합니다.
아무나 데이터베이스에 접근하면 안 되겠죠?" 회사를 생각해보세요. 모든 직원이 모든 방에 들어갈 수 있다면 어떻게 될까요?
중요한 서류가 사라지거나, 기밀이 유출될 수 있습니다. 그래서 출입증을 발급하고, 직급과 부서에 따라 출입 가능한 구역을 제한합니다.
AWS의 IAM 역할도 똑같은 개념입니다. Lambda 함수에게 "당신은 이 DynamoDB 테이블에 데이터를 읽고 쓸 수 있습니다"라는 허가증을 주는 것입니다.
IAM 역할은 정책(Policy)으로 구성됩니다. 정책은 JSON 형식으로 작성되며, 어떤 작업(Action)을 어떤 리소스(Resource)에 허용(Allow) 또는 거부(Deny)할지 정의합니다.
위의 예제 정책을 살펴봅시다. Effect는 "Allow"로 설정되어 있습니다.
이것은 "허용한다"는 의미입니다. Action 배열에는 DynamoDB에서 수행할 수 있는 작업들이 나열되어 있습니다.
PutItem은 새로운 항목을 추가하는 작업입니다. GetItem은 항목을 조회하고, UpdateItem은 수정하며, DeleteItem은 삭제합니다.
Query와 Scan은 여러 항목을 검색하는 작업입니다. Resource는 어느 테이블에 대한 권한인지 지정합니다.
ARN(Amazon Resource Name)이라는 고유 식별자로 표현됩니다. 여기서는 ap-northeast-2 리전의 Users 테이블을 가리킵니다.
실무에서는 최소 권한 원칙을 따릅니다. 필요한 작업만 허용하는 것입니다.
예를 들어, 데이터를 읽기만 하는 Lambda라면 GetItem과 Query만 허용하면 됩니다. UpdateItem이나 DeleteItem까지 줄 필요가 없습니다.
김개발 씨가 질문합니다. "그럼 이 정책을 어디에 설정하나요?" 박시니어 씨가 답합니다.
"AWS Console의 IAM 서비스에서 설정할 수 있고, Terraform이나 CloudFormation 같은 IaC 도구를 사용할 수도 있어요." Lambda 함수를 생성할 때 실행 역할(Execution Role)을 지정하게 됩니다. 이 역할에 DynamoDB 권한 정책을 연결하면 됩니다.
실제로 많은 개발자들이 처음에는 권한 설정을 간과합니다. 로컬 환경에서는 AWS 자격 증명이 설정되어 있어서 잘 작동하지만, Lambda에 배포하면 에러가 발생하는 경우가 많습니다.
또 하나 주의할 점은 와일드카드() 사용입니다. 개발 중에는 편의상 "dynamodb:"처럼 모든 작업을 허용하기도 하는데, 프로덕션 환경에서는 절대 안 됩니다.
보안 취약점이 될 수 있습니다. 특정 테이블의 특정 인덱스에만 접근 권한을 주고 싶다면 Resource ARN을 더 구체적으로 작성할 수 있습니다.
예를 들어 "arn:aws:dynamodb:region:account-id:table/Users/index/EmailIndex"처럼 인덱스까지 지정할 수 있습니다. 박시니어 씨가 마무리합니다.
"권한 설정은 귀찮게 느껴질 수 있지만, 실제로는 시스템을 안전하게 지켜주는 보호막입니다. 처음부터 제대로 설정하는 습관을 들이세요."
실전 팁
💡 - 최소 권한 원칙을 따르세요 (필요한 작업만 허용)
- 프로덕션 환경에서는 절대 와일드카드(*) 사용하지 마세요
- Resource ARN은 특정 테이블로 제한하는 것이 안전합니다
3. CRUD 함수 구현
권한 설정을 마친 김개발 씨는 이제 본격적으로 CRUD 함수를 작성할 차례입니다. 박시니어 씨가 말합니다.
"Create, Read, Update, Delete 네 가지 기본 작업만 제대로 구현하면 웬만한 기능은 다 만들 수 있어요."
CRUD는 데이터베이스의 가장 기본적인 네 가지 작업입니다. 생성(Create), 조회(Read), 수정(Update), 삭제(Delete)를 의미합니다.
DynamoDB에서는 각각 PutItem, GetItem, UpdateItem, DeleteItem 메서드로 구현됩니다.
다음 코드를 살펴봅시다.
import boto3
import json
from decimal import Decimal
dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-2')
table = dynamodb.Table('Users')
def lambda_handler(event, context):
operation = event.get('operation')
if operation == 'create':
# Create: 새 사용자 생성
table.put_item(Item={
'userId': event['userId'],
'name': event['name'],
'email': event['email'],
'age': event['age']
})
return {'statusCode': 200, 'body': '생성 완료'}
elif operation == 'read':
# Read: 사용자 조회
response = table.get_item(Key={'userId': event['userId']})
item = response.get('Item', {})
return {'statusCode': 200, 'body': json.dumps(item, default=str)}
elif operation == 'update':
# Update: 사용자 정보 수정
table.update_item(
Key={'userId': event['userId']},
UpdateExpression='SET #name = :name, age = :age',
ExpressionAttributeNames={'#name': 'name'},
ExpressionAttributeValues={':name': event['name'], ':age': event['age']}
)
return {'statusCode': 200, 'body': '수정 완료'}
elif operation == 'delete':
# Delete: 사용자 삭제
table.delete_item(Key={'userId': event['userId']})
return {'statusCode': 200, 'body': '삭제 완료'}
김개발 씨는 드디어 실제로 데이터를 다루는 코드를 작성하게 되었습니다. 설렘 반, 긴장 반입니다.
박시니어 씨가 말합니다. "모든 데이터베이스 작업은 CRUD로 시작합니다.
이 네 가지만 제대로 익히면 나머지는 응용일 뿐이에요." CRUD라는 용어를 들어본 적 있으신가요? 프로그래밍을 하다 보면 자주 듣게 되는 단어입니다.
Create(생성), Read(조회), Update(수정), Delete(삭제)의 첫 글자를 따서 만든 약어입니다. 일상생활로 비유해봅시다.
여러분이 연락처 앱을 사용한다고 생각해보세요. 새 친구를 추가하는 것이 Create, 친구 정보를 확인하는 것이 Read, 전화번호를 바꾸는 것이 Update, 연락처를 삭제하는 것이 Delete입니다.
DynamoDB에서도 똑같습니다. 다만 메서드 이름이 조금 다릅니다.
먼저 put_item부터 살펴봅시다. 새로운 항목을 테이블에 추가하는 작업입니다.
Item 파라미터에 딕셔너리 형태로 데이터를 전달합니다. userId, name, email, age 같은 속성들을 넣을 수 있습니다.
여기서 중요한 점이 있습니다. userId는 이 테이블의 파티션 키입니다.
파티션 키는 반드시 제공해야 하며, 각 항목을 고유하게 식별합니다. 마치 주민등록번호처럼 중복될 수 없는 고유값입니다.
get_item은 데이터를 조회하는 메서드입니다. Key 파라미터에 파티션 키 값을 전달하면, 해당하는 항목을 가져옵니다.
결과는 response 딕셔너리로 반환되며, 실제 데이터는 'Item' 키에 들어 있습니다. 만약 해당 userId를 가진 항목이 없다면 어떻게 될까요?
에러가 발생하지 않습니다. 대신 response에 'Item' 키가 없습니다.
그래서 response.get('Item', {})처럼 기본값을 제공하는 것이 안전합니다. update_item은 조금 복잡합니다.
DynamoDB는 특별한 표현식 문법을 사용합니다. UpdateExpression에 'SET #name = :name, age = :age' 같은 문자열을 작성합니다.
왜 이렇게 복잡하게 작성할까요? 보안과 유연성 때문입니다.
ExpressionAttributeNames는 'name' 같은 예약어를 안전하게 사용하기 위한 것이고, ExpressionAttributeValues는 실제 값을 전달하는 부분입니다. 김개발 씨가 질문합니다.
"그냥 put_item으로 덮어쓰면 안 되나요?" 박시니어 씨가 답합니다. "가능하지만, update_item을 쓰면 변경하고 싶은 속성만 지정할 수 있어요.
나머지 속성은 그대로 유지됩니다." delete_item은 가장 단순합니다. Key만 제공하면 해당 항목이 삭제됩니다.
삭제할 항목이 없어도 에러가 발생하지 않습니다. 그냥 조용히 성공 응답을 반환합니다.
실무에서는 event 객체를 통해 클라이언트로부터 데이터를 받습니다. API Gateway를 통해 Lambda를 호출하는 경우, event['body']에 JSON 문자열이 들어오므로 파싱해야 합니다.
또 하나 주의할 점은 DynamoDB의 숫자 타입입니다. Python의 float는 정밀도 문제가 있어서, boto3는 Decimal을 사용합니다.
JSON 직렬화할 때 이 부분을 처리해야 합니다. 박시니어 씨가 마무리합니다.
"CRUD 네 가지 작업만 확실히 익히면, 복잡한 애플리케이션도 만들 수 있습니다. 지금 작성한 코드를 기반으로 확장해 나가세요."
실전 팁
💡 - put_item은 항목 전체를 덮어쓰고, update_item은 특정 속성만 변경합니다
- get_item 결과에서 Item이 없을 수 있으니 항상 기본값을 제공하세요
- UpdateExpression 문법을 익혀두면 복잡한 수정 작업도 쉽게 처리할 수 있습니다
4. 에러 처리
김개발 씨가 작성한 함수가 잘 작동하는 것 같았지만, 가끔 예상치 못한 에러가 발생했습니다. 박시니어 씨가 말합니다.
"실전에서는 에러 처리가 절반입니다. 어떤 에러가 발생할 수 있는지 알고, 적절히 대응해야 해요."
에러 처리는 프로그램이 예외 상황에서도 안정적으로 동작하도록 만드는 작업입니다. DynamoDB에서는 다양한 에러가 발생할 수 있으며, 각 에러 타입에 맞는 처리가 필요합니다.
try-except 블록과 boto3의 예외 클래스를 활용합니다.
다음 코드를 살펴봅시다.
import boto3
from botocore.exceptions import ClientError, BotoCoreError
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-2')
table = dynamodb.Table('Users')
def lambda_handler(event, context):
try:
# DynamoDB 작업 수행
response = table.get_item(Key={'userId': event['userId']})
if 'Item' not in response:
# 항목이 없는 경우
return {
'statusCode': 404,
'body': '사용자를 찾을 수 없습니다'
}
return {
'statusCode': 200,
'body': response['Item']
}
except ClientError as e:
error_code = e.response['Error']['Code']
if error_code == 'ResourceNotFoundException':
# 테이블이 존재하지 않음
logger.error(f"테이블을 찾을 수 없습니다: {e}")
return {'statusCode': 500, 'body': '서버 설정 오류'}
elif error_code == 'ProvisionedThroughputExceededException':
# 요청 한도 초과
logger.warning(f"요청 한도 초과: {e}")
return {'statusCode': 429, 'body': '잠시 후 다시 시도하세요'}
else:
# 기타 DynamoDB 에러
logger.error(f"DynamoDB 에러: {e}")
return {'statusCode': 500, 'body': '데이터 처리 중 오류 발생'}
except KeyError as e:
# 필수 파라미터 누락
logger.error(f"필수 파라미터 누락: {e}")
return {'statusCode': 400, 'body': 'userId가 필요합니다'}
except Exception as e:
# 예상치 못한 에러
logger.error(f"예상치 못한 에러: {e}")
return {'statusCode': 500, 'body': '서버 오류'}
김개발 씨는 자신이 작성한 Lambda 함수를 테스트하다가 당황스러운 상황을 맞이했습니다. 갑자기 함수 전체가 중단되면서 이상한 에러 메시지가 출력된 것입니다.
박시니어 씨가 다가와 화면을 보더니 고개를 끄덕입니다. "아, 에러 처리를 안 했네요.
프로그램은 완벽할 수 없어요. 언제든 문제가 생길 수 있다고 가정하고 코드를 작성해야 합니다." 운전을 생각해보세요.
아무리 조심해서 운전해도 갑자기 앞차가 급정거할 수 있고, 타이어가 펑크 날 수도 있습니다. 그래서 안전벨트를 매고, 에어백을 장착하고, 여분의 타이어를 준비합니다.
프로그래밍도 마찬가지입니다. DynamoDB를 사용할 때 발생할 수 있는 에러는 여러 가지입니다.
테이블이 존재하지 않을 수도 있고, 네트워크 연결이 끊길 수도 있으며, 요청이 너무 많아서 한도를 초과할 수도 있습니다. 가장 흔한 에러는 ClientError입니다.
이것은 boto3가 DynamoDB와 통신하다가 발생하는 모든 클라이언트 측 에러를 포함합니다. 이 에러를 캐치하면 세부적인 에러 코드를 확인할 수 있습니다.
에러 코드는 e.response['Error']['Code']로 접근합니다. ResourceNotFoundException은 테이블이나 인덱스가 존재하지 않을 때 발생합니다.
이것은 치명적인 설정 오류이므로 로그를 남기고 500 에러를 반환합니다. ProvisionedThroughputExceededException은 읽기/쓰기 용량을 초과했을 때 발생합니다.
DynamoDB는 초당 처리할 수 있는 요청 수가 정해져 있는데, 이것을 넘어서면 요청을 거부합니다. 이 경우 429 상태 코드를 반환해서 클라이언트가 재시도하도록 유도합니다.
김개발 씨가 질문합니다. "그럼 모든 에러 코드를 다 처리해야 하나요?" 박시니어 씨가 답합니다.
"아니요. 자주 발생하는 것들만 명시적으로 처리하고, 나머지는 포괄적인 Exception 핸들러로 받으면 됩니다." KeyError는 파이썬에서 딕셔너리 키가 없을 때 발생하는 에러입니다.
event 객체에 'userId'가 없으면 이 에러가 발생합니다. 클라이언트가 잘못된 요청을 보낸 것이므로 400 에러를 반환합니다.
로깅도 중요합니다. Lambda는 CloudWatch Logs에 자동으로 로그를 보내는데, logger를 사용하면 나중에 문제를 추적하기 쉽습니다.
**logger.error()**는 심각한 에러를, **logger.warning()**은 경고를, **logger.info()**는 일반 정보를 기록합니다. 실무에서는 에러가 발생했을 때 단순히 로그만 남기는 것이 아니라, 모니터링 시스템에 알림을 보내기도 합니다.
CloudWatch Alarms나 SNS를 활용하면 실시간으로 에러를 감지할 수 있습니다. 또 하나 중요한 패턴은 **조기 반환(early return)**입니다.
항목이 없는 경우를 먼저 체크하고 404를 반환하면, 이후 코드에서는 항목이 존재한다고 가정할 수 있습니다. 코드가 훨씬 깔끔해집니다.
에러 메시지를 작성할 때도 주의가 필요합니다. 사용자에게는 "잠시 후 다시 시도하세요" 같은 친절한 메시지를 보내고, 로그에는 상세한 기술적 정보를 남깁니다.
보안상 내부 에러 정보를 외부에 노출하면 안 됩니다. 박시니어 씨가 마무리합니다.
"에러 처리는 귀찮게 느껴질 수 있지만, 실제 서비스에서는 필수입니다. 사용자는 언제나 예상치 못한 방식으로 시스템을 사용하거든요."
실전 팁
💡 - ClientError를 캐치하고 에러 코드별로 분기 처리하세요
- 로그는 레벨(error, warning, info)을 구분해서 남기세요
- 사용자에게는 친절한 메시지를, 로그에는 상세한 정보를 기록하세요
5. 배치 작업
김개발 씨는 한 번에 여러 사용자를 등록해야 하는 요구사항을 받았습니다. 반복문으로 하나씩 put_item을 호출하려고 하자, 박시니어 씨가 말립니다.
"그렇게 하면 너무 느려요. 배치 작업을 사용하세요."
배치 작업은 여러 항목을 한 번에 처리하는 방법입니다. DynamoDB의 batch_write_item은 최대 25개 항목을 하나의 요청으로 처리할 수 있어서 훨씬 효율적입니다.
네트워크 왕복 횟수가 줄어들어 속도가 크게 향상됩니다.
다음 코드를 살펴봅시다.
import boto3
from botocore.exceptions import ClientError
dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-2')
table = dynamodb.Table('Users')
def lambda_handler(event, context):
users = event.get('users', [])
# 배치 쓰기: 최대 25개씩 처리
with table.batch_writer() as batch:
for user in users:
batch.put_item(Item={
'userId': user['userId'],
'name': user['name'],
'email': user['email'],
'age': user.get('age', 0)
})
# 배치 읽기: 여러 항목을 한 번에 조회
response = dynamodb.batch_get_item(
RequestItems={
'Users': {
'Keys': [
{'userId': 'user1'},
{'userId': 'user2'},
{'userId': 'user3'}
]
}
}
)
items = response.get('Responses', {}).get('Users', [])
return {
'statusCode': 200,
'body': f'{len(users)}명 등록 완료, {len(items)}명 조회'
}
김개발 씨는 새로운 요구사항을 받았습니다. CSV 파일에서 100명의 사용자 데이터를 읽어서 DynamoDB에 등록해야 합니다.
가장 간단한 방법은 for 반복문으로 하나씩 put_item을 호출하는 것입니다. 하지만 100번의 네트워크 요청이 발생하므로 시간이 오래 걸립니다.
박시니어 씨가 더 나은 방법을 알려줍니다. "DynamoDB에는 배치 작업이라는 기능이 있어요.
여러 항목을 한 번에 처리할 수 있습니다." 택배를 생각해보세요. 물건을 하나씩 보내면 배송비도 많이 들고 시간도 오래 걸립니다.
하지만 여러 물건을 한 상자에 담아서 보내면 훨씬 효율적입니다. 배치 작업도 같은 원리입니다.
DynamoDB의 **batch_writer()**는 아주 편리한 도구입니다. with 문으로 사용하면, 자동으로 25개씩 묶어서 요청을 보냅니다.
개발자는 그냥 반복문으로 put_item을 호출하기만 하면 됩니다. 내부적으로 batch_writer는 영리하게 동작합니다.
항목을 버퍼에 모아두다가 25개가 되면 자동으로 batch_write_item API를 호출합니다. with 블록이 끝나면 남은 항목들도 모두 처리됩니다.
만약 중간에 에러가 발생하면 어떻게 될까요? batch_writer는 자동으로 재시도합니다.
일시적인 네트워크 문제나 용량 초과 같은 경우에 몇 번 더 시도해서 성공 확률을 높입니다. 김개발 씨가 질문합니다.
"읽기도 배치로 할 수 있나요?" 박시니어 씨가 답합니다. "물론이죠.
batch_get_item을 사용하면 됩니다." batch_get_item은 조금 다릅니다. RequestItems라는 딕셔너리를 전달해야 하는데, 테이블 이름을 키로 하고 조회할 키 목록을 값으로 제공합니다.
여러 테이블에서 동시에 데이터를 가져올 수도 있습니다. 결과는 Responses 딕셔너리에 들어옵니다.
테이블 이름을 키로 해서 접근하면 항목 리스트를 얻을 수 있습니다. 주의할 점은 요청한 순서와 응답 순서가 다를 수 있다는 것입니다.
배치 작업에도 제한이 있습니다. 한 번에 최대 25개 항목만 처리할 수 있고, 총 크기는 16MB를 넘을 수 없습니다.
100개의 항목을 처리하려면 4번의 배치 요청이 필요합니다. 실무에서는 이런 제한을 고려해서 코드를 작성합니다.
예를 들어 1000개 항목을 처리해야 한다면, 25개씩 묶어서 반복 처리하는 로직을 구현합니다. 또 하나 중요한 점은 **처리되지 않은 항목(UnprocessedItems)**입니다.
용량 한도 때문에 일부 항목이 처리되지 못할 수 있습니다. 이 경우 response의 UnprocessedItems를 확인하고 재시도해야 합니다.
배치 작업은 트랜잭션이 아닙니다. 일부는 성공하고 일부는 실패할 수 있습니다.
모든 항목이 성공하거나 모두 실패해야 한다면 트랜잭션을 사용해야 합니다. 박시니어 씨가 팁을 하나 더 줍니다.
"대량의 데이터를 처리할 때는 Lambda의 타임아웃 설정도 확인하세요. 기본 3초로는 부족할 수 있어요."
실전 팁
💡 - batch_writer는 자동으로 25개씩 묶어서 처리하므로 편리합니다
- UnprocessedItems를 확인하고 실패한 항목은 재시도하세요
- 배치 작업은 트랜잭션이 아니므로 부분 실패가 가능합니다
6. 트랜잭션 사용
김개발 씨가 결제 시스템을 개발하다가 문제를 발견했습니다. 사용자 포인트는 차감됐는데 주문은 생성되지 않은 것입니다.
박시니어 씨가 말합니다. "이럴 때 트랜잭션을 써야 해요.
전부 성공하거나 전부 실패하게 만들어야 합니다."
트랜잭션은 여러 작업을 하나의 원자적 단위로 묶는 기능입니다. 모든 작업이 성공하면 커밋되고, 하나라도 실패하면 전체가 롤백됩니다.
DynamoDB는 transact_write_items와 transact_get_items를 제공하며, 최대 100개 항목을 하나의 트랜잭션으로 처리할 수 있습니다.
다음 코드를 살펴봅시다.
import boto3
from botocore.exceptions import ClientError
dynamodb = boto3.client('dynamodb', region_name='ap-northeast-2')
def lambda_handler(event, context):
user_id = event['userId']
order_id = event['orderId']
points = event['points']
try:
# 트랜잭션: 포인트 차감과 주문 생성을 원자적으로 처리
response = dynamodb.transact_write_items(
TransactItems=[
{
'Update': {
'TableName': 'Users',
'Key': {'userId': {'S': user_id}},
'UpdateExpression': 'SET points = points - :points',
'ConditionExpression': 'points >= :points',
'ExpressionAttributeValues': {':points': {'N': str(points)}}
}
},
{
'Put': {
'TableName': 'Orders',
'Item': {
'orderId': {'S': order_id},
'userId': {'S': user_id},
'points': {'N': str(points)},
'status': {'S': 'pending'}
}
}
}
]
)
return {'statusCode': 200, 'body': '주문 완료'}
except ClientError as e:
if e.response['Error']['Code'] == 'TransactionCanceledException':
# 조건 불만족 (포인트 부족 등)
return {'statusCode': 400, 'body': '포인트가 부족합니다'}
else:
return {'statusCode': 500, 'body': '주문 실패'}
김개발 씨는 심각한 버그를 발견했습니다. 사용자가 포인트로 상품을 구매했는데, 포인트는 차감됐지만 주문이 생성되지 않았습니다.
사용자는 돈만 날리고 상품은 받지 못한 셈입니다. 박시니어 씨가 코드를 보더니 한숨을 쉽니다.
"아, 트랜잭션을 안 썼네요. 이런 경우에는 반드시 트랜잭션이 필요합니다." 은행 계좌이체를 생각해보세요.
A 계좌에서 돈이 빠져나가고, B 계좌에 돈이 들어와야 합니다. 만약 A 계좌에서만 돈이 나가고 B 계좌에는 안 들어온다면 큰일입니다.
둘 다 성공하거나, 둘 다 실패해야 합니다. 트랜잭션은 바로 이런 "전부 아니면 전무(all-or-nothing)" 원칙을 구현합니다.
여러 작업을 하나로 묶어서, 모두 성공하면 커밋하고, 하나라도 실패하면 전체를 취소합니다. DynamoDB의 transact_write_items는 최대 100개의 작업을 하나의 트랜잭션으로 처리할 수 있습니다.
Put, Update, Delete, ConditionCheck 네 가지 작업을 조합할 수 있습니다. 위의 예제를 살펴봅시다.
첫 번째 작업은 Users 테이블에서 포인트를 차감합니다. 중요한 부분은 ConditionExpression입니다.
'points >= :points' 조건을 검사해서, 포인트가 부족하면 작업을 거부합니다. 두 번째 작업은 Orders 테이블에 새 주문을 생성합니다.
Put 작업으로 orderId, userId, points, status 정보를 저장합니다. 이 두 작업은 원자적으로 실행됩니다.
포인트 차감이 성공하면 주문도 반드시 생성됩니다. 포인트가 부족해서 차감이 실패하면 주문도 생성되지 않습니다.
데이터 일관성이 완벽하게 유지됩니다. 김개발 씨가 질문합니다.
"그럼 트랜잭션이 실패하면 어떻게 알 수 있나요?" 박시니어 씨가 답합니다. "TransactionCanceledException 에러가 발생합니다." 이 에러는 조건이 만족되지 않았거나, 충돌이 발생했을 때 던져집니다.
예를 들어 포인트가 부족하거나, 다른 트랜잭션이 동시에 같은 항목을 수정하려고 할 때 발생합니다. 트랜잭션 읽기도 가능합니다.
transact_get_items를 사용하면 여러 항목을 일관된 시점에서 조회할 수 있습니다. 읽는 중간에 다른 트랜잭션이 데이터를 변경해도 영향을 받지 않습니다.
실무에서 트랜잭션을 사용할 때 주의할 점이 있습니다. 트랜잭션은 일반 작업보다 비용이 두 배입니다.
WCU(쓰기 용량 단위)를 두 배로 소비합니다. 따라서 꼭 필요한 경우에만 사용해야 합니다.
또한 트랜잭션은 서로 다른 리전의 테이블에는 사용할 수 없습니다. 같은 AWS 계정, 같은 리전 내의 테이블만 하나의 트랜잭션으로 묶을 수 있습니다.
트랜잭션 내에서는 같은 항목을 여러 번 수정할 수 없습니다. 예를 들어 같은 userId를 두 번 Update하려고 하면 에러가 발생합니다.
박시니어 씨가 마무리합니다. "트랜잭션은 강력한 도구지만 남용하면 안 됩니다.
데이터 일관성이 정말로 중요한 경우에만 사용하세요. 간단한 작업은 일반 API로도 충분합니다." 김개발 씨는 이제 Lambda에서 DynamoDB를 자유자재로 다룰 수 있게 되었습니다.
boto3 클라이언트 설정부터 CRUD, 에러 처리, 배치 작업, 트랜잭션까지 모두 익혔습니다. 실무에서 만나는 대부분의 상황을 해결할 수 있을 것입니다.
실전 팁
💡 - 트랜잭션은 비용이 두 배이므로 꼭 필요할 때만 사용하세요
- ConditionExpression으로 조건을 검사해서 데이터 무결성을 보장하세요
- TransactionCanceledException을 캐치해서 실패 원인을 파악하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.