DynamoDB에서 단일 테이블 설계를 쓰지 말아야 할 때
단일 테이블 설계는 DynamoDB의 기본 조언이며, 그럴 만합니다: 하나의 Query가 부모와 그
자식을 돌려주고, 조인도 N+1도 없습니다.
하지만 그것은 거래입니다 — 경직되고 불투명한 스키마로 읽기 속도를 삽니다. 어떤 워크로드는 그 대가를 감당할 수 없고, 그것들에 단일 테이블을 강요하는 것은 그 자체로 지뢰입니다.
DynamoDB에서 단일 테이블 설계를 쓰지 말아야 할 때는 언제인가요?
워크로드가 무거운 OLAP 분석이거나, 무관한 소수의 엔티티에 대한 단순 CRUD이거나, 독립적으로 확장하고 장애가 나는 엔티티라면 단일 테이블 설계를 피하세요. 이런 경우 다중 테이블이 더 읽기 쉽고, 비용은 동일하며, 유연성을 유지합니다. 단일 테이블 설계는 액세스 패턴이 알려져 있고, 서로 연관되며, 대용량일 때만 이깁니다.
- 무거운 분석? 단일 테이블로 하지 마세요. 오버로딩된 키는 OLAP에 적대적입니다 — 컬럼형 저장소로 내보내 거기서 쿼리하세요.
- 소수의 액세스 패턴을 가진 평범한 CRUD? 엔티티당 하나의 테이블이 괜찮고, 읽기 쉽고, 성능에서 아무 비용도 들지 않습니다.
- 독립적으로 확장하거나 실패하는 엔티티? 별도 테이블은 각각을 따로 튜닝하고, 청구하고, 폭발 반경을 제한하게 해줍니다.
- 단일 테이블은 여전히 이깁니다 — 패턴이 알려져 있고, 관련되고, 대용량일 때. 그것이 단일 테이블이 만들어진 케이스입니다.
단일 테이블이 실제로 무엇을 비용으로 하는지 알기
단일 테이블 설계는 공짜가 아닙니다. 그저 비용을 읽기 경로에서 다른 모든 것으로 옮길 뿐입니다. 가독성과 유연성으로 비용을 냅니다.
PK/SK 뒤에 다섯 가지 엔티티 유형을 담은 테이블은 읽기 어렵고, 온보딩하기 어렵고,
바꾸기 어렵습니다. 새 액세스 패턴은 파티션 내 모든 아이템 유형에 걸친 백필을 의미할 수
있습니다.
그러니 질문은 "단일 테이블이 좋은가?"가 아닙니다. "내 액세스 패턴이 그 경직성을 정당화하는가?"입니다. 그렇지 않을 때는 다중 테이블에 손을 뻗으세요.
분석 워크로드를 단일 테이블로 하지 마라
DynamoDB는 OLTP를 위해 만들어졌습니다 — 작고, 알려진, 포인트·범위 읽기. 분석은 OLAP입니다:
GROUP BY, 큰 집계, 데이터셋 전체에 걸친 즉석 슬라이싱. 둘은 반대 방향으로 당깁니다.
단일 테이블 설계는 OLAP를 더 낫게가 아니라 더 나쁘게 만듭니다. 오버로딩된 키와 혼합된 엔티티 유형은 분석 작업이 무언가를 합산하기 전에 먼저 어떤 아이템이 무엇인지 풀어내야 함을 의미합니다 — 깔끔한 컬럼형 스캔의 정반대입니다.
SQL에서 넘어왔다면, 반사적으로 살아 있는 테이블에 대고 집계를 작성합니다. DynamoDB에서
그것은 전체 Scan입니다 — 모든 아이템을 읽고 비용을 내며, 그것이 풀 볼륨의
Scan 지뢰입니다.
해법은 더 영리한 키가 아닙니다. 다른 저장소입니다. AWS 자체의 지침은 DynamoDB를 S3로 내보내고 Athena 같은 쿼리 엔진으로 분석을 실행하는 것입니다.
OLTP와 OLAP를 별도의 엔진에 유지하세요(AWS DynamoDB 개발자 가이드,
S3DataExport.html).
단순 CRUD를 단일 테이블로 하지 마라
앱이 몇 개의 무관한 엔티티를 각자의 id로 읽고 쓰며, 액세스 패턴이 세 개쯤이라면, 단일 테이블은 당신에게 아무것도 사주지 않습니다.
작은 B2B 도구를 위한 Tenants 테이블과 ApiKeys 테이블을 봅시다:
| TenantId | Name | PlanTier |
|---|---|---|
| TNT-8842 | Northwind | pro |
| KeyId | TenantId | Scope |
|---|---|---|
| KEY-01H9... | TNT-8842 | read-write |
각 쿼리는 id로 하는 GetItem이거나, TenantId로 키가 지정된 GSI에 대한 Query입니다.
최적화할 부모-자식 패치가 없으므로, 오버로딩된 파티션이 이길 것이 없습니다. 두 개의 깔끔한
테이블이 하나의 탁한 테이블보다 잘 읽힙니다.
함정은 "모범 사례"라서 단일 테이블을 무비판적으로 따라 하는 것입니다. 모범 사례는 액세스 패턴 우선입니다. 패턴이 사소하다면, 단순한 형태가 옳은 형태입니다.
독립적으로 확장하거나 실패하는 테이블은 분리하라
단일 테이블은 하나의 처리량 표면, 하나의 백업, 하나의 폭발 반경을 공유합니다. 두 엔티티가 극단적으로 다른 쓰기 속도나 내구성 요구를 가질 때, 그 공유된 운명은 부채가 됩니다.
차량 추적 시스템을 상상해 보세요. 차량은 거의 변하지 않고, 그 텔레메트리는 매초 쏟아집니다:
| VehicleId | Make | Model | Region |
|---|---|---|---|
| VEH-204 | Volvo | FH16 | eu-west |
| DeviceTs | VehicleId | SpeedKph | Fuel |
|---|---|---|---|
| 2026-06-23T10:00:01Z | VEH-204 | 88 | 0.61 |
두 테이블은 텔레메트리를 소방 호스에 맞게 프로비저닝하고, 차량을 작고 저렴하게 유지하고, 텔레메트리에만 TTL을 설정하고, 텔레메트리 쓰기 폭풍이 차량 카탈로그 읽기를 스로틀하는 것을 막게 해줍니다. 하나의 테이블은 그 모두를 결합합니다.
2007년 Amazon Dynamo 논문에 따르면 분할과 가용성은 일급 관심사입니다 — 독립적 테이블은 둘 다에 대한 독립적 통제를 줍니다.
확정하기 전에 결정을 지도로 그려라
워크로드를 하나의 관문에 통과시키세요: 엔티티가 관련되어 있고, 패턴이 알려져 있고 대용량인가? 아니라면 다중 테이블입니다.
흐름으로 표현한 판단은 다음과 같습니다 — 맨 위에서 시작해 일치하는 첫 분기를 따라가세요:
오직 오른쪽 아래 잎만 단일 테이블을 누립니다. 다른 모든 경로는 둘 이상의 테이블이 더 낫게 처리합니다.
단일 vs 다중, 정면 대결
| 요소 | 단일 테이블 | 다중 테이블 |
|---|---|---|
| 관련 읽기 | 하나의 Query, 조인 없음 | 클라이언트 쪽 조인 또는 추가 왕복 |
| 가독성 | 불투명한 오버로딩 키 | 테이블당 한 엔티티, 자기 문서화 |
| 새 액세스 패턴 | 종종 백필 | 격리해서 테이블이나 GSI 추가 |
| 분석 / OLAP | 적대적 — 집계 전에 풀어내야 함 | 여전히 내보내되, 엔티티별로 더 깔끔 |
| 독립적 확장 | 공유 처리량 + 폭발 반경 | 따로 튜닝·청구·TTL·백업 |
| 최적의 경우 | 알려지고 관련된 대용량 패턴 | 무관하거나, 진화하거나, 분석적 |
함정 + 다음 단계
거울상의 실수는 과잉 교정입니다 — 진짜로 관련되고 대용량인 엔티티를 테이블당 엔티티로 쪼개고 앱에서 SQL식 조인을 재구축하는 것. 그것이 단일 테이블이 죽이는 N+1 지뢰입니다.
도그마가 아니라 액세스 패턴이 요구하는 형태를 고르세요.
관계를 모델링할 때는 올바른 인덱스 유형에 의지하세요 — 추가하기 전에 GSI vs LSI를 참고하세요.
다중 테이블 스키마에 대고 쿼리를 작성할 때는, 먼저
DynamoDB Expression Builder에서
KeyConditionExpression을 스케치하세요.
그래야 전체 Scan 형태가 프로덕션에 닿기 전에 잡아냅니다.
그런 다음 DynoTable을 사용해 자신의 테이블에 두 형태를 둘러보고, 당신의 액세스 패턴이 실제로 어느 쪽을 원하는지 보세요.