작은 메모 앱이 있는데, 메모를 영구적으로 보관할 곳이 없습니다. 서버 자체 디스크에 저장된 기록은 해당 서버가 교체되면 사라지므로, 어떤 기계보다 오래 지속되는 관리형 테이블이 필요합니다. 터미널을 열고 즉시 두 가지 질문에 부딪힙니다. 첫 번째 명령은 무엇이어야 하는지, 그리고 읽기 작업이 저렴한지 비싼지 어떻게 알 수 있을까요?
이 실습에서는 Amazon DynamoDB를 사용하여 이 두 가지 질문에 답합니다. DynamoDB는 API를 통해서만 접근할 수 있는 관리형 테이블 서비스입니다. 핵심 주장은 간단합니다. 테이블을 생성할 때 선택하는 파티션 키(partition key)가 두 가지 것을 결정한다는 것입니다: 모든 쓰기 작업이 어떤 항목에 영향을 미치는지, 그리고 모든 읽기 작업이 얼마나 많은 작업을 수행하게 하는지. 키로 읽는 것은 단일 항목으로 바로 이동합니다. 스캔(Scan)은 모든 항목을 읽으며, 응답 자체에서 그 차이를 관찰할 수 있습니다. 테이블을 생성하고, 채우고, 세 가지 방식으로 읽어보고, 각각의 비용을 비교한 다음, 삭제하는 과정을 진행하게 됩니다.
설정은 간단합니다: 하나의 ID, 하나의 리전, 하나의 과금 방식
계정, 루트 사용자(root user)가 아닌 일상적인 관리자 ID, 그리고 AWS command-line interface (CLI), 버전 2를 준비하고 해당 ID로 로그인해야 합니다. 이 실습에서는 Asia Pacific (Mumbai) 리전인 ap-south-1을 사용합니다. CLI가 아직 해당 리전으로 설정되어 있지 않다면 다음 명령어를 실행하세요:
aws configure set region ap-south-1
로그인한 ID를 확인하려면 aws sts get-caller-identity를 실행합니다. 출력의 Account 필드가 12자리 계정 ID입니다. CLI 문서에 따르면 이 명령어는 권한이 필요하지 않으므로 어떤 ID를 사용하든 작동합니다.
이제 비용 문제입니다. 테이블은 **온디맨드 용량(on-demand capacity)**을 사용하며, 이는 읽기 및 쓰기 요청마다 지불하고 사전에 용량을 계획할 필요가 없는 방식입니다. 서비스 문서에서는 이를 기본적이고 권장되는 방식으로 부릅니다. 또한
2026년 10월 2일 기준으로, 가격 책정 페이지에는 리전당 Free Tier로 25 GB의 스토리지 용량이 명시되어 있었습니다. 또한 무료 읽기 및 쓰기 용량 단위가 각각 25개씩 명시되었지만, 이는 온디맨드(on-demand) 요청이 아닌 프로비저닝(provisioned) 모드에 적용됩니다. 이 실습에서 사용되는 몇십 개의 요청은 개별 요청당 비용이 청구됩니다.
Table listing which on-demand table costs are billed: requests and storage yes, idle throughput no
온디맨드 방식은 요청 및 저장된 데이터 각각에 대해 비용을 청구하며, 유휴 상태의 테이블에는 처리량(throughput) 요금이 없습니다.
파티션 키가 첫 번째 결정 사항인 이유: 항목의 식별자이기 때문입니다.
**테이블(table)**은 **항목(item)**들의 모음이며, 이는 행(row)과 유사합니다. 각 항목은 필드와 유사한 속성(attribute) 그룹입니다. 모든 항목은 다른 어떤 항목도 공유하지 않는 **기본 키(primary key)**를 가져야 합니다. 가장 간단한 경우 이 키는 하나의 속성이며, 이것이 바로 **파티션 키(partition key)**입니다. DynamoDB는 파티션 키의 값을 내부 해시 함수에 입력하고, 그 결과가 해당 항목을 어디에 저장할지 결정합니다.
이를 번호가 매겨진 사물함 벽이라고 생각해보세요. 테이블이 벽이고, 파티션 키는 각 문에 적힌 번호입니다. 무언가를 가져오려면 번호를 부르고 곧장 그 사물함으로 걸어갑니다.
이 실습의 테이블은 hoc-notes이며, 파티션 키는 문자열(string)인 noteId이고 정렬 키(sort key)가 없습니다. 이를 생성하세요:
aws dynamodb create-table \
--table-name hoc-notes \
--attribute-definitions AttributeName=noteId,AttributeType=S \
...
이 명령어의 세 부분은 학습에 도움이 됩니다:
--attribute-definitions는 오직 키만 나열합니다. title이나 body 같은 것은 절대 선언하지 않습니다. 키를 제외하고 테이블은 _스키마리스(schemaless)_이며, 이는 각 항목이 다른 속성을 가질 수 있다는 의미입니다.
KeyType=HASH는 noteId를 파티션 키로 지정합니다. 파티션 키의 이전 명칭은 해시 함수 이후의
왜 title 대신 noteId를 사용해야 할까요? 앱이 링크나 API 경로를 통해 ID로 노트를 가져오기 때문이며, 키는 고유해야 하기 때문입니다. 제목은 반복될 수 있습니다. 다음 섹션에서 보여주듯이, 키가 같은 두 개의 노트는 공존할 수 없습니다.
확인해야 할 사항: 명령을 실행하면 즉시 "TableStatus": "CREATING"이 반환됩니다. 상태가 변경될 때까지 확인하세요:
aws dynamodb describe-table --table-name hoc-notes --query "Table.TableStatus"
"ACTIVE"가 출력되면, 테이블은 쓰기 작업 준비가 완료된 것입니다.
Writing or reading by noteId passes the key through the hash function to one storage location
파티션 키의 값은 각 항목이 어디에 저장될지 결정하는 해시 함수를 거칩니다.
항목은 타입 지정된 JSON(typed JSON)으로 작성되며, 재사용된 키는 조용히 대체됩니다
CLI는 항목을 **타입 지정된 JSON(typed JSON)**으로 작성합니다: 모든 값은 자신의 타입으로 감싸집니다. 문자열은 {"S": "Buy milk"}입니다. 숫자 2는 {"N": "2"}입니다.
이 숫자는 따옴표 안에 있습니다. 처음에는 잘못되어 보이지만, 이것이 올바른 방식입니다. N은 값이 숫자임을 나타내고, 그 값 자체는 문자열로 전송됩니다. {"N": 2}를 작성하는 것이 주의해야 할 실수입니다.
몇 가지 노트를 추가해 보세요:
aws dynamodb put-item --table-name hoc-notes \
--item '{"noteId": {"S": "n-001"}, "title": {"S": "Buy milk"}, "body": {"S": "Two litres"}}'
...
고유한 ID를 가진 두세 개의 항목을 더 추가합니다. n-002에는 priority 속성이 있고 n-001에는 그렇지 않다는 점에 주목하세요. 이것이 스키마리스(schemaless) 저장 방식의 작동입니다.
휴대폰 연락처 목록이라고 생각해보세요. 일부 연락처는 생일이 있고, 일부는 이메일 주소가 있으며, ID만 필요할 수도 있습니다.
이제 의도적으로 실패를 시도해 보세요. 다른 내용으로 다시 n-001을 작성합니다:
aws dynamodb put-item --table-name hoc-notes \
--item '{"noteId": {"S": "n-001"}, "title": {"S": "Call home"}}'
확인해야 할 사항: 아무것도 없습니다. 오류나 경고가 없습니다.
항목을 읽어보면 Call home이라고 표시되며, 아예 body가 없습니다. 문서에는 put 작업이 정확히 설명되어 있습니다. 이는 “새 항목을 생성하거나 기존 항목을 새 항목으로 대체합니다.”라는 내용입니다. 동일한 키로 두 번 쓰기(write)를 하면 하나의 항목에 대한 두 가지 버전이 됩니다. 두 번째 쓰기가 첫 번째 것을 속성까지 포함하여 완전히 대체합니다. 로커 그림에서, 당신은 두 번째 로커를 추가하지 않았습니다. 기존 로커를 비우고 다시 채운 것입니다.
A second put with key n-001 succeeds silently and the later read returns only Call home
동일한 키로 두 번의 put을 수행하면 하나의 항목만 남습니다: 두 번째가 첫 번째를 완전히 대체합니다
GetItem은 하나의 항목만 읽으며, 이는 키로 바로 이동하기 때문입니다
노트 앱에는 두 가지 다른 작업이 있습니다. 특정 노트 링크를 열면 앱이 키를 알고 있다는 의미이고, 홈 페이지를 보여주려면 모든 노트를 나열해야 하므로 어떤 키도 모른다는 의미입니다. 각 작업은 다른 읽기(read) 작업을 요구하므로 둘 다 측정해야 합니다.
GetItem은 전체 기본 키(primary key)를 받아 해당 항목 하나만 반환하거나, 아무것도 반환하지 않습니다. 이 요청이 소비한 용량을 보고하도록 요청해 보세요:
aws dynamodb get-item --table-name hoc-notes \
--key '{"noteId": {"S": "n-002"}}' \
--return-consumed-capacity TOTAL
확인해야 할 사항: 해당 항목과 함께, 이 요청이 사용한 읽기 용량을 보여주는 ConsumedCapacity 블록이 나타납니다. 이 수치를 적어두세요. 이것이 당신의 기준선(baseline)입니다.
읽기는 기본적으로 최종적 일관성(eventually consistent)을 가집니다. --consistent-read를 추가하면 대신 강한 일관성(strongly consistent read)을 얻게 됩니다. 이는 그 이전에 완료된 모든 쓰기를 반영하며, 비용은 두 배로 많이 듭니다. 플래그와 함께 명령어를 한 번 실행하고 두 ConsumedCapacity 수치를 비교하세요.
Scan은 모든 항목을 읽으며, 필터는 이를 더 저렴하게 만들지 못합니다
홈 페이지의 경우 이동할 키가 없습니다. 벽에 걸린 모든 로커를 하나씩 열어봅니다. 이것이 바로 Scan입니다:
aws dynamodb scan --table-name hoc-notes --return-consumed-capacity TOTAL
확인해야 할 사항: 모든 노트 목록, ConsumedCapacity 블록, 그리고 두 개의 카운트가 표시됩니다. ScannedCount는 읽은 항목의 수를 나타냅니다. Count는 반환된 항목의 수를 나타냅니다. 필터링되지 않은 Scan의 경우 이 둘은 같습니다.
이제 필터를 통해 노트 하나만 요청해 보세요. **필터 표현식(filter expression)**은 돌아오는 결과를 좁힙니다:
aws dynamodb scan --table-name hoc-notes \
--filter-expression "title = :t" \
--expression-attribute-values '{":t": {"S": "Call home"}}' \
...
확인해야 할 사항: Count는 1로 떨어지지만, ScannedCount는 여전히 전체 노트 수와 같습니다. 문서에는 그 이유가 명시되어 있습니다: "Scan은 필터 표현식이 존재하는지와 관계없이 동일한 양의 읽기 용량(read capacity)을 소비합니다." 필터는 읽기 작업 후에 적용됩니다. 이는 작업량을 줄이는 것이 아니라, 답변만 줄입니다. 모든 사물함을 열고 빨간 가방만 가져오는 것과 같습니다.
Scan은 페이지 단위로 실행됩니다. Scan은 요청당 최대 1MB를 읽으며, CLI는 전체 테이블을 읽을 때까지 계속해서 다음 페이지를 요청합니다.
작은 Count 옆에 큰 ScannedCount가 있다는 것은 비효율적인 읽기 작업의 특징입니다. 노트가 다섯 개일 때는 그 낭비가 눈에 띄지 않습니다. 하지만 노트가 백만 개라면, 하나의 제목을 찾는 Scan이 백만 개의 항목을 읽는 반면, 키를 사용한 GetItem은 여전히 하나만 읽습니다. On-demand 용량(capacity)은 읽은 양에 대해 청구하므로, 이는 속도 문제일 뿐만 아니라 비용 문제이기도 합니다.
GetItem goes from key to one read; a filtered Scan reads all items, then filters, then returns fewer
GetItem은 이름이 지정된 항목만 읽지만, 필터링된 Scan은 모든 항목을 읽고 더 적게 반환합니다.
제 관점에서는 작업 규칙이 직접적으로 따라옵니다. 연습 앱의 노트 목록과 같이 모든 항목이 필요하고 테이블 크기가 작은 경우에는 Scan을 선택하세요. 키를 지정할 수 있을 때는 언제든지 GetItem을 사용하세요.
Query는 여기서 GetItem과 유사하며, 정렬 키(sort key)와 함께 그 자리를 차지합니다
세 번째 읽기 작업은 Query입니다. Query는 하나의 파티션 키 값을 공유하는 항목들을 반환합니다. 이의 키 조건(key condition)은 파티션 키가 등가성(equality)을 테스트해야 하며, 값은 콜론(:)으로 시작하는 플레이스홀더를 통해 전달되어야 합니다:
aws dynamodb query --table-name hoc-notes \
--key-condition-expression "noteId = :id" \
--expression-attribute-values '{":id": {"S": "n-002"}}'
...
어떤 것을 보게 될까요: 하나의 항목입니다. 파티션 키만 있는 테이블에서는 두 개의 항목이 같은 값을 공유할 수 없으므로, Query는 최대 하나의 항목을 반환하며, 이는 GetItem이 반환하는 것과 동일합니다.
Query가 필요한 경우는 테이블에 **정렬 키(sort key)**라는 선택적 두 번째 키 속성이 있을 때입니다. 파티션 키를 공유하는 항목들은 정렬 키 순서로 함께 저장됩니다. 아티스트와 노래 제목으로 키가 지정된 음악 테이블을 상상해 보세요. 한 아티스트를 조회하는 것은 그 아티스트 이름 서랍을 꺼내는 것과 같습니다: 모든 노래는 제목 순서로 나오고, 다른 어떤 서랍도 건드리지 않습니다.
Card comparing GetItem, Query and Scan by what you must know and how many items each reads
세 가지 읽기 작업은 무엇을 알아야 하고 얼마나 많이 읽는지에서 차이가 납니다
정리하면 청구할 것이 없습니다