중급6분 분량

DynamoDB의 일대다 관계

SaaS 컨트롤 플레인에는 거의 항상 포함 계층이 있습니다. 하나의 워크스페이스가 여러 프로젝트를 소유하죠. SQL이라면 프로젝트 테이블에 workspace_id 외래 키를 두고 JOIN했을 겁니다.

DynamoDB에는 조인도 외래 키도 없으므로, 관계는 키 스키마 자체에 담겨야 합니다. 제대로만 하면 "워크스페이스 하나와 그 안의 모든 프로젝트 불러오기"가 한 번 읽고 이어서 스캔하는 대신 단일 Query가 됩니다.

DynamoDB에서 일대다 관계를 어떻게 모델링하나요?

부모와 모든 자식에 동일한 파티션 키를 부여해 하나의 아이템 컬렉션을 공유하게 한 뒤, 정렬 키로 구분하세요. DynamoDB에는 조인도 외래 키도 없으므로 관계는 키 스키마 자체에 담겨야 합니다. 이렇게 하면 부모와 모든 자식을 불러오는 작업이 조인 대신 단일 Query가 됩니다.

  • 엔티티가 아니라 읽기를 모델링하세요. 일대다 관계는 오직 "워크스페이스의 프로젝트 나열하기"를 위해 존재합니다 — 그 쿼리에 맞춰 키를 설계하세요.
  • 부모를 자식의 파티션 키에 인코딩하세요. 워크스페이스와 모든 프로젝트에 같은 파티션 키 값을 부여해 하나의 아이템 컬렉션으로 모이게 합니다.
  • 그러면 목록 읽기가 단일 Query가 됩니다. 부모와 임의 개수의 자식이 한 번 과금되는 호출로 돌아옵니다 — 조인도, 두 번째 왕복도 없습니다.
  • 핫 파티션을 주의하세요. 거대한 테넌트 하나가 모든 트래픽을 한 파티션에 집중시킵니다. 아주 큰 워크스페이스는 샤딩된 키와 팬아웃 읽기가 필요할 수 있습니다.

먼저 액세스 패턴

DynamoDB 모델링은 엔티티 우선이 아니라 액세스 패턴 우선입니다 — 싱글 테이블 디자인을 떠받치는 같은 규율이죠. 어떤 키를 고르기 전에, 앱이 실제로 실행하는 읽기를 먼저 적으세요:

  • 워크스페이스 하나의 설정 가져오기.
  • 워크스페이스의 모든 프로젝트를 최신순으로 나열하기.
  • id로 특정 프로젝트 하나 가져오기.

"워크스페이스 하나, 프로젝트 다수" 관계가 중요한 이유는 오직 읽기 #2 때문입니다. 워크스페이스의 프로젝트를 함께 나열할 일이 전혀 없다면, 관계 자체를 모델링하지 않고 프로젝트를 독립적으로 저장했을 겁니다.

그러니 질문은 결코 추상적인 "일대다를 어떻게 표현하지?"가 아닙니다. "이 관계가 어떤 쿼리들을 충족해야 하는가?"입니다. 그 답을 정한 뒤 거기에 맞춰 키를 설계하세요.

외래 키가 여기서 도움이 안 되는 이유

DynamoDB에서 모든 GetItemQuery파티션 키를 대상으로 하며, 서비스는 그 키를 해시해 아이템이 있는 파티션을 찾습니다.

AWS는 핵심 구성 요소 문서에서 직접 말합니다. 파티션 키 값은 데이터가 어디에 위치할지 결정하는 내부 해시 함수의 입력이라고요.

그 해시 기반 배치는 원래 2007년의 Dynamo: Amazon's Highly Available Key-value Store 논문에서 물려받은 것으로, 거기서는 일관 해싱이 키를 노드 전체에 분산합니다.

프로젝트 아이템에 그냥 붙은 workspace_id 속성 은 그 메커니즘에 보이지 않습니다 — DynamoDB는 그것을 "따라갈" 수 없습니다.

관련 아이템을 한 요청으로 가져오려면, 부모의 정체성이 프로젝트의 파티션 키에 인코딩되어야 합니다. 그래야 워크스페이스의 모든 아이템이 같은 파티션으로 해시되고 하나의 Query로 한꺼번에 쓸어 담을 수 있습니다.

실전 예제: 워크스페이스와 프로젝트

범용적인 오버로드 키 스키마를 쓰세요. 파티션 키를 EntityRef, 정렬 키를 Detail이라 부릅니다. 워크스페이스의 정체성은 워크스페이스 아이템과 그 아래 모든 프로젝트 양쪽 모두EntityRef로 들어갑니다:

EntityRefDetailattributes
WS#acmeMETAdisplayName, region, seatLimit
WS#acmePROJ#2026-0007title, status, createdBy
WS#acmePROJ#2026-0042title, status, createdBy
WS#acmePROJ#2026-0118title, status, createdBy
WS#globexMETAdisplayName, region, seatLimit
WS#globexPROJ#2026-0009title, status, createdBy

워크스페이스와 모든 프로젝트가 EntityRef = "WS#acme"를 공유하므로, 한 파티션 위에 함께 사는 하나의 아이템 컬렉션을 이룹니다.

Detail 정렬 키가 이들을 구분합니다. META는 워크스페이스 레코드이고, 각 프로젝트는 자연스럽게 정렬되도록 0으로 패딩된 시간 순서 id에 PROJ# 접두사를 답니다.

시각적으로, 부모와 자식들은 정렬 키 순서로 한 파티션 안에 쌓입니다:

파티션: EntityRef = WS#acmeMETA 워크스페이스 설정PROJ#2026-0007PROJ#2026-0042PROJ#2026-0118

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은 이들을 묶어 보여 주므로, 일대다 관계를 별도 테이블 사이에서 추측하는 대신 하나의 연속된 블록으로 보게 됩니다.

DynoTable의 테이블 뷰에서 하나의 아이템 컬렉션으로 묶인 워크스페이스 META 아이템과 그 PROJ# 자식들.
DynoTable의 테이블 뷰에서 하나의 아이템 컬렉션으로 묶인 워크스페이스 META 아이템과 그 PROJ# 자식들.

함정과 대안적 형태

주의할 점 몇 가지:

  • 핫 파티션. 워크스페이스 하나의 모든 아이템이 한 파티션에 살므로, 아주 크거나 바쁜 테넌트 하나가 트래픽을 집중시킵니다. AWS가 설명하는 적응형 용량 동작이 어느 정도의 편향은 흡수하지만, 수백만 개의 프로젝트를 가진 워크스페이스는 샤딩된 키(예: WS#acme#01 … #10)와 팬아웃 읽기가 필요할 수 있습니다.
  • 아이템 컬렉션 크기. 로컬 보조 인덱스가 있으면 단일 파티션의 아이템 컬렉션은 10GB로 제한됩니다. LSI가 없으면 그런 한계는 없습니다. 여기서 인덱스 유형을 저울질하고 있다면 GSI vs LSI를 보세요.
  • Scan이 아니라 Query를 쓰세요. 이 설계 전체가 한 파티션을 Query할 수 있게 하려고 존재합니다. "워크스페이스의 프로젝트 찾기"를 위해 필터링된 Scan으로 물러나는 건 모델을 내버리고 테이블 전체를 읽는 것입니다 — Query vs Scan에서 다룬 함정이죠.

워크스페이스들을 가로질러 프로젝트를 나열해야 한다면(예: 전역적으로 모든 status = ACTIVE 프로젝트), 베이스 테이블은 답할 수 없습니다 — 파티션 키가 워크스페이스 범위로 잡혀 있으니까요.

그건 프로젝트를 다른 속성으로 다시 파티셔닝하는 보조 인덱스의 일이지, 이 관계를 재설계할 일이 아닙니다.

다음 단계

액세스 패턴을 모델링하고, 부모를 자식의 파티션 키에 인코딩하면, 일대다 읽기는 단일 Query가 됩니다. DynamoDB 표현식 빌더로 키 조건을 만들고 검증하세요.

그런 다음 DynoTable을 다운로드해 이 스키마를 불러오고, 워크스페이스→프로젝트 아이템 컬렉션을 라이브로 둘러보며, 각 쿼리가 정확히 한 번씩만 읽는지 확인하세요.

업데이트됨