DynamoDB의 일대다 관계
SaaS 컨트롤 플레인에는 거의 항상 포함 계층이 있습니다. 하나의 워크스페이스가
여러 프로젝트를 소유하죠. SQL이라면 프로젝트 테이블에 workspace_id 외래
키를 두고 JOIN할 것입니다.
DynamoDB에는 조인도 외래 키도 없으므로, 관계는 키 스키마 자체 안에 담겨야
합니다. 제대로 하면 "워크스페이스와 그 안의 모든 프로젝트를 로드하기"가 읽기 하나
뒤에 후속 스캔이 따르는 대신 단일 Query 하나가 됩니다.
DynamoDB에서 일대다 관계는 어떻게 모델링하나요?
부모와 그 모든 자식에게 동일한 를 부여해 하나의 을 공유하게 하고, 정렬 키로 구분하세요. DynamoDB에는 조인도 외래 키도 없으므로 관계는 키 스키마 자체 안에 담깁니다. 그러면 부모 하나와 모든 자식을 로드하는 일이 조인 대신 단일 Query 하나가 됩니다.
- 엔티티가 아니라 읽기를 모델링하세요. 일대다 관계는 오직 "한 워크스페이스의 프로젝트를 나열한다"를 위해 존재합니다 — 그 쿼리에 맞춰 키를 설계하세요.
- 부모를 자식의 에 인코딩하세요. 워크스페이스와 그 모든 프로젝트에 동일한 파티션 키 값을 부여해 하나의 에 모이게 하세요.
- 그러면 목록 읽기는
Query하나입니다. 부모와 그 자식이 함께 돌아옵니다 — 조인도 없고 두 번째 왕복도 없습니다(Query는 페이지당 최대 1 MB를 반환하고, 그 너머는LastEvaluatedKey로 페이지네이션합니다). - 을 조심하세요. 하나의 거대한 테넌트는 모든 트래픽을 한 파티션에 집중시킵니다. 아주 큰 워크스페이스라면 샤딩된 키와 팬아웃 읽기가 필요할 수 있습니다.
먼저 액세스 패턴
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는 워크스페이스 레코드이고, 각 프로젝트는
PROJ# 접두사와 0으로 채운 시간순 id를 지녀 프로젝트가 자연스럽게 정렬됩니다.
시각적으로, 부모와 그 자식은 정렬 키 순서로 하나의 파티션 안에 쌓입니다.
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에서 돌아옵니다. 조인도
없고 두 번째 왕복도 없죠 — DynamoDB는 페이지당 최대 1 MB를 반환하고 나머지를
가져올 LastEvaluatedKey를 건네줍니다. 그것이 외래 키 속성과 Scan으로는 할 수
없는 동작입니다.
그 begins_with 조건을 손으로 쓰는 일은 성가십니다 — 키 조건과 프로젝션 표현식
문법이 물어뜯거든요.
DynamoDB Expression Builder는
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)와 팬아웃 읽기가 필요할 수 있습니다. - 항목 컬렉션 크기. 로컬 보조 인덱스가 있으면 단일 파티션의 항목 컬렉션은 10 GB로 제한됩니다. LSI가 없으면 그런 제한은 없습니다. 여기서 인덱스 유형을 저울질하고 있다면 GSI 대 LSI를 참고하세요.
Scan이 아니라Query에 손을 뻗으세요. 이 설계 전체는 한 파티션을Query할 수 있도록 존재합니다. "한 워크스페이스의 프로젝트를 찾으려고" 필터가 달린Scan으로 물러서는 것은 모델을 내던지고 테이블 전체를 읽는 일입니다 — Query 대 Scan에서 다루는 함정이죠.
만약 정말로 워크스페이스를 가로질러 프로젝트를 나열해야 한다면(예: 전 세계의 모든
status = ACTIVE 프로젝트), 베이스 테이블은 그에 답할 수 없습니다 — 파티션 키가
워크스페이스 범위이기 때문이죠.
그건 이 관계를 다시 빚는 일이 아니라, 프로젝트를 다른 속성으로 다시 파티셔닝하는 보조 인덱스가 할 일입니다.
다음 단계
액세스 패턴을 모델링하고, 부모를 자식의 파티션 키에 인코딩하면, 일대다 읽기는 단일
Query가 됩니다. DynamoDB Expression Builder로
키 조건을 만들고 검증하세요 — 그리고 액세스 패턴 자체에서 시작하고 싶다면, 무료
싱글 테이블 디자인 도구가 예시 항목이 포함된
PK/SK/GSI 계획을 초안으로 잡아 줍니다.
그런 다음 DynoTable을 다운로드해 이 스키마를 로드하고,
워크스페이스→프로젝트 항목 컬렉션을 실시간으로 살펴보며, 각 쿼리가 정확히 읽기 한
번만 하는지 확인하세요. 워크스페이스와 프로젝트를 조인된 관계형 뷰로 보고 싶다면,
DynoTable의 SQL 워크벤치가 그 JOIN도 실행해 줍니다.


