DynamoDB의 일대다 관계
SaaS 컨트롤 플레인에는 거의 항상 포함 계층이 있습니다. 하나의 워크스페이스가
여러 프로젝트를 소유하죠. SQL이라면 프로젝트 테이블에 workspace_id 외래
키를 두고 JOIN했을 겁니다.
DynamoDB에는 조인도 외래 키도 없으므로, 관계는 키 스키마 자체에 담겨야 합니다.
제대로만 하면 "워크스페이스 하나와 그 안의 모든 프로젝트 불러오기"가 한 번 읽고
이어서 스캔하는 대신 단일 Query가 됩니다.
DynamoDB에서 일대다 관계를 어떻게 모델링하나요?
부모와 모든 자식에 동일한 파티션 키를 부여해 하나의 아이템 컬렉션을 공유하게 한 뒤, 정렬 키로 구분하세요. DynamoDB에는 조인도 외래 키도 없으므로 관계는 키 스키마 자체에 담겨야 합니다. 이렇게 하면 부모와 모든 자식을 불러오는 작업이 조인 대신 단일 Query가 됩니다.
- 엔티티가 아니라 읽기를 모델링하세요. 일대다 관계는 오직 "워크스페이스의 프로젝트 나열하기"를 위해 존재합니다 — 그 쿼리에 맞춰 키를 설계하세요.
- 부모를 자식의 파티션 키에 인코딩하세요. 워크스페이스와 모든 프로젝트에 같은 파티션 키 값을 부여해 하나의 아이템 컬렉션으로 모이게 합니다.
- 그러면 목록 읽기가 단일
Query가 됩니다. 부모와 임의 개수의 자식이 한 번 과금되는 호출로 돌아옵니다 — 조인도, 두 번째 왕복도 없습니다. - 핫 파티션을 주의하세요. 거대한 테넌트 하나가 모든 트래픽을 한 파티션에 집중시킵니다. 아주 큰 워크스페이스는 샤딩된 키와 팬아웃 읽기가 필요할 수 있습니다.
먼저 액세스 패턴
DynamoDB 모델링은 엔티티 우선이 아니라 액세스 패턴 우선입니다 — 싱글 테이블 디자인을 떠받치는 같은 규율이죠. 어떤 키를 고르기 전에, 앱이 실제로 실행하는 읽기를 먼저 적으세요:
- 워크스페이스 하나의 설정 가져오기.
- 워크스페이스의 모든 프로젝트를 최신순으로 나열하기.
- id로 특정 프로젝트 하나 가져오기.
"워크스페이스 하나, 프로젝트 다수" 관계가 중요한 이유는 오직 읽기 #2 때문입니다. 워크스페이스의 프로젝트를 함께 나열할 일이 전혀 없다면, 관계 자체를 모델링하지 않고 프로젝트를 독립적으로 저장했을 겁니다.
그러니 질문은 결코 추상적인 "일대다를 어떻게 표현하지?"가 아닙니다. "이 관계가 어떤 쿼리들을 충족해야 하는가?"입니다. 그 답을 정한 뒤 거기에 맞춰 키를 설계하세요.
외래 키가 여기서 도움이 안 되는 이유
DynamoDB에서 모든 GetItem과 Query는 파티션 키를 대상으로 하며, 서비스는
그 키를 해시해 아이템이 있는 파티션을 찾습니다.
AWS는 핵심 구성 요소 문서에서 직접 말합니다. 파티션 키 값은 데이터가 어디에 위치할지 결정하는 내부 해시 함수의 입력이라고요.
그 해시 기반 배치는 원래 2007년의 Dynamo: Amazon's Highly Available Key-value Store 논문에서 물려받은 것으로, 거기서는 일관 해싱이 키를 노드 전체에 분산합니다.
프로젝트 아이템에 그냥 붙은 workspace_id 속성 은 그 메커니즘에 보이지
않습니다 — DynamoDB는 그것을 "따라갈" 수 없습니다.
관련 아이템을 한 요청으로 가져오려면, 부모의 정체성이 프로젝트의 파티션 키에
인코딩되어야 합니다. 그래야 워크스페이스의 모든 아이템이 같은 파티션으로 해시되고
하나의 Query로 한꺼번에 쓸어 담을 수 있습니다.
실전 예제: 워크스페이스와 프로젝트
범용적인 오버로드 키 스키마를 쓰세요. 파티션 키를 EntityRef, 정렬 키를
Detail이라 부릅니다. 워크스페이스의 정체성은 워크스페이스 아이템과 그 아래 모든
프로젝트 양쪽 모두의 EntityRef로 들어갑니다:
| EntityRef | Detail | attributes |
|---|---|---|
| WS#acme | META | displayName, region, seatLimit |
| WS#acme | PROJ#2026-0007 | title, status, createdBy |
| WS#acme | PROJ#2026-0042 | title, status, createdBy |
| WS#acme | PROJ#2026-0118 | title, status, createdBy |
| WS#globex | META | displayName, region, seatLimit |
| WS#globex | PROJ#2026-0009 | title, status, createdBy |
워크스페이스와 모든 프로젝트가 EntityRef = "WS#acme"를 공유하므로, 한 파티션
위에 함께 사는 하나의 아이템 컬렉션을 이룹니다.
Detail 정렬 키가 이들을 구분합니다. META는 워크스페이스 레코드이고, 각
프로젝트는 자연스럽게 정렬되도록 0으로 패딩된 시간 순서 id에 PROJ# 접두사를
답니다.
시각적으로, 부모와 자식들은 정렬 키 순서로 한 파티션 안에 쌓입니다:
EntityRef = "WS#acme"에 대한 하나의 Query가 스택 전체를 — 부모와 모든 자식을 —
단일 읽기로 쓸어 담습니다.
이제 세 가지 액세스 패턴이 각각 한 번의 호출로 축약됩니다:
- 워크스페이스 설정 —
GetItem(EntityRef="WS#acme", Detail="META"). - 프로젝트 최신순 나열 —
Detail begins_with "PROJ#"와 함께Query(EntityRef="WS#acme")를 내림차순으로 실행 (ScanIndexForward = false). - 프로젝트 하나 —
GetItem(EntityRef="WS#acme", Detail="PROJ#2026-0042").
두 번째가 핵심입니다. 부모와 임의 개수의 자식이 하나의 과금되는 Query로
돌아옵니다 — 조인도, 두 번째 왕복도 없습니다. 외래 키 속성과 Scan으로는 할 수
없는 수입니다.
그 begins_with 조건을 손으로 작성하는 건 까다롭습니다 — 키 조건과 프로젝션
표현식 문법이 발목을 잡거든요.
DynamoDB 표현식 빌더는
KeyConditionExpression, #name/:value 플레이스홀더 맵, 그리고 바로 실행
가능한 SDK 스니펫을 생성해 주므로 문법과 씨름할 필요가 없습니다:
KeyConditionExpression "#er = :er AND begins_with(#d, :p)"
ExpressionAttributeNames { "#er": "EntityRef", "#d": "Detail" }
ExpressionAttributeValues { ":er": "WS#acme", ":p": "PROJ#" }
DynoTable에서 아이템 컬렉션 살펴보기
이 레이아웃의 보상은 시각적입니다. EntityRef를 공유하는 모든 행이 워크스페이스와
그 자식들로, 서로 나란히 자리합니다.
DynoTable은 이들을 묶어 보여 주므로, 일대다 관계를 별도 테이블 사이에서 추측하는 대신 하나의 연속된 블록으로 보게 됩니다.

함정과 대안적 형태
주의할 점 몇 가지:
- 핫 파티션. 워크스페이스 하나의 모든 아이템이 한 파티션에 살므로, 아주 크거나
바쁜 테넌트 하나가 트래픽을 집중시킵니다. AWS가 설명하는
적응형 용량
동작이 어느 정도의 편향은 흡수하지만, 수백만 개의 프로젝트를 가진 워크스페이스는
샤딩된 키(예:
WS#acme#01 … #10)와 팬아웃 읽기가 필요할 수 있습니다. - 아이템 컬렉션 크기. 로컬 보조 인덱스가 있으면 단일 파티션의 아이템 컬렉션은 10GB로 제한됩니다. LSI가 없으면 그런 한계는 없습니다. 여기서 인덱스 유형을 저울질하고 있다면 GSI vs LSI를 보세요.
Scan이 아니라Query를 쓰세요. 이 설계 전체가 한 파티션을Query할 수 있게 하려고 존재합니다. "워크스페이스의 프로젝트 찾기"를 위해 필터링된Scan으로 물러나는 건 모델을 내버리고 테이블 전체를 읽는 것입니다 — Query vs Scan에서 다룬 함정이죠.
워크스페이스들을 가로질러 프로젝트를 나열해야 한다면(예: 전역적으로 모든
status = ACTIVE 프로젝트), 베이스 테이블은 답할 수 없습니다 — 파티션 키가
워크스페이스 범위로 잡혀 있으니까요.
그건 프로젝트를 다른 속성으로 다시 파티셔닝하는 보조 인덱스의 일이지, 이 관계를 재설계할 일이 아닙니다.
다음 단계
액세스 패턴을 모델링하고, 부모를 자식의 파티션 키에 인코딩하면, 일대다 읽기는 단일
Query가 됩니다. DynamoDB 표현식 빌더로 키
조건을 만들고 검증하세요.
그런 다음 DynoTable을 다운로드해 이 스키마를 불러오고, 워크스페이스→프로젝트 아이템 컬렉션을 라이브로 둘러보며, 각 쿼리가 정확히 한 번씩만 읽는지 확인하세요.


