DynamoDB의 단일 테이블 설계
SQL에서 넘어오면 본능은 엔티티마다 테이블 하나다: customers, orders,
order_items. DynamoDB에서 그 본능은 대개 틀렸다. 모든 엔티티를 오버로딩된 키
접두사로 구분해 저장하는 하나의 테이블은, 부모와 그 자식들을 한 번의 Query로
가져오게 해 준다 — 조인도, N+1도 없다.
DynamoDB에서 단일 테이블 설계란?
단일 테이블 설계는 모든 엔티티 — 고객, 주문, 주문 항목 — 를 하나의 DynamoDB 테이블에
저장하고, 오버로딩된 와 정렬 키 접두사로 구분한다. 키가
엔티티가 아니라 접근 패턴을 중심으로 설계되므로, 부모와 그 모든 자식이 하나의
에 살고 단일 Query로 돌아온다 — 조인도, N+1 읽기도
없다.
아이디어
일반적인 키 이름(PK, SK)을 고르고 엔티티 타입을 값에 인코딩한다:
| PK | SK | attributes |
|---|---|---|
| CUSTOMER#42 | PROFILE | name, email, plan |
| CUSTOMER#42 | ORDER#2026-001 | total, status |
| CUSTOMER#42 | ORDER#2026-002 | total, status |
이제 하나의 Query PK = "CUSTOMER#42"가 프로필 과 모든 주문을 단일 청구 읽기로
반환한다. SK begins_with "ORDER#"는 그것을 주문만으로 좁힌다.
시각적으로, 오버로딩된 항목들은 하나의 아래에 단일 으로 쌓인다:
파티션을 한 번 읽으면 고객과 모든 주문을 함께 돌려준다.
오버로딩된 GSI
같은 요령이 인덱스에도 통한다. 항목에 일반적인 GSI1PK/GSI1SK를 두면, 각 항목이 그
속성에 무엇을 쓰느냐에 따라 하나의 가 여러 접근 패턴을 처리한다:
| PK | SK | GSI1PK | GSI1SK |
|---|---|---|---|
| ORDER#001 | METADATA | STATUS#OPEN | 2026-01-04 |
| ORDER#002 | METADATA | STATUS#OPEN | 2026-01-05 |
이제 Query GSI1 WHERE GSI1PK = "STATUS#OPEN"은 열린 주문을 날짜순으로 나열한다 —
기본 테이블이 답할 수 없는 패턴이다. 다른 엔티티는 자신만의 의미로 GSI1을 재사용할
수 있다(예: CATEGORY#books). 하나의 인덱스, 많은 쿼리.
다대다: 인접 리스트
관계(여러 팀에 속한 사용자, 여러 사용자를 가진 팀)에는, id를 뒤바꿔 엣지를 두 번
쓴다: PK=USER#1, SK=TEAM#9와 PK=TEAM#9, SK=USER#1. 어느 쪽을 쿼리하든 다른 쪽을
나열한다 — 조인 테이블에 대한 DynamoDB의 대체물이다.
단일 테이블을 쓰지 말아야 할 때
공짜가 아니다. 오버로딩된 하나의 테이블은 이해하기 더 어렵고, 진화시키기 더 어렵고, 분석에 적대적이다. 접근 패턴이 정말로 알려지지 않았거나 끊임없이 바뀌거나, 데이터가 주로 분석용이라면, 별도의 테이블(또는 다른 저장소)이 더 온당한 선택일 수 있다. 단일 테이블은 패턴이 알려져 있고 대량일 때 이긴다.
잘못된 형태의 비용
별도 테이블로 모델링하면 고객을 재조립하기 위해 Scan이나 클라이언트 측 조인을 강제하게
되며, 그것이 Scan 함정이다. 접근 패턴을 먼저 모델링한 다음,
각 패턴을 Query로 만들도록 키를 설계하자. (모델링하지 않은 애드혹 엔터티 간 질문이라면,
DynoTable의 SQL 워크벤치가 클라이언트에서 그 JOIN을 실행해
준다 — 탐색이 재모델링을 기다릴 필요는 없다.)
설계 자체를 스케치하려면 무료 싱글 테이블 디자인 도구를 쓰자 — 액세스 패턴 목록을 예시 항목과 비용 힌트가 포함된 PK/SK/GSI 계획으로 바꿔 준다. 이 항목들이 읽기당 얼마나 비용이 드는지는 항목 크기 & 용량 계산기로 추정하고, 단일 테이블 스키마를 브라우징하며 오버로딩된 컬렉션들을 나란히 보려면 DynoTable을 사용해 보자.