고급5분 분량

DynamoDB의 키 오버로딩

SQL에서 넘어왔다면 컬럼은 영원히 한 가지를 의미합니다: orders.created_at은 항상 날짜이고, users.email은 항상 이메일입니다. 키 오버로딩은 그것을 내던집니다. 파티션 키와 정렬 키에 일반적인 이름 — pk, sk — 을 주고, 각 아이템 유형이 거기에 다른 의미를 부어 넣게 합니다. 하나의 테이블, 여러 엔티티, 하나의 형태.

DynamoDB에서 키 오버로딩이란 무엇인가요?

키 오버로딩은 pk/sk처럼 중립적인 키 이름 아래 여러 엔티티 유형을 하나의 테이블에 저장하고, 유형 정보를 값에 인코딩하는 패턴입니다 (USER#u_3001, INVOICE#2026-0014). 속성 이름은 중립으로 유지되므로 사용자, 인보이스, 이벤트가 동일한 파티션을 공유하고, 값이 유형을 실어 나르며, 정렬 키 접두사를 이용해 begins_with 조건 하나로 각 엔티티를 슬라이스할 수 있습니다.

  • 일반적인 키 이름, 유형이 담긴 값. 키를 pk/sk로 이름 짓고 엔티티 유형을 값에 넣으세요: pk = "TENANT#acme", sk = "USER#u_3001". 이름은 멍청하고, 값이 유형을 실어 나릅니다.
  • 그것이 단일 테이블 설계를 작동하게 만듭니다. 오버로딩이 없으면 공유 테이블은 그저 잡동사니 서랍입니다. 오버로딩이 있으면 모든 엔티티가 Query할 수 있는 파티션에 앉습니다.
  • begins_with가 보상입니다. 정렬 키의 유형 접두사는 하나의 Query로 엔티티 전체나 그 한 조각을 — Scan도 필터도 없이 — 끌어내게 합니다.
  • 비용은 가독성입니다. 가공되지 않은 pk/sk 덤프는 아무것도 알려주지 않습니다. 접두사를 해독하는 뷰어가 필요합니다. 그러지 않으면 문자열을 노려보게 됩니다.

일반적인 이름이 진짜 이름을 이기는 이유

DynamoDB는 테이블당 정확히 두 개의 키 속성을 가지며, Query는 단일 파티션 키만 겨냥할 수 있습니다. 그래서 키 이름을 userId로 지으면 사용자 아이템만 그 테이블에 깔끔하게 살 수 있습니다 — 다른 모든 것은 userId를 흉내 내거나 자체 테이블로 옮겨야 합니다.

오버로딩은 그것을 비켜갑니다. pk 같은 중립적인 이름은 어떤 엔티티에도 헌신하지 않으므로, 사용자, 인보이스, 감사 이벤트가 모두 같은 키 속성과 같은 테이블을 공유할 수 있습니다. 속성 이름이 아니라 이 아이템이 무엇인지 말합니다.

이것이 단일 테이블 설계를 이론에서 실제로 쿼리할 수 있는 무언가로 바꾸는 한 수입니다. 공유 테이블은 컨테이너이고, 오버로딩은 구별되는 엔티티가 그 안에서 공존하게 하는 것입니다.

멀티테넌트 예제

SaaS 청구 제품을 운영한다고 합시다. 각 테넌트에는 멤버, 인보이스, 감사 추적이 있습니다. 세 개의 테이블 대신, 모든 것을 하나에 넣고 키를 오버로딩하세요:

pkskattributes
TENANT#acmeMETAname="Acme Inc", plan="team"
TENANT#acmeUSER#u_3001email, role="admin"
TENANT#acmeUSER#u_3002email, role="member"
TENANT#acmeINVOICE#2026-0014amount_cents, status="paid"
TENANT#acmeINVOICE#2026-0015amount_cents, status="open"
TENANT#acmeEVENT#2026-06-23T09:12Zactor="u_3001", action="invite"

모든 행이 pk = "TENANT#acme"를 공유하므로 하나의 아이템 컬렉션을 형성합니다 — 모두 같은 위치에 있고, 모두 단일 파티션 읽기로 도달할 수 있습니다.

파티션: TENANT#acmesk: METAsk: USER#u_3001sk: INVOICE#2026-0015sk: EVENT#2026-06-23T09:12Z하나의 Query

정렬 키 접두사가 진짜 일을 합니다. 엔티티를 그룹화하고 또한 정렬합니다.

오버로딩된 컬렉션 쿼리하기

유형이 정렬 키 접두사에 살기 때문에, begins_with는 아무것도 스캔하지 않고 파티션을 엔티티별로 잘라냅니다:

Query pk = "TENANT#acme"  -- 테넌트 전체, 모든 유형
Query pk = "TENANT#acme" AND begins_with(sk, "USER#")  -- 멤버만
Query pk = "TENANT#acme" AND begins_with(sk, "INVOICE#")  -- 인보이스만

파티션 전체가 아니라 조건이 일치하는 아이템에 대해서만 비용을 냅니다 — 읽은 행을 그냥 버리면서도 비용을 내는 필터 걸린 Scan의 정반대입니다. AWS는 이를 키 조건이라 부릅니다. 어떤 데이터든 파티션을 떠나기 전에 키에서 실행됩니다.

begins_with 조건을 직접 손으로 만든다면 유형 태그를 정확히 맞추세요 — USER# 대신 USERS#를 잘못 쓰면 아무것도 반환하지 않습니다, 조용히. expression builder는 접두사가 당신이 실제로 쓴 것과 일치하도록 KeyConditionExpressionExpressionAttributeValues 맵을 생성합니다.

인덱스도 오버로딩하기

같은 트릭이 GSI에도 적용됩니다. 일반적인 키 이름 — gsi1pk, gsi1sk — 을 주고, 각 엔티티가 필요한 것을 쓰게 하세요. 그러면 하나의 인덱스가 베이스 테이블이 할 수 없는 패턴에 답합니다.

pkskgsi1pkgsi1sk
TENANT#acmeINVOICE#2026-0015STATUS#open2026-06-30
TENANT#acmeINVOICE#2026-0014STATUS#paid2026-06-12
TENANT#betaINVOICE#2026-0099STATUS#open2026-06-25

이제 Query gsi1 WHERE gsi1pk = "STATUS#open"모든 테넌트에 걸쳐 열린 모든 인보이스를 마감일 순서로 나열합니다 — 베이스 테이블의 테넌트 범위 키로는 절대 처리할 수 없는 파티션 간 뷰입니다. 다른 엔티티가 자기 의미로 gsi1을 재사용할 수 있으므로 (예: gsi1pk = "ROLE#admin"), 하나의 인덱스가 여러 읽기를 커버합니다. 다만 GSI는 최종적 일관성을 가진다는 점을 기억하세요 — 그 쓰기는 베이스 테이블보다 뒤처집니다.

DynoTable에서 해보기

가공되지 않은 오버로딩된 키는 읽기에 적대적입니다: INVOICE#2026-0015EVENT#2026-06-23T09:12Z는 평평한 목록에서 뒤섞입니다. 파티션별로 그룹화하고 접두사를 드러내는 뷰어는 잡동사니 서랍을 다시 엔티티로 되돌립니다.

DynoTable이 한 테넌트의 아이템 컬렉션을 둘러보는 모습 — 단일 오버로딩된 파티션 키 아래 그룹화된 META, USER, INVOICE, EVENT 아이템.
DynoTable이 한 테넌트의 아이템 컬렉션을 둘러보는 모습 — 단일 오버로딩된 파티션 키 아래 그룹화된 META, USER, INVOICE, EVENT 아이템.

함정

  • 구분자는 한 번 고르고 절대 바꾸지 마세요. #이 관례입니다. 엔티티 전반에 #:을 섞으면 아무것도 경고하지 않는 방식으로 begins_with가 깨집니다.
  • 범위 연산이 필요한 값을 오버로딩하지 마세요. INVOICE#2026-0015라는 정렬 키는 숫자가 아니라 사전식으로 정렬됩니다 — ID는 0으로 채우고 날짜는 ISO-8601을 써서 문자열 순서가 의도한 순서와 일치하게 하세요.
  • 접두사 네임스페이스를 예약하세요. 둘 다 USER로 시작하는 두 엔티티 유형(예: USER#USERGROUP#)은 begins_with(sk, "USER") 아래에서 충돌합니다. 접두사를 첫 글자부터 모호하지 않게 만드세요.
  • 키보다 읽기를 먼저 계획하세요. 오버로딩은 당신이 열거한 액세스 패턴을 처리합니다. 읽기를 아직 모른다면, 먼저 단일 테이블 설계를 참고하세요 — 키는 쿼리의 하류에 있습니다.

파티션을 그려본 다음, DynoTable을 다운로드하여 자신의 오버로딩된 키를 둘러보고 하나의 Query가 테넌트 전체를 한 번에 끌어오는 모습을 지켜보세요.

업데이트됨