본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 19. · 7 Views
DynamoDB CRUD 완벽 가이드
AWS DynamoDB의 기본 CRUD 작업부터 조건부 작업까지, 초급 개발자를 위한 완벽한 실무 가이드입니다. 실제 프로젝트에서 바로 사용할 수 있는 패턴과 주의사항을 담았습니다.
목차
1. PutItem으로 생성
입사 한 달 차 신입 개발자 김개발 씨는 처음으로 AWS DynamoDB를 다루게 되었습니다. 사용자 데이터를 저장하는 API를 만들어야 하는데, 어디서부터 시작해야 할지 막막합니다.
선배 박시니어 씨가 다가와 말합니다. "DynamoDB는 간단해요.
먼저 PutItem으로 데이터를 넣어보세요."
PutItem은 DynamoDB에 새로운 데이터를 저장하는 가장 기본적인 작업입니다. 마치 노트에 새로운 메모를 적는 것처럼, 테이블에 항목을 추가하거나 기존 항목을 덮어씁니다.
파티션 키와 정렬 키가 같은 항목이 있으면 자동으로 교체되므로, 생성과 수정을 한 번에 처리할 수 있습니다.
다음 코드를 살펴봅시다.
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, PutCommand } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({ region: "ap-northeast-2" });
const docClient = DynamoDBDocumentClient.from(client);
// 사용자 정보를 저장하는 함수
async function createUser(userId, email, name) {
const params = {
TableName: "Users",
Item: {
userId: userId, // 파티션 키
email: email,
name: name,
createdAt: new Date().toISOString()
}
};
// PutItem 실행
const result = await docClient.send(new PutCommand(params));
return result;
}
김개발 씨는 첫 번째 미션을 받았습니다. 회원가입 API를 만들어서 사용자 정보를 DynamoDB에 저장하는 것입니다.
RDB만 다뤄본 김개발 씨는 INSERT 문이 익숙한데, DynamoDB는 어떻게 다를까요? 박시니어 씨가 모니터 앞으로 다가와 설명을 시작합니다.
"DynamoDB는 NoSQL 데이터베이스예요. SQL이 아니라 SDK를 통해 작업하죠." PutItem이란 무엇일까요? 쉽게 비유하자면, PutItem은 마치 책장에 책을 꽂는 것과 같습니다.
같은 제목의 책이 이미 있다면 그 자리에 새 책으로 교체하고, 없다면 새로운 자리에 꽂습니다. DynamoDB도 똑같이 동작합니다.
같은 키를 가진 항목이 있으면 덮어쓰고, 없으면 새로 생성합니다. 전통적인 데이터베이스의 문제점 PutItem이 등장하기 전, 아니 정확히는 NoSQL이 등장하기 전에는 어땠을까요?
관계형 데이터베이스에서는 데이터를 저장하려면 먼저 테이블 스키마를 정의해야 했습니다. 컬럼 타입도 미리 정하고, 제약 조건도 설정해야 했습니다.
문제는 서비스가 커지면서 스키마 변경이 필요할 때마다 ALTER TABLE을 실행해야 했고, 이는 대용량 테이블에서 엄청난 시간이 걸렸습니다. DynamoDB의 유연한 접근 바로 이런 문제를 해결하기 위해 DynamoDB 같은 NoSQL이 등장했습니다.
PutItem을 사용하면 스키마에 구애받지 않고 자유롭게 데이터를 저장할 수 있습니다. 또한 분산 환경에서도 빠른 쓰기 성능을 보장합니다.
무엇보다 자동으로 확장되어 트래픽이 급증해도 안정적으로 동작한다는 큰 이점이 있습니다. 코드 한 줄씩 뜯어보기 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 DynamoDBClient를 생성하는 부분에서 리전을 지정합니다. 서울 리전은 ap-northeast-2입니다.
다음으로 DynamoDBDocumentClient를 만드는데, 이것이 핵심입니다. 일반 DynamoDB 클라이언트는 데이터 타입을 명시해야 하지만, DocumentClient는 자동으로 변환해줍니다.
params 객체에서 TableName은 테이블 이름을, Item은 실제 저장할 데이터를 담습니다. userId는 파티션 키로 설정된 필드입니다.
마지막으로 PutCommand를 생성해서 send 메서드로 실행하면 데이터가 저장됩니다. 실무에서는 어떻게 쓸까요? 실제 현업에서는 어떻게 활용할까요?
예를 들어 소셜 미디어 서비스를 개발한다고 가정해봅시다. 사용자가 게시물을 작성할 때마다 PutItem으로 Posts 테이블에 저장합니다.
게시물 ID를 파티션 키로 사용하면, 같은 ID로 다시 저장할 때 자동으로 수정이 됩니다. 많은 스타트업에서 이런 패턴을 적극적으로 사용하고 있습니다.
흔히 하는 실수들 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 PutItem을 항상 생성 용도로만 생각한다는 것입니다.
같은 키로 PutItem을 실행하면 기존 데이터가 사라진다는 점을 간과하면 데이터 손실이 발생할 수 있습니다. 따라서 생성과 수정을 명확히 구분해야 한다면 ConditionExpression을 사용해야 합니다.
김개발 씨의 성공 다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 자신감이 생겼습니다.
"아, 생각보다 간단하네요!" PutItem을 제대로 이해하면 DynamoDB의 기초를 탄탄히 다질 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - DocumentClient를 사용하면 타입 변환을 자동으로 처리해줍니다
- 덮어쓰기를 방지하려면 ConditionExpression에 attribute_not_exists를 사용하세요
- 배치 작업이 필요하다면 BatchWriteItem을 고려해보세요
2. GetItem으로 조회
데이터를 저장하는 데 성공한 김개발 씨는 이제 저장된 데이터를 불러와야 합니다. 사용자 프로필 페이지를 만들어야 하는데, 어떻게 특정 사용자의 정보를 가져올 수 있을까요?
박시니어 씨가 말합니다. "키를 알고 있다면 GetItem이 가장 빠릅니다."
GetItem은 파티션 키와 정렬 키를 사용하여 단일 항목을 조회하는 작업입니다. 마치 도서관에서 청구기호로 정확히 한 권의 책을 찾는 것처럼, 키를 알면 즉시 데이터를 가져올 수 있습니다.
가장 빠른 조회 방법이며, 읽기 용량 단위도 가장 적게 소비합니다.
다음 코드를 살펴봅시다.
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, GetCommand } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({ region: "ap-northeast-2" });
const docClient = DynamoDBDocumentClient.from(client);
// 특정 사용자 정보를 조회하는 함수
async function getUser(userId) {
const params = {
TableName: "Users",
Key: {
userId: userId // 파티션 키로 조회
}
};
// GetItem 실행
const result = await docClient.send(new GetCommand(params));
// 데이터가 없으면 undefined 반환
return result.Item;
}
김개발 씨는 이제 두 번째 미션을 받았습니다. 사용자 ID를 받아서 해당 사용자의 프로필 정보를 보여주는 API를 만드는 것입니다.
데이터는 이미 저장했으니 불러오기만 하면 되는데, 어떻게 해야 할까요? 박시니어 씨가 커피를 한 모금 마시고 설명을 시작합니다.
"조회는 생각보다 선택지가 많아요. 하지만 키를 정확히 안다면 GetItem이 답입니다." GetItem의 핵심 원리 GetItem은 어떻게 동작할까요?
쉽게 비유하자면, GetItem은 마치 우편함에서 자신의 번호로 우편물을 찾는 것과 같습니다. 우편함 번호만 알면 다른 우편함을 뒤질 필요 없이 바로 꺼낼 수 있습니다.
DynamoDB도 마찬가지입니다. 파티션 키를 사용해 해시 함수로 정확한 위치를 계산하고, 그 위치에서 바로 데이터를 가져옵니다.
전통적인 조회 방식의 한계 SQL 데이터베이스에서 조회는 어땠을까요? SELECT 문으로 데이터를 가져올 때, 인덱스가 없으면 전체 테이블을 스캔해야 했습니다.
데이터가 많아질수록 조회 속도가 느려졌습니다. 인덱스를 만들어도 B-Tree를 탐색하는 시간이 필요했고, 복잡한 JOIN은 성능을 더욱 악화시켰습니다.
DynamoDB의 효율적인 접근 바로 이런 문제를 해결하기 위해 DynamoDB는 키-값 방식을 채택했습니다. GetItem을 사용하면 일정한 속도로 데이터를 조회할 수 있습니다.
테이블에 데이터가 백만 건이든 십억 건이든 조회 속도는 거의 동일합니다. 또한 읽기 용량 단위를 가장 적게 소비하여 비용도 절감됩니다.
무엇보다 코드가 단순해서 유지보수가 쉽다는 큰 이점이 있습니다. 코드 상세 분석 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 params 객체에서 TableName은 조회할 테이블을 지정합니다. Key 객체에는 파티션 키를 담는데, 여기서는 userId를 사용합니다.
만약 복합 키 테이블이라면 정렬 키도 함께 지정해야 합니다. GetCommand를 실행하면 result 객체가 반환됩니다.
중요한 점은 result.Item이 undefined일 수 있다는 것입니다. 해당 키로 데이터가 없으면 에러가 아니라 빈 결과를 반환합니다.
따라서 항상 Item의 존재 여부를 확인해야 합니다. 실무 활용 시나리오 실제 현업에서는 어떻게 활용할까요?
예를 들어 전자상거래 플랫폼을 개발한다고 가정해봅시다. 사용자가 주문 상세 페이지를 열 때, orderId를 파티션 키로 사용해서 GetItem으로 주문 정보를 가져옵니다.
밀리초 단위의 빠른 응답으로 사용자 경험을 향상시킬 수 있습니다. 아마존을 비롯한 많은 대규모 서비스에서 이런 패턴을 사용합니다.
조심해야 할 함정 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 GetItem으로 여러 항목을 조회하려고 반복문을 사용하는 것입니다.
이렇게 하면 네트워크 왕복 시간이 누적되어 성능이 급격히 나빠집니다. 따라서 여러 항목을 한 번에 조회해야 한다면 BatchGetItem을 사용해야 합니다.
성과 확인 다시 김개발 씨의 이야기로 돌아가 봅시다. 코드를 작성하고 테스트해본 김개발 씨는 놀랐습니다.
"와, 진짜 빠르네요!" GetItem을 제대로 이해하면 효율적인 조회 API를 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 데이터가 없을 수 있으므로 result.Item의 존재 여부를 항상 확인하세요
- ProjectionExpression을 사용하면 필요한 속성만 가져와 비용을 절감할 수 있습니다
- 여러 항목을 조회할 때는 BatchGetItem으로 한 번에 처리하세요
3. Query와 Scan 차이
김개발 씨는 새로운 요구사항을 받았습니다. 특정 날짜에 가입한 모든 사용자를 조회해야 한다고 합니다.
GetItem은 키를 알아야 하는데, 조건으로 검색하려면 어떻게 해야 할까요? 박시니어 씨가 중요한 이야기를 꺼냅니다.
"Query와 Scan의 차이를 정확히 이해해야 합니다."
Query는 파티션 키를 기준으로 데이터를 검색하는 효율적인 방법입니다. Scan은 테이블 전체를 읽어서 필터링하는 무차별 대입 방식입니다.
Query는 인덱스를 활용해 빠르고 비용이 적지만, Scan은 느리고 비용이 많이 듭니다. 가능한 한 Query를 사용하고, Scan은 최후의 수단으로만 사용해야 합니다.
다음 코드를 살펴봅시다.
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, QueryCommand, ScanCommand } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({ region: "ap-northeast-2" });
const docClient = DynamoDBDocumentClient.from(client);
// Query: 특정 사용자의 모든 주문 조회 (효율적)
async function getUserOrders(userId) {
const params = {
TableName: "Orders",
KeyConditionExpression: "userId = :userId",
ExpressionAttributeValues: {
":userId": userId
}
};
const result = await docClient.send(new QueryCommand(params));
return result.Items;
}
// Scan: 모든 VIP 사용자 조회 (비효율적)
async function getVIPUsers() {
const params = {
TableName: "Users",
FilterExpression: "userType = :type",
ExpressionAttributeValues: {
":type": "VIP"
}
};
const result = await docClient.send(new ScanCommand(params));
return result.Items;
}
김개발 씨는 복잡한 요구사항을 받았습니다. 특정 사용자의 모든 주문 내역을 보여주고, 또 다른 화면에서는 전체 VIP 회원 목록을 보여줘야 합니다.
GetItem으로는 안 될 것 같은데, 어떻게 해야 할까요? 박시니어 씨가 화이트보드 앞으로 가서 그림을 그리기 시작합니다.
"이건 정말 중요해요. Query와 Scan의 차이를 모르면 큰일 납니다." Query의 똑똑한 검색 Query는 정확히 어떻게 동작할까요?
쉽게 비유하자면, Query는 마치 도서관에서 분류번호로 책을 찾는 것과 같습니다. 소설 섹션의 800번대로 바로 가서, 그 안에서 저자 이름으로 정렬된 책들을 쭉 훑어봅니다.
처음부터 범위가 좁혀져 있으니 빠를 수밖에 없습니다. DynamoDB의 Query도 똑같이 파티션 키로 데이터 범위를 먼저 좁히고, 그 안에서 정렬 키로 검색합니다.
Scan의 무식한 검색 반면 Scan은 어떨까요? Scan은 마치 도서관의 모든 책을 처음부터 끝까지 확인하는 것과 같습니다.
한 권씩 집어서 "이게 내가 찾는 책인가?" 확인하고, 아니면 다음 책을 봅니다. 도서관에 책이 열 권이면 괜찮지만, 백만 권이면 어떻게 될까요?
상상만 해도 끔찍합니다. 성능과 비용의 엄청난 차이 왜 이 차이가 중요할까요?
실제 운영 환경에서 Scan을 잘못 사용하면 재앙이 됩니다. 예를 들어 천만 건의 데이터가 있는 테이블에서 Scan을 실행하면, 모든 데이터를 읽어야 합니다.
읽기 용량 단위가 폭발적으로 증가하고, AWS 요금 고지서를 보면 식은땀이 납니다. 더 큰 문제는 다른 요청들까지 느려진다는 것입니다.
Query의 강력한 능력 바로 이런 문제를 피하기 위해 Query를 최대한 활용해야 합니다. Query를 사용하면 파티션 내부에서만 검색하므로 읽는 데이터 양이 최소화됩니다.
또한 정렬 키로 범위 검색도 가능합니다. 무엇보다 일관된 빠른 성능으로 사용자 경험을 보장한다는 큰 이점이 있습니다.
코드로 차이 확인하기 위의 코드를 비교해 보겠습니다. Query 예제를 보면 KeyConditionExpression에서 파티션 키 조건을 지정합니다.
userId가 특정 값인 항목들만 검색하므로 효율적입니다. Orders 테이블이 userId를 파티션 키로, orderId를 정렬 키로 설계되어 있다면 해당 사용자의 주문만 읽습니다.
반면 Scan 예제는 FilterExpression을 사용합니다. 중요한 점은 필터링이 읽기 이후에 적용된다는 것입니다.
즉, 전체 테이블을 다 읽은 후에 userType이 VIP인 것만 골라냅니다. 읽기 비용은 전체 테이블 기준으로 청구됩니다.
실무에서의 설계 전략 실제 현업에서는 어떻게 활용할까요? 예를 들어 블로그 서비스를 개발한다고 가정해봅시다.
특정 작성자의 모든 게시물을 보여주려면, authorId를 파티션 키로 설계하고 Query로 조회합니다. 하지만 "인기 게시물"처럼 전체 데이터를 분석해야 한다면, Scan 대신 Global Secondary Index를 만들어서 Query로 해결하는 것이 좋습니다.
Scan을 써야 할 때 그렇다면 Scan은 절대 쓰면 안 될까요? 아닙니다.
Scan도 적절한 사용처가 있습니다. 예를 들어 배치 작업으로 전체 데이터를 마이그레이션하거나, 통계를 내기 위해 모든 데이터를 분석할 때는 Scan이 필요합니다.
중요한 것은 Scan을 실행할 때 Limit 파라미터로 한 번에 읽는 양을 제한하고, 여러 번 나눠서 실행하는 것입니다. 주의할 함정들 초보 개발자들이 흔히 하는 실수가 있습니다.
첫 번째는 Scan에 FilterExpression을 추가하면 Query처럼 효율적이라고 생각하는 것입니다. 필터는 읽기 이후에 적용되므로 비용은 동일합니다.
두 번째는 Query를 위해 테이블 설계를 바꾸지 않고 Scan으로 우회하려는 것입니다. 이는 기술 부채가 쌓이는 지름길입니다.
깨달음의 순간 다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"그러면 테이블 설계부터 Query를 염두에 두고 해야겠네요!" Query와 Scan의 차이를 제대로 이해하면 효율적인 데이터베이스 설계를 할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 가능하면 항상 Query를 사용하도록 테이블과 인덱스를 설계하세요
- Scan을 꼭 써야 한다면 Limit과 페이지네이션을 활용하세요
- FilterExpression은 읽기 비용을 줄이지 못한다는 점을 기억하세요
4. UpdateItem으로 수정
김개발 씨는 이제 사용자 정보 수정 기능을 만들어야 합니다. 사용자가 프로필에서 이름만 바꾸거나 이메일만 바꿀 수 있어야 하는데, 어떻게 부분 수정을 할 수 있을까요?
박시니어 씨가 말합니다. "UpdateItem은 특정 속성만 골라서 수정할 수 있어요."
UpdateItem은 기존 항목의 특정 속성만 수정하는 작업입니다. PutItem과 달리 전체를 덮어쓰지 않고, 지정한 속성만 변경하거나 추가, 삭제할 수 있습니다.
마치 워드 문서에서 일부 단어만 수정하는 것처럼, 나머지 데이터는 그대로 유지됩니다. 원자적 카운터 증가 같은 고급 기능도 지원합니다.
다음 코드를 살펴봅시다.
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, UpdateCommand } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({ region: "ap-northeast-2" });
const docClient = DynamoDBDocumentClient.from(client);
// 사용자 이메일만 수정하는 함수
async function updateUserEmail(userId, newEmail) {
const params = {
TableName: "Users",
Key: {
userId: userId
},
// SET으로 속성 값 변경
UpdateExpression: "SET email = :email, updatedAt = :now",
ExpressionAttributeValues: {
":email": newEmail,
":now": new Date().toISOString()
},
// 수정된 항목 반환
ReturnValues: "ALL_NEW"
};
const result = await docClient.send(new UpdateCommand(params));
return result.Attributes;
}
// 좋아요 수를 원자적으로 증가시키는 함수
async function incrementLikes(postId) {
const params = {
TableName: "Posts",
Key: { postId: postId },
UpdateExpression: "ADD likes :increment",
ExpressionAttributeValues: {
":increment": 1
},
ReturnValues: "UPDATED_NEW"
};
return await docClient.send(new UpdateCommand(params));
}
김개발 씨는 사용자 프로필 수정 API를 만들고 있습니다. 처음에는 GetItem으로 읽어서 값을 바꾼 다음 PutItem으로 저장하려 했습니다.
그런데 박시니어 씨가 코드 리뷰에서 지적합니다. "이렇게 하면 동시성 문제가 생길 수 있어요." 김개발 씨는 고개를 갸우뚱합니다.
"동시성 문제요?" UpdateItem의 원자성 UpdateItem은 어떻게 다를까요? 쉽게 비유하자면, UpdateItem은 마치 은행 계좌에서 잔액을 수정하는 것과 같습니다.
읽고-수정하고-쓰기를 따로 하면, 그 사이에 다른 거래가 끼어들 수 있습니다. 하지만 UpdateItem은 이 모든 과정을 하나의 원자적 작업으로 처리합니다.
중간에 다른 요청이 끼어들 수 없어서 데이터 무결성이 보장됩니다. PutItem의 위험성 왜 PutItem으로 수정하면 안 될까요?
PutItem은 항목 전체를 교체합니다. 예를 들어 사용자 A가 이름을 수정하는 동안, 사용자 B가 같은 사람의 이메일을 수정한다고 가정해봅시다.
A가 GetItem으로 읽고, B도 GetItem으로 읽습니다. A가 이름을 바꿔서 PutItem하고, B가 이메일을 바꿔서 PutItem합니다.
결과는 어떻게 될까요? B의 수정이 나중이므로 A가 바꾼 이름은 사라집니다.
UpdateItem의 안전한 방식 바로 이런 문제를 해결하기 위해 UpdateItem을 사용합니다. UpdateItem을 사용하면 지정한 속성만 수정되고 나머지는 유지됩니다.
또한 UpdateExpression으로 정확히 어떤 속성을 어떻게 바꿀지 명시합니다. 무엇보다 원자적 연산이므로 동시 수정에도 안전하다는 큰 이점이 있습니다.
UpdateExpression 문법 파헤치기 위의 코드를 한 줄씩 살펴보겠습니다. 첫 번째 예제에서 UpdateExpression은 "SET email = :email, updatedAt = :now"입니다.
SET 키워드는 속성 값을 설정하거나 변경합니다. 콜론으로 시작하는 :email은 플레이스홀더이고, 실제 값은 ExpressionAttributeValues에서 정의합니다.
이렇게 분리하면 SQL 인젝션 같은 보안 문제를 예방할 수 있습니다. ReturnValues는 수정 후 어떤 값을 반환할지 지정합니다.
ALL_NEW는 수정된 전체 항목을, UPDATED_NEW는 변경된 속성만 반환합니다. 반환값이 필요 없으면 NONE으로 설정하여 응답 크기를 줄일 수 있습니다.
두 번째 예제는 ADD 키워드를 사용합니다. ADD는 숫자를 증가시키거나 집합에 요소를 추가할 때 씁니다.
likes를 1 증가시키는데, 읽기와 쓰기가 원자적으로 처리되어 동시에 여러 사용자가 좋아요를 눌러도 정확히 카운트됩니다. 실무에서의 다양한 활용 실제 현업에서는 어떻게 활용할까요?
예를 들어 쇼핑몰 장바구니 서비스를 개발한다고 가정해봅시다. 사용자가 상품을 추가할 때마다 UpdateItem으로 items 배열에 append합니다.
REMOVE 키워드로 특정 속성을 삭제하거나, SET으로 중첩된 객체의 일부만 수정할 수도 있습니다. 많은 전자상거래 플랫폼에서 이런 패턴을 사용합니다.
더 강력한 기능들 UpdateItem은 단순 수정 이상의 기능을 제공합니다. SET 외에도 REMOVE로 속성을 삭제하고, DELETE로 집합에서 요소를 제거하고, ADD로 값을 증가시킬 수 있습니다.
if_not_exists 함수로 속성이 없을 때만 기본값을 설정하거나, list_append 함수로 배열에 요소를 추가할 수도 있습니다. 이런 연산들을 모두 하나의 UpdateExpression에 조합할 수 있습니다.
피해야 할 실수들 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 속성 이름을 직접 쓰는 것입니다.
예를 들어 "name"이라는 속성은 DynamoDB의 예약어와 충돌할 수 있습니다. 따라서 ExpressionAttributeNames를 사용해서 "#name"처럼 별칭을 만들어야 합니다.
또 다른 실수는 UpdateExpression에서 속성을 잘못 참조하여 새로운 속성을 의도치 않게 생성하는 것입니다. 자신감 상승 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 코드를 다시 작성했습니다. "이제 동시성 문제도 해결되고 코드도 더 간결해졌네요!" UpdateItem을 제대로 이해하면 안전하고 효율적인 수정 기능을 만들 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 예약어 충돌을 피하려면 항상 ExpressionAttributeNames를 사용하세요
- ADD 키워드로 원자적 카운터를 구현할 수 있습니다
- ReturnValues를 NONE으로 설정하면 응답 크기를 줄여 성능을 개선할 수 있습니다
5. DeleteItem으로 삭제
김개발 씨는 사용자 계정 삭제 기능을 구현해야 합니다. 사용자가 탈퇴 버튼을 누르면 데이터베이스에서 해당 사용자를 제거해야 하는데, 어떻게 해야 할까요?
박시니어 씨가 조언합니다. "DeleteItem은 간단하지만, 실수하면 복구가 어려우니 조심하세요."
DeleteItem은 키를 사용하여 단일 항목을 삭제하는 작업입니다. 마치 종이 문서를 파쇄기에 넣는 것처럼, 한 번 삭제하면 복구할 수 없습니다.
GetItem과 비슷하게 파티션 키와 정렬 키만 있으면 즉시 실행되며, 조건부 삭제도 가능합니다.
다음 코드를 살펴봅시다.
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, DeleteCommand } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({ region: "ap-northeast-2" });
const docClient = DynamoDBDocumentClient.from(client);
// 사용자를 삭제하는 함수
async function deleteUser(userId) {
const params = {
TableName: "Users",
Key: {
userId: userId
},
// 삭제 전 항목을 반환
ReturnValues: "ALL_OLD"
};
// DeleteItem 실행
const result = await docClient.send(new DeleteCommand(params));
// 삭제된 항목 정보 반환 (로그 기록용)
return result.Attributes;
}
// 조건부 삭제: 작성자만 게시물을 삭제할 수 있음
async function deletePost(postId, authorId) {
const params = {
TableName: "Posts",
Key: {
postId: postId
},
// 작성자가 일치할 때만 삭제
ConditionExpression: "authorId = :authorId",
ExpressionAttributeValues: {
":authorId": authorId
}
};
return await docClient.send(new DeleteCommand(params));
}
김개발 씨는 드디어 마지막 기본 기능인 삭제를 구현하게 되었습니다. 사용자 탈퇴 API를 만들어야 하는데, 데이터베이스에서 사용자 정보를 지우면 되겠죠?
그런데 박시니어 씨의 표정이 심각합니다. "삭제는 정말 조심해야 해요." 김개발 씨는 긴장됩니다.
"삭제가 그렇게 어려운가요?" DeleteItem의 단순함과 위험성 DeleteItem은 어떻게 동작할까요? 쉽게 비유하자면, DeleteItem은 마치 지우개로 연필 글씨를 지우는 것과 같습니다.
간단하고 빠르지만, 한 번 지우면 되돌릴 수 없습니다. 복사본을 만들어두지 않았다면 영원히 사라집니다.
DynamoDB의 DeleteItem도 똑같습니다. 파티션 키만 있으면 즉시 실행되고, 되돌릴 방법이 없습니다.
전통적인 삭제 방식 관계형 데이터베이스에서는 삭제를 어떻게 했을까요? DELETE FROM 문으로 데이터를 삭제했습니다.
하지만 실무에서는 물리적 삭제보다 논리적 삭제를 많이 사용했습니다. deleted_at 컬럼에 삭제 시각을 기록하고, 조회할 때 이 컬럼이 NULL인 것만 가져오는 방식입니다.
나중에 복구가 필요하거나 감사 목적으로 기록을 남겨야 할 때 유용했습니다. DynamoDB에서의 삭제 전략 DynamoDB에서도 같은 전략을 사용할 수 있을까요?
네, 가능합니다. DeleteItem으로 물리적 삭제를 할 수도 있지만, UpdateItem으로 isDeleted 같은 속성을 추가하는 논리적 삭제도 가능합니다.
또한 TTL 기능을 사용하면 지정된 시간이 지나면 자동으로 삭제되게 할 수도 있습니다. 무엇보다 삭제 전에 백업을 남기는 것이 안전하다는 점을 기억해야 합니다.
코드 상세 분석 위의 코드를 한 줄씩 살펴보겠습니다. 첫 번째 예제에서 ReturnValues를 ALL_OLD로 설정했습니다.
이렇게 하면 삭제되기 전의 항목 전체가 반환됩니다. 로그에 기록하거나 백업 용도로 유용합니다.
반환값이 필요 없다면 NONE으로 설정할 수 있습니다. 두 번째 예제는 ConditionExpression을 사용합니다.
게시물을 삭제할 때 작성자 ID가 일치하는지 확인합니다. 조건이 맞지 않으면 ConditionalCheckFailedException 에러가 발생하고 삭제되지 않습니다.
이런 방식으로 권한 검증을 데이터베이스 레벨에서 처리할 수 있습니다. 실무에서의 안전한 삭제 실제 현업에서는 어떻게 활용할까요?
예를 들어 채팅 애플리케이션을 개발한다고 가정해봅시다. 사용자가 메시지를 삭제할 때, 즉시 DeleteItem을 실행하기보다는 먼저 S3에 백업을 저장합니다.
그 다음 DeleteItem을 실행하고, 결과를 로그에 기록합니다. 나중에 복구 요청이 들어와도 백업에서 찾을 수 있습니다.
배치 삭제와 조건부 삭제 DeleteItem 하나로 부족할 때도 있습니다. 여러 항목을 한 번에 삭제하려면 BatchWriteItem을 사용할 수 있습니다.
최대 25개까지 한 번에 삭제할 수 있어서 네트워크 왕복을 줄입니다. 또한 ConditionExpression으로 복잡한 조건을 설정할 수도 있습니다.
예를 들어 특정 날짜 이전의 데이터만 삭제하거나, 특정 상태인 항목만 삭제하는 식입니다. TTL을 활용한 자동 삭제 더 편리한 방법도 있습니다.
DynamoDB는 TTL 기능을 제공합니다. 테이블에 TTL 속성을 지정하면, 그 시각이 지난 항목은 자동으로 삭제됩니다.
예를 들어 임시 세션 데이터나 만료된 쿠폰 같은 것들을 관리할 때 유용합니다. 개발자가 직접 DeleteItem을 실행할 필요 없이 DynamoDB가 알아서 정리해줍니다.
흔히 하는 실수들 초보 개발자들이 조심해야 할 점이 있습니다. 첫 번째는 삭제 전에 백업을 고려하지 않는 것입니다.
중요한 데이터라면 반드시 백업 전략이 있어야 합니다. 두 번째는 연관된 데이터를 함께 삭제하지 않는 것입니다.
예를 들어 사용자를 삭제할 때 해당 사용자의 주문, 리뷰, 포인트 등도 처리해야 합니다. 세 번째는 삭제 권한을 검증하지 않는 것입니다.
ConditionExpression으로 반드시 권한을 확인해야 합니다. 복구 불가능의 의미 박시니어 씨가 강조합니다.
"DeleteItem은 영구 삭제예요. Point-in-time Recovery를 활성화해도 백업 시점으로만 복구 가능하죠." 김개발 씨는 고개를 끄덕입니다.
"그럼 중요한 데이터는 논리적 삭제를 쓰는 게 낫겠네요." 안전한 개발자로 성장 다시 김개발 씨의 이야기로 돌아가 봅시다. 코드를 신중하게 작성한 김개발 씨는 테스트 환경에서 충분히 검증했습니다.
"이제 삭제도 무섭지 않아요!" DeleteItem을 제대로 이해하면 안전한 삭제 기능을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 중요한 데이터는 물리적 삭제보다 논리적 삭제를 고려하세요
- ReturnValues를 ALL_OLD로 설정하여 삭제 전 데이터를 백업하세요
- TTL 기능으로 임시 데이터를 자동으로 정리할 수 있습니다
6. 조건부 작업
김개발 씨는 거의 모든 기능을 구현했지만, 한 가지 문제가 생겼습니다. 동시에 여러 사람이 같은 재고 상품을 주문하면 재고가 마이너스가 되는 버그가 발생합니다.
박시니어 씨가 해결책을 제시합니다. "ConditionExpression으로 조건부 작업을 사용하세요."
조건부 작업은 특정 조건이 만족될 때만 작업을 실행하는 강력한 기능입니다. 마치 ATM에서 잔액이 충분할 때만 출금이 되는 것처럼, DynamoDB도 조건을 검사한 후 작업을 수행합니다.
낙관적 잠금, 중복 방지, 권한 검증 등 다양한 용도로 활용되며, 데이터 무결성을 보장하는 핵심 도구입니다.
다음 코드를 살펴봅시다.
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { DynamoDBDocumentClient, UpdateCommand, PutCommand } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({ region: "ap-northeast-2" });
const docClient = DynamoDBDocumentClient.from(client);
// 재고가 충분할 때만 차감하는 함수
async function decreaseStock(productId, quantity) {
const params = {
TableName: "Products",
Key: { productId: productId },
UpdateExpression: "SET stock = stock - :quantity",
// 재고가 충분한지 확인
ConditionExpression: "stock >= :quantity",
ExpressionAttributeValues: {
":quantity": quantity
},
ReturnValues: "UPDATED_NEW"
};
try {
const result = await docClient.send(new UpdateCommand(params));
return { success: true, newStock: result.Attributes.stock };
} catch (error) {
if (error.name === "ConditionalCheckFailedException") {
return { success: false, message: "재고 부족" };
}
throw error;
}
}
// 중복 생성 방지: 같은 키가 없을 때만 생성
async function createUserIfNotExists(userId, userData) {
const params = {
TableName: "Users",
Item: {
userId: userId,
...userData,
createdAt: new Date().toISOString()
},
// userId가 없을 때만 생성
ConditionExpression: "attribute_not_exists(userId)"
};
try {
await docClient.send(new PutCommand(params));
return { success: true };
} catch (error) {
if (error.name === "ConditionalCheckFailedException") {
return { success: false, message: "이미 존재하는 사용자" };
}
throw error;
}
}
김개발 씨는 심각한 버그 리포트를 받았습니다. 인기 상품이 품절되었는데도 주문이 계속 들어오고, 재고 수량이 마이너스가 되었다는 것입니다.
어떻게 이런 일이 생긴 걸까요? 박시니어 씨가 코드를 보더니 한숨을 쉽니다.
"조건부 검사를 안 했네요." 김개발 씨는 당황합니다. "재고를 확인한 다음 차감했는데요?" 조건부 작업의 필요성 왜 조건 확인만으로는 부족할까요?
쉽게 비유하자면, 조건부 작업은 마치 화장실 문의 잠금장치와 같습니다. 누군가 문을 열려고 할 때 "지금 비어있나요?"라고 물어보는 것만으로는 부족합니다.
물어본 순간에는 비어있었지만, 문을 여는 사이에 다른 사람이 들어갈 수 있습니다. 잠금장치는 "비어있는지 확인하고 들어가기"를 하나의 원자적 작업으로 만듭니다.
경쟁 조건의 위험 김개발 씨의 코드에는 무슨 문제가 있었을까요? 기존 코드는 먼저 GetItem으로 재고를 조회하고, 충분하면 UpdateItem으로 차감했습니다.
문제는 GetItem과 UpdateItem 사이에 다른 요청이 끼어들 수 있다는 것입니다. 사용자 A가 재고 1개를 확인하고, 사용자 B도 재고 1개를 확인합니다.
그런 다음 A가 차감하고 B도 차감하면, 재고는 -1이 됩니다. 조건부 작업의 원자성 바로 이런 문제를 해결하기 위해 ConditionExpression이 등장했습니다.
ConditionExpression을 사용하면 조건 확인과 작업 실행이 하나의 원자적 작업이 됩니다. 또한 조건이 맞지 않으면 작업이 전혀 실행되지 않고 에러가 발생합니다.
무엇보다 애플리케이션 코드에서 복잡한 잠금 메커니즘을 구현할 필요가 없다는 큰 이점이 있습니다. 코드로 원리 이해하기 위의 코드를 한 줄씩 살펴보겠습니다.
첫 번째 예제에서 ConditionExpression은 "stock >= :quantity"입니다. 이것은 현재 재고가 차감하려는 수량보다 크거나 같은지 확인합니다.
중요한 점은 이 확인과 UpdateExpression의 실행이 원자적으로 처리된다는 것입니다. 조건이 맞지 않으면 ConditionalCheckFailedException이 발생하고 재고는 변하지 않습니다.
두 번째 예제는 attribute_not_exists 함수를 사용합니다. 이것은 해당 속성이 존재하지 않을 때만 true를 반환합니다.
같은 userId로 PutItem을 두 번 실행해도, 첫 번째만 성공하고 두 번째는 실패합니다. 이렇게 중복 생성을 방지할 수 있습니다.
다양한 조건 표현식 ConditionExpression은 매우 강력합니다. 비교 연산자로는 =, <>, <, >, <=, >= 등을 사용할 수 있습니다.
함수로는 attribute_exists, attribute_not_exists, begins_with, contains 등이 있습니다. AND, OR, NOT으로 여러 조건을 조합할 수도 있습니다.
예를 들어 "status = :active AND balance > :minBalance" 같은 복잡한 조건도 가능합니다. 실무에서의 활용 사례 실제 현업에서는 어떻게 활용할까요?
예를 들어 예약 시스템을 개발한다고 가정해봅시다. 특정 시간대의 좌석을 예약할 때, ConditionExpression으로 해당 좌석이 아직 예약되지 않았는지 확인합니다.
또 다른 사례로 포인트 시스템에서 포인트 차감 시 잔액이 충분한지 검증할 수 있습니다. 많은 금융 서비스와 전자상거래 플랫폼에서 이런 패턴을 사용합니다.
낙관적 잠금 구현 조건부 작업의 고급 활용법도 있습니다. version 속성을 추가하고, UpdateItem할 때 "version = :oldVersion" 조건을 걸면 낙관적 잠금을 구현할 수 있습니다.
누군가 먼저 수정했다면 version이 증가하여 조건이 실패하고, 다시 읽어서 재시도해야 합니다. 이 방식은 비관적 잠금보다 성능이 좋고, 동시성도 높습니다.
에러 처리의 중요성 조건부 작업에서 에러 처리는 필수입니다. ConditionalCheckFailedException은 조건이 맞지 않을 때 발생하는데, 이것을 일반적인 에러와 구분해야 합니다.
재고 부족은 사용자에게 "품절"이라고 안내하면 되지만, 다른 에러는 시스템 장애일 수 있습니다. 따라서 try-catch로 에러 타입을 확인하고 적절히 처리해야 합니다.
성능과 비용 고려사항 조건부 작업은 공짜가 아닙니다. 조건을 검사하는 과정에서도 읽기 용량 단위가 소비됩니다.
조건이 실패해도 용량은 차감됩니다. 하지만 이것은 데이터 무결성을 위한 필수 비용입니다.
잘못된 데이터가 저장되어 나중에 수동으로 정리하는 비용보다 훨씬 저렴합니다. 흔히 하는 실수들 초보 개발자들이 조심해야 할 점이 있습니다.
첫 번째는 조건식에서 속성 이름을 잘못 쓰는 것입니다. 오타가 나면 attribute_not_exists처럼 작동하여 예상과 다른 결과가 나옵니다.
두 번째는 ConditionalCheckFailedException을 처리하지 않아서 애플리케이션이 크래시되는 것입니다. 세 번째는 모든 작업에 불필요한 조건을 추가하여 복잡도를 높이는 것입니다.
완벽한 이해 다시 김개발 씨의 이야기로 돌아가 봅시다. 조건부 작업을 적용한 김개발 씨는 다시 테스트했습니다.
여러 사용자가 동시에 주문해도 재고가 정확히 관리됩니다. "이제 동시성 문제도 해결됐어요!" 조건부 작업을 제대로 이해하면 안전하고 견고한 시스템을 만들 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 재고, 포인트 같은 중요한 값은 반드시 조건부 작업으로 보호하세요
- attribute_not_exists로 중복 생성을 방지할 수 있습니다
- version 속성으로 낙관적 잠금을 구현하면 동시성을 높일 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
서비스 메시 완벽 가이드
마이크로서비스 간 통신을 안전하고 효율적으로 관리하는 서비스 메시의 핵심 개념부터 실전 도입까지, 초급 개발자를 위한 완벽한 입문서입니다. Istio와 Linkerd 비교, 사이드카 패턴, 실무 적용 노하우를 담았습니다.
EFK 스택 로깅 완벽 가이드
마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.
Grafana 대시보드 완벽 가이드
실시간 모니터링의 핵심, Grafana 대시보드를 처음부터 끝까지 배워봅니다. Prometheus 연동부터 알람 설정까지, 초급 개발자도 쉽게 따라할 수 있는 실전 가이드입니다.
분산 추적 완벽 가이드
마이크로서비스 환경에서 요청의 전체 흐름을 추적하는 분산 추적 시스템의 핵심 개념을 배웁니다. Trace, Span, Trace ID 전파, 샘플링 전략까지 실무에 필요한 모든 것을 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.