

# DynamoDB의 관계형 데이터 모델링 모범 사례
<a name="bp-relational-modeling"></a>

이 섹션에서는 Amazon DynamoDB의 관계형 데이터 모델링 모범 사례를 제공합니다. 먼저 기존 데이터 모델링 개념을 소개합니다. 그런 다음 기존 관계형 데이터베이스 관리 시스템에 비해 DynamoDB를 사용할 때의 이점, 즉 JOIN 작업의 필요성이 없어지고 오버헤드가 줄어드는 것에 대해 설명합니다.

또한 효율적으로 확장되는 DynamoDB 테이블을 설계하는 방법을 설명합니다. 마지막으로 DynamoDB에서 관계형 데이터를 모델링하는 방법에 대한 예를 제공합니다.

**Topics**
+ [기존 관계형 데이터베이스 모델](#SQLtoNoSQL.relational-modeling2)
+ [DynamoDB를 통해 JOIN 작업의 필요성을 없애는 방법](#bp-relational-modeling-joins)
+ [DynamoDB 트랜잭션이 쓰기 프로세스의 오버헤드를 제거하는 방법](#bp-relational-modeling-transactions)
+ [DynamoDB의 관계형 데이터를 모델링하는 첫 번째 단계](bp-modeling-nosql.md)
+ [DynamoDB의 관계형 데이터 모델링에 대한 예](bp-modeling-nosql-B.md)

## 기존 관계형 데이터베이스 모델
<a name="SQLtoNoSQL.relational-modeling2"></a>

기존의 관계형 데이터베이스 관리 시스템(RDBMS)은 데이터를 정규화된 관계형 구조로 저장합니다. 관계형 데이터 모델의 목적은 정규화를 통해 데이터 중복을 줄여 참조 무결성을 지원하고 데이터 이상 현상을 줄이는 것입니다.

다음 스키마는 일반 주문 입력 애플리케이션을 위한 관계형 데이터 모델의 예입니다. 이 애플리케이션은 가상의 제조업체의 운영 및 비즈니스 지원 시스템을 뒷받침하는 HR 스키마를 지원합니다.

![\[RDBMS 스키마의 예\]](http://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/images/RDBMS.png)


비관계형 데이터베이스 서비스인 DynamoDB는 기존의 관계형 데이터베이스 관리 시스템에 비해 많은 이점을 제공합니다.

## DynamoDB를 통해 JOIN 작업의 필요성을 없애는 방법
<a name="bp-relational-modeling-joins"></a>

RDBMS는 구조 쿼리 언어(SQL)를 사용하여 데이터를 애플리케이션에 반환합니다. 데이터 모델의 정규화로 인해 이러한 쿼리에서는 일반적으로 `JOIN` 연산자를 사용하여 하나 이상의 테이블에서 데이터를 결합해야 합니다.

예를 들어, 각 항목을 출하할 수 있는 모든 창고의 재고량을 기준으로 정렬된 구매 주문 항목 목록을 생성하려면, 앞서 스키마에 대해 다음 SQL 쿼리를 발행할 수 있습니다.

```
SELECT * FROM Orders
  INNER JOIN Order_Items ON Orders.Order_ID = Order_Items.Order_ID
  INNER JOIN Products ON Products.Product_ID = Order_Items.Product_ID
  INNER JOIN Inventories ON Products.Product_ID = Inventories.Product_ID
  ORDER BY Quantity_on_Hand DESC
```

이런 유형의 SQL 쿼리는 데이터에 유연하게 액세스 할 수 있는 API를 제공할 수 있지만, 상당히 많은 처리량이 필요합니다. 쿼리의 각 조인은 각 테이블의 데이터를 스테이징한 다음 조합하여 결과 집합을 반환해야 하므로 쿼리의 런타임 복잡성이 증가합니다.

쿼리를 실행하는 데 걸리는 시간에 영향을 줄 수 있는 추가 요소로는 테이블 크기 및 조인되는 열의 인덱스 여부가 있습니다. 앞의 쿼리는 여러 테이블을 대상으로 복잡한 쿼리를 시작한 후, 결과 세트를 분류합니다.

`JOINs`에 대한 필요를 없애는 것이 NoSQL 데이터 모델링의 핵심입니다. 이것이 바로 Amazon.com을 지원하기 위해 DynamoDB를 구축한 이유이자 DynamoDB가 모든 규모에서 일관된 성능을 제공할 수 있는 이유입니다. SQL 쿼리 및 `JOINs`의 런타임 복잡성을 고려할 때 RDBMS 성능은 규모에 따라 일정하지 않으므로 고객 애플리케이션이 확장됨에 따라 성능 문제가 발생합니다.

데이터를 정규화하면 디스크에 저장되는 데이터의 양이 줄어들지만 성능에 영향을 미치는 가장 제한적인 리소스는 CPU 시간과 네트워크 지연 시간인 경우가 많습니다.

DynamoDB는 항목에 대한 단일 요청으로 애플리케이션 쿼리에 완전히 응답하도록 `JOINs`을 제거(및 데이터 비정규화를 촉진)하고 데이터베이스 아키텍처를 최적화하고 두 제약 조건을 모두 최소화하도록 구축되었습니다. 이러한 특성 덕분에 DynamoDB는 어떤 규모에서든 한 자릿수 밀리초 수준의 성능을 제공할 수 있습니다. DynamoDB 작업의 런타임 복잡성은 데이터 크기와 관계없이 일반적인 액세스 패턴에서 일정하기 때문입니다.

## DynamoDB 트랜잭션이 쓰기 프로세스의 오버헤드를 제거하는 방법
<a name="bp-relational-modeling-transactions"></a>

RDBMS를 느리게 할 수 있는 또 다른 요인은 트랜잭션을 사용하여 정규화된 스키마에 쓰는 것입니다. 예시에서 볼 수 있는 것처럼 대부분의 온라인 트랜잭션 처리(OLTP) 애플리케이션이 사용하는 관계형 데이터 구조는 구분한 후 RDBMS에 저장할 때 여러 논리적 테이블로 배포해야 합니다.

즉 ACID를 준수하는 트랜잭션 프레임워크로 애플리케이션이 쓰기 처리 중에 객체 읽기를 시도할 때 발생할 수 있는 교착 상태와 데이터 무결성 문제를 방지해야 합니다. 이러한 트랜잭션 프레임워크는 관계형 스키마와 결합될 때 쓰기 프로세스에 상당한 오버헤드를 추가할 수 있습니다.

DynamoDB에서 트랜잭션을 구현하면 RDBMS에서 발생하는 일반적인 규모 조정 문제를 방지할 수 있습니다. 트랜잭션이 단일 API 호출로 실행되고 해당 단일 트랜잭션에서 액세스할 수 있는 항목 수가 제한되기 때문입니다. 장기 실행 트랜잭션은 트랜잭션이 닫히지 않기 때문에 장기간 또는 영구적으로 데이터 잠금을 유지하여 운영상의 문제를 일으킬 수 있습니다.

DynamoDB에서 이러한 문제를 방지하기 위해 트랜잭션은 두 개의 고유한 API 작업인 `TransactWriteItems` 및 `TransactGetItems`를 사용하여 구현되었습니다. 이러한 API 작업에는 RDBMS에서 흔히 볼 수 있는 시작 및 종료 시맨틱이 없습니다. 또한 DynamoDB는 트랜잭션 내에 100개의 항목 액세스 제한을 두어 장기 실행 트랜잭션을 유사하게 방지합니다. DynamoDB 트랜잭션에 대해 자세히 알아보려면 [트랜잭션 작업](transactions.md)을 참조하세요.

이런 이유로 비즈니스에서 트래픽이 많은 쿼리에 지연 시간이 낮은 응답이 요구되는 경우, NoSQL 시스템을 활용하는 것이 기술적 및 경제적으로 합리적입니다. Amazon DynamoDB는 이를 방지해 관계형 시스템의 확장성을 제한하는 문제를 해결하는 데 도움을 줍니다.

RDBMS의 성능은 일반적으로 다음과 같은 이유로 제대로 확장되지 않습니다.
+ 고가의 조인을 사용해 필요한 쿼리 결과 보기를 재수집해야 합니다.
+ 데이터를 정규화해서, 여러 테이블에 저장을 하는 데, 디스크 쓰기 작업에 여러 쿼리가 요구됩니다.
+ ACID 준수 트랜잭션 시스템의 경우 일반적으로 성능과 관련된 '비용'이 발생합니다.

DynamoDB는 다음 이유 때문에 효과적으로 조정이 됩니다.
+ 스키마 유연성 덕분에 DynamoDB가 복잡한 계층적 데이터를 단일 항목으로 저장할 수 있습니다.
+ 복합 키 설계 덕분에 관련 항목을 동일한 테이블에 함께 가까이 저장할 수 있습니다.
+ 트랜잭션은 한 번의 작업으로 수행되며 장시간 실행되는 작업을 피하기 위해 액세스할 수 있는 항목 수를 100개로 제한합니다.

데이터 스토어에 대한 쿼리가 훨씬 더 간단합니다(아래 형식인 경우가 많음).

```
SELECT * FROM Table_X WHERE Attribute_Y = "somevalue"
```

DynamoDB는 이전 예의 RDBMS보다 요청 데이터 반환이 효과적입니다.

# DynamoDB의 관계형 데이터를 모델링하는 첫 번째 단계
<a name="bp-modeling-nosql"></a>

**참고**  
NoSQL 설계에는 RDBMS 설계와 다른 사고 방식이 요구됩니다. RDBMS의 경우, 액세스 패턴을 생각하지 않고 정규화된 데이터 모델을 생성할 수 있습니다. 그런 후 나중에 새로운 질문과 쿼리에 대한 요구 사항이 생길 때 이를 확장할 수 있습니다. 대조적으로 Amazon DynamoDB의 경우 대답해야 할 질문을 모르기 전까지는 스키마 설계를 시작할 수 없습니다. 사전에 비즈니스 문제와 애플리케이션 사용 사례를 이해해야 합니다.

효율적으로 확장되는 DynamoDB 테이블 설계를 시작하려면, 몇 단계를 거쳐야 하는 데 먼저 지원해야 하는 OSS/BSS(운영 지원 시스템 및 비즈니스 지원 시스템)에서 요구되는 액세스 패턴을 파악해야 합니다.
+ 새 애플리케이션의 경우, 활동과 목표에 대한 사용자 사례를 검토합니다. 파악한 다양한 사용 사례를 문서화하고, 여기에 필요한 액세스 패턴을 분석합니다.
+ 기존 애플리케이션의 경우, 쿼리 로그를 분석해 현재 시스템을 사용하고 있는 사람의 수와 핵심 액세스 패턴을 파악해야 합니다.

이런 프로세스를 끝내면 다음과 같은 형태를 가진 목록이 준비되어야 합니다.


**주문 입력 애플리케이션의 액세스 패턴**  

| 패턴 번호 | 액세스 패턴 설명 | 
| --- | --- | 
| 1 | 직원 ID별로 직원 세부 정보 조회 | 
| 2 | 직원 이름별 직원 세부 정보 쿼리 | 
| 3 | 직원의 전화번호 찾기 | 
| 4 | 고객의 전화번호 찾기 | 
| 5 | 날짜 범위 내에서 고객에 대한 주문 가져오기 | 
| 6 | 날짜 범위 내의 모든 미결 주문 표시 | 
| 7 | 최근 채용된 모든 직원 표시 | 
| 8 | 창고의 모든 직원 찾기 | 
| 9 | 제품 주문 시 모든 항목 가져오기 | 
| 10 | 모든 웨어하우스에서 제품에 대한 인벤토리 가져오기 | 
| 11 | 계정 담당자별로 고객 가져오기 | 
| 12 | 계정 담당자별로 주문 가져오기 | 
| 13 | 직함으로 직원 가져오기 | 
| 14 | 제품 및 창고별로 인벤토리 가져오기 | 
| 15 | 전체 제품 인벤토리 가져오기 | 

실제 사용하는 애플리케이션의 경우, 목록이 훨씬 더 길 것입니다. 그러나 이는 프로덕션 환경에서 찾을 수 있는 다양하고 복잡한 쿼리 패턴을 보여줍니다.

DynamoDB 스키마 설계에 대한 최신 접근 방식은 집계 지향 원칙을 사용하여 엄격한 개체 경계가 아닌 액세스 패턴을 기반으로 데이터를 그룹화합니다. 이 접근 방식은 다음과 같이 여러 설계 패턴을 고려합니다.
+ *단일 테이블 설계* - 복합 정렬 키, 오버로드된 글로벌 보조 인덱스 및 인접 목록 패턴을 사용하여 여러 엔터티 유형을 하나의 테이블에 저장
+ *다중 테이블 설계* - 독립된 운영 특성과 낮은 액세스 상관관계를 가진 엔터티에 대해 별도의 테이블을 교차 엔터티 쿼리에 대한 전략적 GSI와 함께 사용
+ *집계 설계* - 항상 함께 액세스할 때 관련 데이터 임베딩(주문 \$1 OrderItems) 또는 관계 식별을 위한 항목 컬렉션 사용(제품 \$1 인벤토리)

이러한 접근 방식 중에서 선택하는 방법은 특정 액세스 패턴, 데이터 특성 및 운영 요구 사항에 따라 달라집니다. 이런 요소들을 사용해 데이터를 구조화, 애플리케이션이 테이블이나 인덱스에 대한 한 번의 쿼리로 특정 액세스 패턴에 필요한 것을 검색하도록 만들 수 있습니다.

**참고**  
단일 테이블 설계와 다중 테이블 설계 중에서 선택하는 것은 특정 요구 사항에 따라 달라집니다. 단일 테이블 설계는 개체의 액세스 상관관계가 높고 운영 특성이 유사한 경우에 적합합니다. 다중 테이블 설계는 엔터티에 독립적인 운영 요구 사항, 다양한 액세스 패턴이 있거나 명확한 운영 경계가 필요한 경우에 선호됩니다. 이 가이드의 예제는 전략적 집계 및 비정규화를 사용하는 다중 테이블 접근 방식을 보여줍니다.

DynamoDB용 NoSQL Workbench를 사용하여 파티션 키 설계를 시각화하려면 [NoSQL Workbench로 데이터 모델 빌드](workbench.Modeler.md) 섹션을 참조하세요.

# DynamoDB의 관계형 데이터 모델링에 대한 예
<a name="bp-modeling-nosql-B"></a>

이 예는 Amazon DynamoDB에서 관계형 데이터를 모델링하는 방법을 설명합니다. DynamoDB 테이블 설계는 [관계형 모델링](bp-relational-modeling.md)에 표시된 관계형 주문 입력 스키마에 해당합니다. 이 설계는 단일 인접 목록 대신 여러 개의 특수 테이블을 사용하여 명확한 운영 경계를 제공하는 동시에 전략적 GSI를 활용하여 모든 액세스 패턴을 효율적으로 제공합니다.

설계 접근 방식은 집계 지향 원칙을 사용하여 엄격한 개체 경계가 아닌 액세스 패턴을 기반으로 데이터를 그룹화합니다. 주요 설계 결정에는 액세스 상관관계가 낮은 개체에 대해 별도의 테이블 사용, 항상 함께 액세스할 때 관련 데이터 포함, 관계를 식별하기 위한 항목 컬렉션 사용이 포함됩니다.

다음 테이블과 함께 제공되는 인덱스는 관계형 순서 항목 스키마를 지원합니다.

## 직원 테이블 설계
<a name="employee-table-design"></a>

직원 테이블은 직원 정보를 항목당 단일 엔터티로 저장하고 직원 직접 조회에 최적화되어 있으며 전략적 GSI를 통해 여러 쿼리 패턴을 지원합니다. 이 표는 독립적인 운영 특성과 낮은 교차 엔터티 액세스 상관관계를 가진 엔터티에 대해 별도의 테이블을 설계하는 원칙을 보여줍니다.

테이블은 각 직원이 고유한 엔터티이므로 정렬 키 없이 단순 파티션 키(employee\$1id)를 사용합니다. 4개의 GSI를 사용하면 다양한 속성을 기준으로 효율적인 쿼리를 수행할 수 있습니다.
+ *EmployeeByName GSI* - 모든 직원 속성과 함께 INCLUDE 프로젝션을 사용하여 이름으로 전체 직원 세부 정보 검색을 지원하고 employee\$1id를 정렬 키로 사용하여 중복될 수 있는 이름을 처리합니다.
+ *EmployeeByWarehouse GSI* - 필수 속성(이름, job\$1title, hire\$1date)만 있는 INCLUDE 프로젝션을 사용하여 스토리지 비용을 최소화하는 동시에 웨어하우스 기반 쿼리를 지원합니다.
+ *EmployeeByJobTitle GSI* - 보고 및 조직 분석을 위해 INCLUDE 프로젝션을 사용하여 역할 기반 쿼리를 활성화합니다.
+ *EmployeeByHireDate GSI* - 최근 채용과 관련하여 효율적인 날짜 범위 쿼리를 활성화하기 위해 hire\$1date를 정렬 키로 사용하여 정적 파티션 키 값 "EMPLOYEE"를 사용합니다. 직원 추가/업데이트는 일반적으로 1,000WCU 미만이므로 단일 파티션이 핫 파티션 문제 없이 쓰기 로드를 처리할 수 있습니다.


**직원 테이블 - 기본 테이블 구조**  

| employee\$1id(PK) | 이름 | phone\$1number | warehouse\$1id | job\$1title | hire\$1date | entity\$1type | 
| --- | --- | --- | --- | --- | --- | --- | 
| emp\$1001 | John Smith | ["\$11-555-0101"] | wh\$1sea | Manager | 2024-03-15 | 피고용인 | 
| emp\$1002 | Jane Doe | ["\$11-555-0102", "\$11-555-0103"] | wh\$1sea | 연결 | 2025-01-10 | 피고용인 | 
| emp\$1003 | Bob Wilson | ["\$11-555-0104"] | wh\$1pdx | 연결 | 2025-06-20 | 피고용인 | 
| emp\$1004 | Alice Brown | ["\$11-555-0105"] | wh\$1pdx | Supervisor | 2023-11-05 | 피고용인 | 
| emp\$1005 | Charlie Davis | ["\$11-555-0106"] | wh\$1sea | 연결 | 2025-12-01 | 피고용인 | 


**EmployeeByName GSI - 직원 이름 쿼리 지원**  

| 이름(GSI-PK) | employee\$1id(GSI-SK) | phone\$1number | warehouse\$1id | job\$1title | hire\$1date | 
| --- | --- | --- | --- | --- | --- | 
| Alice Brown | emp\$1004 | ["\$11-555-0105"] | wh\$1pdx | Supervisor | 2023-11-05 | 
| Bob Wilson | emp\$1003 | ["\$11-555-0104"] | wh\$1pdx | 연결 | 2025-06-20 | 
| Charlie Davis | emp\$1005 | ["\$11-555-0106"] | wh\$1sea | 연결 | 2025-12-01 | 
| Jane Doe | emp\$1002 | ["\$11-555-0102", "\$11-555-0103"] | wh\$1sea | 연결 | 2025-01-10 | 
| John Smith | emp\$1001 | ["\$11-555-0101"] | wh\$1sea | Manager | 2024-03-15 | 


**EmployeeByWarehouse GSI - 웨어하우스 쿼리 지원**  

| warehouse\$1id(GSI-PK) | employee\$1id(GSI-SK) | 이름 | job\$1title | hire\$1date | 
| --- | --- | --- | --- | --- | 
| wh\$1pdx | emp\$1003 | Bob Wilson | 연결 | 2025-06-20 | 
| wh\$1pdx | emp\$1004 | Alice Brown | Supervisor | 2023-11-05 | 
| wh\$1sea | emp\$1001 | John Smith | Manager | 2024-03-15 | 
| wh\$1sea | emp\$1002 | Jane Doe | 연결 | 2025-01-10 | 
| wh\$1sea | emp\$1005 | Charlie Davis | 연결 | 2025-12-01 | 


**EmployeeByJobTitle GSI - 직함 쿼리 지원**  

| job\$1title(GSI-PK) | employee\$1id(GSI-SK) | 이름 | warehouse\$1id | hire\$1date | 
| --- | --- | --- | --- | --- | 
| 연결 | emp\$1002 | Jane Doe | wh\$1sea | 2025-01-10 | 
| 연결 | emp\$1003 | Bob Wilson | wh\$1pdx | 2025-06-20 | 
| 연결 | emp\$1005 | Charlie Davis | wh\$1sea | 2025-12-01 | 
| Manager | emp\$1001 | John Smith | wh\$1sea | 2024-03-15 | 
| Supervisor | emp\$1004 | Alice Brown | wh\$1pdx | 2023-11-05 | 


**EmployeeByHireDate GSI - 최근 고용 쿼리 지원**  

| entity\$1type(GSI-PK) | hire\$1date(GSI-SK) | employee\$1id | 이름 | warehouse\$1id | 
| --- | --- | --- | --- | --- | 
| 피고용인 | 2023-11-05 | emp\$1004 | Alice Brown | wh\$1pdx | 
| 피고용인 | 2024-03-15 | emp\$1001 | John Smith | wh\$1sea | 
| 피고용인 | 2025-01-10 | emp\$1002 | Jane Doe | wh\$1sea | 
| 피고용인 | 2025-06-20 | emp\$1003 | Bob Wilson | wh\$1pdx | 
| 피고용인 | 2025-12-01 | emp\$1005 | Charlie Davis | wh\$1sea | 

## 고객 테이블 설계
<a name="customer-table-design"></a>

고객 테이블은 효율적인 계정 담당자 쿼리를 활성화하기 위해 account\$1rep\$1id의 전략적 비정규화로 고객 정보를 유지합니다. 이 설계 선택은 쿼리 성능을 위해 약간의 스토리지 오버헤드를 상쇄하므로 고객과 계정 담당자 데이터를 조인할 필요가 없습니다.

이 표는 DynamoDB의 스키마 유연성을 보여주는 목록 속성을 사용하여 고객당 여러 전화번호를 지원합니다. 단일 GSI는 계정 담당자 워크플로를 활성화합니다.
+ *CustomerByAccountRep GSI* - 전체 고객 레코드 검색 없이 계정 담당자 고객 관리를 지원하기 위해 이름 및 이메일 속성과 함께 INCLUDE 프로젝션 사용


**고객 테이블 - 기본 테이블 구조**  

| customer\$1id(PK) | 이름 | phone\$1number | 이메일 | account\$1rep\$1id | 
| --- | --- | --- | --- | --- | 
| cust\$1001 | Acme Corp | ["\$11-555-1001"] | contact@acme.com | rep\$1001 | 
| cust\$1002 | TechStart Inc | ["\$11-555-1002", "\$11-555-1003"] | info@techstart.com | rep\$1001 | 
| cust\$1003 | 글로벌 거래자 | ["\$11-555-1004"] | sales@globaltraders.com | rep\$1002 | 
| cust\$1004 | BuildRight LLC | ["\$11-555-1005"] | orders@buildright.com | rep\$1002 | 
| cust\$1005 | FastShip Co | ["\$11-555-1006"] | support@fastship.com | rep\$1003 | 


**CustomerByAccountRep GSI - 계정 담당자 쿼리 지원**  

| account\$1rep\$1id(GSI-PK) | customer\$1id(GSI-SK) | 이름 | 이메일 | 
| --- | --- | --- | --- | 
| rep\$1001 | cust\$1001 | Acme Corp | contact@acme.com | 
| rep\$1001 | cust\$1002 | TechStart Inc | info@techstart.com | 
| rep\$1002 | cust\$1003 | 글로벌 거래자 | sales@globaltraders.com | 
| rep\$1002 | cust\$1004 | BuildRight LLC | orders@buildright.com | 
| rep\$1003 | cust\$1005 | FastShip Co | support@fastship.com | 

## 주문 테이블 설계
<a name="order-table-design"></a>

주문 테이블은 주문 헤더 및 주문 항목에 대해 별도의 항목과 함께 수직 파티셔닝을 사용합니다. 이 설계를 사용하면 효율적인 액세스를 위해 모든 주문 구성 요소를 동일한 파티션 내에 유지하면서 효율적인 제품 기반 쿼리를 수행할 수 있습니다. 각 주문은 여러 항목으로 구성됩니다.
+ *주문 헤더* - PK=order\$1id, SK=order\$1id의 주문 메타데이터 포함
+ *주문 항목* - PK=order\$1id, SK=product\$1id가 있는 개별 품목, 직접 제품 쿼리 활성화

**참고**  
이 수직 분할 접근 방식은 쿼리 유연성을 높이기 위해 임베디드 주문 항목의 단순성을 절충합니다. 각 주문 항목은 별도의 DynamoDB 항목이 되어, 단일 요청으로 효율적으로 검색할 수 있도록 모든 주문 데이터를 동일한 파티션 내에 유지하면서 효율적인 제품 기반 쿼리를 가능하게 합니다.

테이블에는 account\$1rep\$1id(고객 테이블에서 복제됨)의 전략적 비정규화가 포함되어 있어 고객 조회 없이 직접 계정 담당자 쿼리를 활성화할 수 있습니다. 처리량이 많은 쓰기 시나리오의 경우 OPEN 주문에는 여러 파티션에서 쓰기 샤딩을 활성화할 수 있는 상태 및 샤드 속성이 포함됩니다.

4개의 GSI가 최적화된 프로젝션으로 다양한 쿼리 패턴을 지원합니다.
+ *OrderByCustomerDate GSI* - 주문 요약 및 항목 세부 정보와 함께 INCLUDE 프로젝션을 사용하여 날짜 범위 필터링으로 고객 주문 기록을 지원합니다.
+ *OpenOrdersByDate GSI(Sparse, Sharded)* - 5개의 샤드가 있는 다중 속성 파티션 키(상태 \$1 샤드)를 사용하여 파티션 간에 5,000WPS(초당 쓰기 수)를 분산합니다(각 1,000WPS, DynamoDB의 파티션당 1,000WCU 제한과 일치). OPEN 주문(총 주문의 20%)만 인덱싱하므로 GSI 스토리지 비용을 줄이는 데 도움이 될 수 있습니다. 클라이언트 측 결과 병합을 사용하여 샤드 5개 모두에 병렬 쿼리 필요
+ *OrderByAccountRep GSI* - 주문 요약 속성과 함께 INCLUDE 프로젝션을 사용하여 전체 주문 세부 정보 없이 계정 담당자 워크플로 지원
+ *ProductInOrders GSI* - OrderItem 레코드(PK=order\$1id, SK=product\$1id)에서 생성된 이 GSI를 사용하면 쿼리가 특정 제품이 포함된 모든 주문을 찾을 수 있습니다. 제품 수요 분석을 위해 주문 컨텍스트(customer\$1id, order\$1date, 수량)와 함께 INCLUDE 프로젝션을 사용합니다.


**순서 테이블 - 기본 테이블 구조(수직 분할)**  

| PK | SK | customer\$1id | order\$1date | status | account\$1rep\$1id | quantity | 가격 | 샤드 | 
| --- | --- | --- | --- | --- | --- | --- | --- | --- | 
| ord\$1001 | ord\$1001 | cust\$1001 | 2025-11-15 | CLOSED | rep\$1001 |  |  |  | 
| ord\$1001 | prod\$1100 |  |  |  |  | 5 | 25.00 |  | 
| ord\$1002 | ord\$1002 | cust\$1001 | 2025-12-20 | OPEN | rep\$1001 |  |  | 0 | 
| ord\$1002 | prod\$1101 |  |  |  |  | 10 | 15.00 |  | 
| ord\$1003 | ord\$1003 | cust\$1002 | 2026-01-05 | OPEN | rep\$1001 |  |  | 2 | 
| ord\$1003 | prod\$1100 |  |  |  |  | 3 | 25.00 |  | 


**OrderByCustomerDate GSI - 고객 주문 쿼리 지원**  

| customer\$1id(GSI-PK) | order\$1date(GSI-SK) | order\$1id | status | total\$1amount | order\$1items | 샤드 | 
| --- | --- | --- | --- | --- | --- | --- | 
| cust\$1001 | 2025-11-15 | ord\$1001 | CLOSED | 225.00 | [\$1product\$1id: "prod\$1100", qty: 5\$1] |  | 
| cust\$1001 | 2025-12-20 | ord\$1002 | OPEN | 150.00 | [\$1product\$1id: "prod\$1101", qty: 10\$1] | 0 | 
| cust\$1002 | 2026-01-05 | ord\$1003 | OPEN | 175.00 | [\$1product\$1id: "prod\$1100", qty: 3\$1] | 2 | 
| cust\$1003 | 2025-10-10 | ord\$1004 | CLOSED | 250.00 | [\$1product\$1id: "prod\$1101", qty: 5\$1] |  | 
| cust\$1004 | 2026-01-03 | ord\$1005 | OPEN | 200.00 | [\$1product\$1id: "prod\$1100", qty: 20\$1] | 1 | 


**OpenOrdersByDate GSI(Sparse, Sharded) - 높은 처리량 미결 주문 쿼리 지원**  

| 상태(GSI-PK-1) | 샤드(GSI-PK-2) | order\$1date(SK) | order\$1id | customer\$1id | account\$1rep\$1id | order\$1items | total\$1amount | 
| --- | --- | --- | --- | --- | --- | --- | --- | 
| OPEN | 0 | 2025-12-20 | ord\$1002 | cust\$1001 | rep\$1001 | [\$1product\$1id: "prod\$1101", qty: 10\$1] | 150.00 | 
| OPEN | 1 | 2026-01-03 | ord\$1005 | cust\$1004 | rep\$1002 | [\$1product\$1id: "prod\$1100", qty: 20\$1] | 200.00 | 
| OPEN | 2 | 2026-01-05 | ord\$1003 | cust\$1002 | rep\$1001 | [\$1product\$1id: "prod\$1100", qty: 3\$1] | 175.00 | 


**OrderByAccountRep GSI - 계정 담당자 주문 쿼리 지원**  

| account\$1rep\$1id(GSI-PK) | order\$1date(GSI-SK) | order\$1id | customer\$1id | status | total\$1amount | 
| --- | --- | --- | --- | --- | --- | 
| rep\$1001 | 2025-11-15 | ord\$1001 | cust\$1001 | CLOSED | 225.00 | 
| rep\$1001 | 2025-12-20 | ord\$1002 | cust\$1001 | OPEN | 150.00 | 
| rep\$1001 | 2026-01-05 | ord\$1003 | cust\$1002 | OPEN | 175.00 | 
| rep\$1002 | 2025-10-10 | ord\$1004 | cust\$1003 | CLOSED | 250.00 | 
| rep\$1002 | 2026-01-03 | ord\$1005 | cust\$1004 | OPEN | 200.00 | 


**ProductInOrders GSI - 제품 주문 쿼리 지원**  

| product\$1id(GSI-PK) | order\$1id(GSI-SK) | customer\$1id | order\$1date | quantity | 
| --- | --- | --- | --- | --- | 
| prod\$1100 | ord\$1001 | cust\$1001 | 2025-11-15 | 5 | 
| prod\$1100 | ord\$1003 | cust\$1002 | 2026-01-05 | 3 | 
| prod\$1101 | ord\$1002 | cust\$1001 | 2025-12-20 | 10 | 

## 제품 테이블 설계
<a name="product-table-design"></a>

제품 테이블은 항목 수집 패턴을 사용하여 동일한 파티션 내에 제품 메타데이터와 인벤토리 데이터를 모두 저장합니다. 이 설계는 제품과 인벤토리 간의 식별 관계를 활용합니다. 상위 제품이 없으면 인벤토리가 존재할 수 없습니다. 제품 메타데이터에는 PK=product\$1id를 SK=product\$1id와 함께 사용하고 인벤토리 항목에는 SK=warehouse\$1id를 사용하면 별도의 인벤토리 테이블과 GSI가 필요하지 않으므로 비용이 약 50% 절감됩니다.

이 패턴을 사용하면 개별 웨어하우스 인벤토리(복합 키가 있는 GetItem)와 제품의 모든 웨어하우스 인벤토리(파티션 키에 대한 쿼리) 모두에 대해 효율적인 쿼리를 수행할 수 있습니다. 제품 메타데이터 항목의 total\$1inventory 속성은 빠른 총 인벤토리 조회를 위한 비정규화된 집계를 제공합니다.


**제품 테이블 - 기본 테이블 구조(항목 수집 패턴)**  

| product\$1id(PK) | warehouse\$1id(SK) | product\$1name | category | unit\$1price | inventory\$1quantity | total\$1inventory | 
| --- | --- | --- | --- | --- | --- | --- | 
| prod\$1100 | prod\$1100 | 위젯 A | 하드웨어 | 25.00 |  | 500 | 
| prod\$1100 | wh\$1sea |  |  |  | 200 |  | 
| prod\$1100 | wh\$1pdx |  |  |  | 150 |  | 
| prod\$1100 | wh\$1atl |  |  |  | 150 |  | 
| prod\$1101 | prod\$1101 | 도구 B | 전자 제품 | 50.00 |  | 300 | 
| prod\$1101 | wh\$1sea |  |  |  | 100 |  | 
| prod\$1101 | wh\$1pdx |  |  |  | 200 |  | 

각 테이블은 특정 글로벌 보조 인덱스(GSI)로 설계되어 필요한 액세스 패턴을 효율적으로 지원합니다. 설계에서는 전략적 비정규화 및 희소 인덱싱과 함께 집계 지향 원칙을 사용하여 성능과 비용을 최적화합니다.

주요 설계 최적화에는 다음이 포함됩니다.
+ *구문 분석 GSI* - OpenOrdersByDate는 OPEN 주문(총 20%)만 인덱싱하므로 GSI 스토리지 비용을 줄이는 데 도움이 될 수 있습니다.
+ *항목 수집 패턴* - 제품 테이블은 PK=product\$1id, SK=warehouse\$1id를 사용하여 인벤토리를 저장하여 별도의 인벤토리 테이블을 제거합니다.
+ *Order \$1 OrderItems 집계* - 100% 액세스 상관 관계로 인해 단일 항목으로 포함됩니다.
+ *전략적 비정규화* - 효율적인 쿼리를 위해 주문 테이블에 account\$1rep\$1id가 중복됩니다.

마지막으로 앞서 정의한 액세스 패턴을 다시 살펴보겠습니다. 다음 표는 전략적 GSI와 함께 다중 테이블 설계를 사용하여 각 액세스 패턴을 효율적으로 지원하는 방법을 보여줍니다. 각 패턴은 직접 키 조회 또는 단일 GSI 쿼리를 사용하여 비용이 많이 드는 스캔을 방지하고 모든 규모에서 일관된 성능을 제공합니다.


| 일련 번호 | 액세스 패턴 | 쿼리 조건 | 
| --- | --- | --- | 
|  1  |  직원 ID별로 직원 세부 정보 조회  |  직원 테이블: GetItem(employee\$1id="emp\$1001")  | 
|  2  |  직원 이름별 직원 세부 정보 쿼리  |  EmployeeByName GSI: Query(name="John Smith")  | 
|  3  |  직원의 전화번호 찾기  |  직원 테이블: GetItem(employee\$1id="emp\$1001")  | 
|  4  |  고객의 전화번호 찾기  |  고객 테이블: GetItem(customer\$1id="cust\$1001")  | 
|  5  |  날짜 범위 내에서 고객에 대한 주문 가져오기  |  OrderByCustomerDate GSI: Query(customer\$1id="cust\$1001", order\$1date BETWEEN "2025-01-01" AND "2025-12-31")  | 
|  6  |  날짜 범위 내의 모든 미결 주문 표시  |  OpenOrdersByDate GSI: 다중 속성 PK(status="OPEN" \$1 shard=0-4), SK=order\$1date BETWEEN "2025-01-01" AND "2025-12-31"와 함께 5개의 샤드 쿼리, 결과 병합  | 
|  7  |  최근 채용된 모든 직원 표시  |  EmployeeByHireDate GSI: Query(entity\$1type="EMPLOYEE", hire\$1date >= "2025-01-01")  | 
|  8  |  창고의 모든 직원 찾기  |  EmployeeByWarehouse GSI: Query(warehouse\$1id="wh\$1sea")  | 
|  9  |  제품 주문 시 모든 항목 가져오기  |  ProductInOrders GSI: Query(product\$1id="prod\$1100")  | 
|  10  |  모든 웨어하우스에서 제품에 대한 인벤토리 가져오기  |  제품 테이블: Query(product\$1id="prod\$1100")  | 
|  11  |  계정 담당자별로 고객 가져오기  |  CustomerByAccountRep GSI: Query(account\$1rep\$1id="rep\$1001")  | 
|  12  |  계정 담당자별로 주문 가져오기  |  OrderByAccountRep GSI: Query(account\$1rep\$1id="rep\$1001")  | 
|  13  |  직함으로 직원 가져오기  |  EmployeeByJobTitle GSI: Query(job\$1title="Manager")  | 
|  14  |  제품 및 창고별로 인벤토리 가져오기  |  제품 테이블: GetItem(product\$1id="prod\$1100", warehouse\$1id="wh\$1sea")  | 
|  15  |  전체 제품 인벤토리 가져오기  |  제품 테이블: GetItem(product\$1id="prod\$1100", warehouse\$1id="prod\$1100")  | 