중급6분 분량

DynamoDB의 다대다 관계

한 학생은 여러 강좌에 등록하고, 한 강좌는 여러 학생을 담습니다. SQL이라면 조인 테이블과 4-way JOIN에 손을 뻗었을 겁니다.

DynamoDB에는 조인이 없으므로 관계는 에 담겨야 합니다 — 그리고 비결은 각 등록 엣지를, 양쪽이 직접 Query할 수 있는 형태로 저장하는 것입니다.

이 가이드는 학생 ↔ 강좌 문제를 처음부터 끝까지 따라갑니다: 액세스 패턴, 그것을 푸는 인접 리스트 패턴, 그대로 복사할 수 있는 독자적인 키 스키마, 그리고 테이블을 스캔하지 않고 양방향을 다시 읽는 방법.

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

DynamoDB에는 조인이 없으므로, 한쪽으로 키잉된 자체 엣지 아이템으로 각 연결을 저장하고 키를 맞바꾸는 역전된 GSI를 추가하는 인접 리스트 패턴으로 다대다 관계를 모델링합니다. 한 번 쓴 단일 엣지가 양방향 쿼리를 저렴하게 처리합니다.

  • 각 등록을 자체 엣지 아이템으로 저장하세요, 어느 한쪽의 리스트 속성이 아니라.
  • 엣지를 학생으로 키잉하세요(PK = STU#…, SK = ENROLL#CRS#…). 그러면 한 번의 Query가 학생의 전체 강좌 목록을 돌려줍니다.
  • 역전된 GSI를 추가하세요. 역할을 맞바꾸면(GSI1PK = CRS#…) 같은 엣지가 "이 강좌에 누가 있나?"에도 답합니다.
  • 한 번 쓴 엣지 하나가 양방향으로 저렴하게 읽힙니다 — 그게 게임의 전부입니다.

먼저 액세스 패턴을 정하세요

DynamoDB 모델링은 액세스 패턴 우선입니다. 단 하나의 속성 이름을 고르기 전에 읽기를 먼저 정합니다. 다대다 관계에는 거의 항상 엔티티 조회에 더해 두 개의 대칭적인 읽기가 있습니다:

  • 학생의 프로필을 가져오고, 그 학생이 등록한 모든 강좌를 나열하기.
  • 강좌의 메타데이터를 가져오고, 그 강좌에 등록한 모든 학생을 나열하기.
  • 단일 등록 엣지 조회하기 — 성적을 갱신하거나 수강을 취소하기 위해.

까다로운 점: 두 목록 읽기가 같은 엣지 집합을 정반대 방향으로 가리킵니다. 순진한 설계는 한쪽을 저렴하게 처리하고 다른 쪽엔 Scan을 강요합니다 — Query vs Scan에서 다룬 바로 그 함정이죠.

할 일은 양쪽 방향을 단일 Query로 만드는 것입니다.

인접 리스트 패턴을 쓰세요

관계에 대한 DynamoDB 자체의 지침은 인접 리스트(adjacency list) 입니다. 각 관계를, 파티션 키가 한쪽 끝점이고 정렬 키가 다른 쪽 끝점인 아이템으로 모델링합니다.

AWS는 이를 DynamoDB 개발자 안내서의 다대다 관계 관리 모범 사례 페이지에서 문서화합니다.

왜 두 번째 테이블이 아니라 키인가요? DynamoDB가 주는 원시 도구가 단일 파티션을 향한 Query이기 때문입니다.

Query는 한 파티션 키 아래 정렬 키 값들의 연속 범위를 단일 과금 작업으로 읽습니다 — 그게 엔진이 제공하는 유일한 "조인"입니다.

양쪽 에서 저렴하게 읽히는 관계를 얻으려면 엣지를 복제합니다. 학생으로 키잉해 한 번 쓴 뒤, 보조 인덱스로 같은 엣지를 강좌로 키잉해 프로젝션하는 거죠.

이것은 싱글 테이블 디자인의 오버로드 키 사고방식을 부모-자식 계층 대신 관계에 적용한 것입니다.

형태는 같은 엣지의 두 겹 뷰입니다 — 학생으로 키잉된 베이스 테이블, 강좌로 키잉된 역전 GSI:

역전 GSI1 강좌로 키잉베이스 테이블 학생으로 키잉같은 엣지, 맞바꿈같은 엣지, 맞바꿈PK STU#a91SK ENROLL#CRS#math204PK STU#a91SK ENROLL#CRS#cs101GSI1PK CRS#math204GSI1SK STU#a91GSI1PK CRS#cs101GSI1SK STU#a91

각 엣지는 베이스 테이블에 한 번 쓰이고, 키를 맞바꾼 채 GSI에 프로젝션되므로, 어느 파티션에 Query해도 관계가 저렴하게 읽힙니다.

그 계보는 2007년 Amazon Dynamo 논문으로 거슬러 올라갑니다. 파티션 키가 분산의 단위이고, 단일 키 접근이 빠른 경로입니다.

DynamoDB에서 관계란 다대다 읽기를 그 빠른 경로에 끼워 맞추는 연습입니다.

예제 풀어보기: 학생 ↔ 강좌

범용 키 PKSK를 가진 하나의 테이블을 쓰고, 엔티티 유형은 값에 인코딩합니다. 등록 엣지가 핵심입니다:

PKSKattributes
STU#a91PROFILEname, year, major
STU#a91ENROLL#CRS#math204 enrolledOn, grade
STU#a91ENROLL#CRS#cs101enrolledOn, grade
CRS#math204METADATAtitle, credits, term
CRS#cs101METADATAtitle, credits, term

단일 Query PK = "STU#a91"은 학생의 프로필 모든 등록을 한 번의 읽기로 돌려줍니다. SK begins_with "ENROLL#"로 좁히면 강좌 엣지만 얻습니다. 이걸로 "학생의 강좌 나열하기"가 해결됩니다.

하지만 "강좌의 학생 나열하기"는 반대 방향을 가리키고 — 학생 id가 파티션 키에 있고 정렬 키에 없으므로 베이스 테이블은 답할 수 없습니다.

역할을 맞바꾸는 역전 글로벌 보조 인덱스를 추가하세요. 엣지 아이템에 범용 GSI1PK/GSI1SK 쌍을 주어, 강좌를 파티션 쪽에, 학생을 정렬 쪽에 담습니다:

PKSKGSI1PKGSI1SK
STU#a91ENROLL#CRS#math204CRS#math204STU#a91
STU#b30ENROLL#CRS#math204CRS#math204STU#b30
STU#a91ENROLL#CRS#cs101CRS#cs101STU#a91

이제 Query GSI1 WHERE GSI1PK = "CRS#math204"가 그 강좌의 모든 학생을 나열합니다 — 베이스 테이블이 제공할 수 없던 읽기죠. 한 번 쓴 하나의 엣지 아이템이 양방향에 답합니다.

LSI가 아니라 GSI여야 합니다. 강좌 파티션은 학생 파티션과 완전히 다른데, LSI는 베이스 테이블의 파티션 키를 공유하기 때문입니다.

이 인덱스는 여러 파티션에 걸치므로 글로벌이어야 합니다 — GSI vs LSI를 보세요.

한 가지 주의: DynamoDB의 GSI는 비동기로 채워집니다. 갓 생긴 등록은 CRS#… 방향에 나타나기까지 잠시 걸릴 수 있습니다.

강좌 명단 읽기는 최종적 일관성으로 취급하세요 — 개발자 안내서가 글로벌 보조 인덱스에 대해 명시적으로 짚는 점입니다.

DynoTable에서 쓰고 읽기

등록을 쓴다는 건 네 개의 키 속성과 엣지 자체의 데이터를 설정하는 일입니다. 한 학생이 같은 강좌에 두 번 등록하는 걸 막는 조건은 복합 키에 대한 attribute_not_exists(PK) 가드입니다.

그게 바로 ExpressionAttributeNames와 플레이스홀더 값을 손으로 쓰는 대신 DynamoDB 표현식 빌더로 시각적으로 조립할 수 있는 종류의 조건입니다.

DynoTable에서 QueryGSI1에 겨누고 GSI1PK = "CRS#math204"를 설정하면, 명단이 읽고 정렬하고 제자리에서 편집할 수 있는 테이블로 돌아옵니다 — 관계의 양방향을 한 스키마에서 둘러볼 수 있습니다.

DynoTable에서 역전 GSI를 쿼리해 강좌에 등록한 모든 학생을 나열하는 모습.
DynoTable에서 역전 GSI를 쿼리해 강좌에 등록한 모든 학생을 나열하는 모습.

함정과 다음 단계

  • 한쪽을 리스트 속성으로 저장하지 마세요. 학생 아이템의 courseIds 배열은 강좌가 명단을 필요로 하거나, 배열이 400KB 아이템 한계에 부딪히거나, 두 등록이 경쟁해 서로를 덮어쓸 때까지는 깔끔해 보입니다. 개별 엣지 아이템은 독립적으로 확장되고 갱신됩니다.
  • 엣지 데이터는 엣지에 두세요. 등록의 gradeenrolledOn은 학생이나 강좌에 중복되는 게 아니라 엣지 아이템에 속합니다 — (학생, 강좌) 쌍마다 갱신할 행이 정확히 하나뿐입니다.
  • GSI 전파를 유념하세요. 역전 인덱스 방향은 최종적 일관성이라, 등록 직후의 읽기는 1초의 일부만큼 늦을 수 있습니다.
  • 명단에 필요한 것만 프로젝션하세요. 명단 뷰가 id만 필요하다면 KEYS_ONLY나 좁은 프로젝션이 GSI를 작게 유지합니다.

주변 패턴을 더 깊이 보려면, 오버로드 키는 싱글 테이블 디자인을, 역전 인덱스가 글로벌이어야 하는 경우는 GSI vs LSI를 읽으세요.

그런 다음 DynoTable을 다운로드해 학생 ↔ 강좌 스키마를 실제로 모델링하세요 — 엣지를 쓰고, 표현식 빌더로 조건을 만들고, 단 한 번의 스캔도 없이 관계의 양방향을 쿼리하세요.

업데이트됨