DynamoDB에서 데이터를 모델링하는 법
SQL에서는 먼저 엔터티와 관계를 모델링한 뒤, 나중에 무엇을 요청하든 쿼리 플래너가 알아서 조립해 주리라 믿습니다. DynamoDB는 그것을 뒤집습니다. 여러분은 이미 하게 될 줄 아는 읽기를 모델링하고, 키는 그 읽기를 위해 존재합니다.
조인 엔진도 없고, 런타임에 전략을 고르는 플래너도 없습니다. Query는 하나의 키를 따라 하나의 파티션을 읽으며, 그것이 성능 계약의 전부입니다. 그러니 여러분은 깔끔한 스키마가 아니라 알려진 액세스 패턴을 위해 키를 설계합니다.
AWS는 모범 사례 안내서에서 이를 분명히 말합니다: "스키마가 답해야 할 질문을 알기 전에는 스키마 설계를 시작해서는 안 됩니다."
이 가이드는 하나의 도메인 위에서 전체 과정을 밟아 나갑니다: 플레이어, 그들이 치르는 경기, 시즌별 순위를 추적하는 멀티플레이어 게임 리더보드입니다. 질문 목록에서 시작해 작동하는 키 스키마에 도달합니다.
DynamoDB에서 데이터를 어떻게 모델링하나요?
테이블이 아니라 읽기를 먼저 모델링하세요. 앱이 하는 모든 쿼리를 나열한 뒤, 각 질문이 하나의 Query나 GetItem으로 해결되도록 와 를 설계하세요. 함께 읽는 항목을 같은 곳에 두고, 값의 범위를 정렬 키에서 훑고, 베이스 테이블이 처리할 수 없는 액세스 패턴에는 GSI를 추가하세요.
- 테이블이 아니라 읽기를 먼저 나열하세요. 질문이 곧 명세이고, 명사는 주의를 흩뜨립니다.
- 각 질문은 하나의
Query나GetItem이어야 합니다. 어떤 질문이Scan을 필요로 한다면 모델이 잘못된 것입니다. - 함께 놓인 항목은 를 공유하고, 범위를 훑는 것은 무엇이든 에 들어갑니다.
- 베이스 테이블이 답할 수 없는 질문에는 를 부여하세요 — 필터를 곁들인
Scan은 절대 안 됩니다.
1단계 — 문제를 테이블이 아니라 질문으로 틀 잡기
players, matches, scores 테이블을 그리고 싶은 충동을 참으세요. 그 본능은 SQL 습관이며, 여기서는 틀립니다. 대신 앱이 실제로 수행하는 모든 읽기를 적어 두세요. 우리 리더보드의 경우:
- id로 한 플레이어의 프로필 가져오기.
- 한 플레이어의 최근 경기를 최신순으로 나열하기.
- 특정 시즌의 상위 N명 플레이어를 레이팅순으로 보여 주기.
- 공개 핸들로 플레이어 조회하기(예: 프로필 URL용).
이 네 질문이 — 명사가 아니라 — 명세입니다. 각각은 하나의 Query(또는 GetItem)로 해결되어야 합니다. 그것이 DynamoDB가 규모에서 저렴하게 처리하는 유일한 액세스 형태이기 때문입니다.
어떤 질문이 테이블을 스캔해야만 답할 수 있다면 모델이 잘못된 것이고, 여러분은 그것을 지연 시간과 비용에서 느끼게 됩니다 — Scan이 피해야 할 함정인 이유는 Query 대 Scan을 보세요.
전체 방법은 도메인마다 한 번씩 돌리는 짧고 순서 있는 파이프라인입니다:
아래 각 단계는 하나의 상자에 대응합니다: 나열, 열거, 키 설계, 나머지를 위한 인덱스 추가, 그리고 검증.
2단계 — 모델링에 쓰는 기본 요소 이해하기
테이블에는 항목이 어느 물리 파티션에 사는지 고르는 파티션 키(PK)와, 항목을 그 파티션 안에서 정렬하는 선택적 정렬 키(SK)가 있습니다.
AWS의 핵심 구성 요소 문서는 이 쌍을 항목의 기본 키라 부릅니다. Query는 언제나 정확히 하나의 PK 값을 대상으로 하며 SK를 범위 스캔하거나 필터링할 수 있습니다 — 그것이 도구 전부입니다.
이 단일 파티션 설계가 바로 DynamoDB가 2007년 Amazon Dynamo 논문에서 처음 기술된, 예측 가능하고 지연 시간이 낮으며 수평으로 분할된 읽기를 제공하게 하는 것입니다.
아래의 모든 결정을 이끄는 두 가지 결과가 있습니다:
- 함께 읽는 항목은 파티션 키를 공유해야 하나의
Query가 그것들을 단일 청구 요청으로 반환합니다. - 범위를 훑고 싶은 것은 무엇이든(최근 경기, 상위 레이팅) 정렬 키에 있어야 합니다.
Query가 정렬하고 경계 지을 수 있는 유일한 속성이기 때문입니다.
어떤 질문이 베이스 테이블이 제공하는 것과 다른 액세스 형태를 필요로 할 때, 글로벌 보조 인덱스를 추가합니다 — 다른 PK/SK 아래로 테이블을 재프로젝션한 것입니다.
(GSI 대 로컬 보조 인덱스는 GSI 대 LSI를 보세요.)
3단계 — 한 번에 질문 하나씩, 키 설계하기
우리는 범용의 오버로드된 키 속성을 가진 단일 테이블을 사용합니다 — 싱글 테이블 방식 — 플레이어와 그의 경기가 함께 읽히기 때문입니다.
여러분만의 접두사를 만드세요. 여기서는 PLAYER#, MATCH#, SEASON#이 그 밖에는 범용인 키 안에서 엔터티 유형을 태그합니다.
질문 1과 2(프로필 + 최근 경기)는 파티션을 공유하므로 둘 다 같은 PK에 매달립니다:
| partitionId | rangeId | attributes |
|---|---|---|
| PLAYER#u8231 | PROFILE | handle, region, createdAt |
| PLAYER#u8231 | MATCH#2026-06-23T14 | result=win, ratingDelta=+18, mapId |
| PLAYER#u8231 | MATCH#2026-06-23T11 | result=loss, ratingDelta=-15, mapId |
Query partitionId = "PLAYER#u8231"은 프로필과 모든 경기를 한 번의 읽기로 반환합니다. 프로필만 필요하면 GetItem.
최근 경기는 rangeId begins_with "MATCH#"에 ScanIndexForward = false를 곁들이면 최신순으로 훑습니다 — 정렬 키의 타임스탬프가 정렬을 거저 해 줍니다.
질문 3과 4는 그 파티션에서 답할 수 없습니다 — 이들은 시즌 순위와 핸들을 축으로 삼는데, 둘 다 베이스 PK가 아닙니다. 각각 GSI를 부여합니다.
우리는 두 쌍의 범용 인덱스 속성을 추가합니다 — 순위 인덱스용 seasonPartition / seasonSort와 핸들 인덱스용 handlePartition / handleSort — 이를 같은 프로필 항목(3단계에서 쓴 그 항목이며, 이제 인덱스 속성이 채워진 채로 표시됨)에 채워 넣습니다:
| partitionId | rangeId | seasonPartition | seasonSort | handlePartition | handleSort |
|---|---|---|---|---|---|
| PLAYER#u8231 | PROFILE | SEASON#2026-Q2 | RATING#1842 | HANDLE#nighthawk | PLAYER#u8231 |
이제 시즌 인덱스를 WHERE seasonPartition = "SEASON#2026-Q2"로 Query하고 ScanIndexForward = false를 주면 레이팅순으로 정렬된 플레이어가 반환됩니다 — 그것이 리더보드입니다.
handlePartition = "HANDLE#…"로 키를 잡은 두 번째 인덱스는 공개 핸들을 한 번의 읽기로 플레이어 id로 해결합니다. 하나의 물리 테이블, 네 개의 단일 Query 액세스 패턴.
RATING#1842에 대한 참고: DynamoDB는 정렬 키를 숫자가 아니라 사전순으로 정렬하므로, 레이팅은 고정 너비로 제로 패딩되어야 합니다(RATING#01842). 그러지 않으면9가1000뒤에 정렬됩니다. 이것은 처음부터 제대로 잡아 둘 가치가 있는 고전적인 모델링 함정입니다.
4단계 — DynoTable에서 모델 검증하기
키 스키마는 실제 Query가 기대한 항목을 정확히, 그 이상도 이하도 없이 반환하는 것을 지켜볼 때에만 신뢰를 얻습니다.
DynoTable에서 테이블을 열고, 시즌 인덱스에 대해 리더보드 쿼리를 실행하고, 파티션이 정렬되고 경계 지어진 채로 반환되는지 확인하세요 — Scan도, 클라이언트 측 정렬도 없이요.

이 쿼리들의 조건 표현식 — begins_with, seasonPartition = :p, 플레이스홀더 :p 바인딩 — 을 만들 때는 DynamoDB 표현식 빌더에 맡기세요.
이것은 KeyConditionExpression, ExpressionAttributeNames, ExpressionAttributeValues를 생성하므로, result 같은 예약어나 오타 난 플레이스홀더가 읽기를 조용히 깨뜨리는 일이 없습니다.
5단계 — 함정과 다음 단계
모델을 출시하기 전에 확인할 몇 가지 함정:
- 함께 읽지 않는 관계는 모델링하지 마세요. 질문마다 GSI 하나는 값싸지만, 낭비된 GSI는 반복되는 비용입니다. 인덱스는 투기적으로가 아니라 질문 목록에서 추가하세요.
- 파티션 열기를 주시하세요. 하나의 PK(유명 플레이어, 하나의 뜨거운 시즌)가 대부분의 트래픽을 흡수하면 그 파티션이 스로틀될 수 있습니다. 키가 명백히 뜨거울 때는 접미사 샤드로 쓰기를 분산하세요 — AWS는 이를 파티션 키 설계에서 다룹니다.
- 정렬 키 안의 모든 숫자·시간 값을 제로 패딩하고 ISO-8601로 표기하세요. 그래야 사전순 정렬이 여러분이 의도한 순서와 일치합니다.
- 새 질문 = 새 키나 인덱스이지, 절대
Scan이 아닙니다. 나중에 정말로 새로운 액세스 패턴이 나타나면 키를 확장하세요. 필터로 얼버무리지 마세요.
먼저 질문을 모델링하고, 각각이 하나의 Query가 되도록 키를 설계한 뒤, 그것을 증명하세요.
가운데 단계를 먼저 앞서가려면, 무료 싱글 테이블 디자인 도구가 이런 액세스 패턴 목록을 예시 아이템과 비용 힌트를 곁들인 PK/SK/GSI 계획으로 바꿔 줍니다.
DynoTable을 사용해 테이블을 탐색하고, 이 쿼리들을 베이스 테이블과 GSI에 나란히 실행하며, 여러분이 설계한 액세스 패턴이 계획한 것을 정확히 반환하는지 지켜보세요. 그리고 모델링하지 않았던 질문에는, SQL Workbench가 실제 JOIN, GROUP BY, 집계를 클라이언트 측에서 실행합니다.


