DynamoDB LSI vs GSI: 다른 속성으로 쿼리할 때 무엇을 선택해야 하는가
DynamoDB 테이블을 설계하다 보면 반드시 마주치는 상황이 있다. 기본 키로는 조회가 잘 되는데, 다른 속성으로도 쿼리해야 하는 요구사항이 생기는 것이다. 이때 LSI(Local Secondary Index)와 GSI(Global Secondary Index) 중 무엇을 써야 하는지 판단하지 못하면, 나중에 테이블을 통째로 재설계해야 하는 상황이 온다.
TL;DR: LSI vs GSI 핵심 비교
| 항목 | LSI | GSI |
|---|---|---|
| 파티션 키 | 기본 테이블과 동일 | 다른 속성 지정 가능 |
| 정렬 키 | 다른 속성으로 변경 가능 | 선택적으로 지정 |
| 생성 시점 | 테이블 생성 시에만 가능 | 언제든지 추가/삭제 가능 |
| 일관성 읽기 | 강력한 일관성 지원 | 최종적 일관성만 지원 |
| 처리량 공유 | 기본 테이블과 공유 | 독립적으로 프로비저닝 |
| 파티션당 크기 제한 | 10GB (파티션 키 값 기준) | 제한 없음 |
| 테이블당 최대 개수 | 5개 | 20개 (기본값, 조정 가능) |
DynamoDB 인덱스가 동작하는 방식
LSI와 GSI를 선택하기 전에, 두 인덱스가 내부적으로 어떻게 다른지 이해해야 한다. 잘못된 선택은 단순히 성능 문제로 끝나지 않는다. LSI는 테이블 생성 후 추가할 수 없기 때문에, 나중에 필요하다는 걸 알았을 때는 이미 늦다.
LSI: 같은 파티션 안에서의 다른 정렬 방식
LSI는 기본 테이블과 동일한 파티션 키를 사용한다. 달라지는 것은 정렬 키뿐이다. 물리적으로 LSI 데이터는 기본 테이블과 같은 파티션에 함께 저장된다. 이 구조 때문에 파티션 키 값 하나에 해당하는 모든 데이터(기본 테이블 + LSI)의 합산 크기가 10GB를 초과할 수 없다는 제약이 생긴다.
같은 파티션에 있기 때문에 강력한 일관성 읽기(strongly consistent read)가 가능하다. 기본 테이블에 쓰기가 완료된 직후 LSI에서도 동일한 데이터를 읽을 수 있다.
GSI: 완전히 별도의 테이블처럼 동작
GSI는 실질적으로 별도의 테이블이다. 파티션 키와 정렬 키를 모두 자유롭게 지정할 수 있고, 기본 테이블과 비동기적으로 복제된다. 이 비동기 복제 때문에 GSI는 최종적 일관성(eventually consistent)만 보장한다. 기본 테이블에 쓰기가 완료된 직후 GSI를 쿼리하면, 아직 복제가 완료되지 않아 이전 값이 보일 수 있다.
기본 테이블 쓰기 스로틀링
- 기본 테이블 쓰기: 아이템이 기본 테이블 파티션에 저장된다.
- LSI 동기 반영: LSI는 같은 파티션에 있으므로 쓰기와 동시에 반영된다. 강력한 일관성 읽기가 가능한 이유다.
- GSI 비동기 복제: GSI는 별도 파티션으로 비동기 복제된다. 복제 지연이 발생할 수 있으며, 이 구간에서 쿼리하면 오래된 데이터가 반환될 수 있다.
- GSI 쓰기 용량: GSI로의 복제도 GSI의 쓰기 용량을 소비한다. 프로비저닝 모드에서는 GSI WCU가 부족하면 기본 테이블 쓰기가 스로틀링될 수 있다.
LSI vs GSI: 어떤 인덱스를 선택해야 하는가
결정 기준은 단순하다. 파티션 키가 달라져야 하는가, 아닌가. 파티션 키가 동일하고 정렬 키만 바꾸면 되는 쿼리라면 LSI가 후보다. 파티션 키 자체를 다른 속성으로 바꿔야 한다면 GSI만 가능하다.
변경해야 하는가?"}; B -- "예" --> C["GSI 사용"]; B -- "아니오
(정렬 키만 변경)" --> D{"테이블이 이미
생성되었는가?"}; D -- "예" --> E["GSI 사용 (LSI 추가 불가)"]; D -- "아니오" --> F{"강력한 일관성
읽기가 필요한가?"}; F -- "예" --> G["LSI 고려"]; F -- "아니오" --> H{"파티션 크기가
10GB 미만 확실?"}; H -- "예" --> G; H -- "아니오 / 불확실" --> I["GSI 사용"]; style C fill:#f96,stroke:#c63 style E fill:#f96,stroke:#c63 style I fill:#f96,stroke:#c63 style G fill:#6af,stroke:#36c
- 파티션 키가 달라져야 한다면 GSI 외에 선택지가 없다.
- 파티션 키가 같고 정렬 키만 달라지는 경우, 강력한 일관성이 필요하거나 파티션 크기가 10GB 미만으로 유지될 것이 확실하다면 LSI를 고려한다.
- 테이블 생성 이후에 인덱스를 추가해야 한다면 GSI만 가능하다.
- 대부분의 실제 운영 환경에서는 GSI가 더 유연하다.
실제 설계 예시: 주문 테이블
전자상거래 주문 테이블을 예로 들어보자. 기본 키는 customerId(파티션 키) + orderId(정렬 키)다.
요구사항 1: 특정 고객의 주문을 날짜순으로 조회
파티션 키(customerId)는 동일하고, 정렬 키를 orderId 대신 orderDate로 바꾸면 된다. LSI가 적합하다. 단, 테이블 생성 시점에 정의해야 한다.
요구사항 2: 주문 상태별로 전체 주문 조회
파티션 키를 orderStatus로 바꿔야 한다. 기본 테이블의 파티션 키(customerId)와 다르므로 GSI만 가능하다.
🔽 테이블 및 인덱스 생성 CLI 예시 (클릭하여 펼치기)
# 테이블 생성 시 LSI 포함 (테이블 생성 이후에는 LSI 추가 불가)
aws dynamodb create-table \
--table-name Orders \
--attribute-definitions \
AttributeName=customerId,AttributeType=S \
AttributeName=orderId,AttributeType=S \
AttributeName=orderDate,AttributeType=S \
AttributeName=orderStatus,AttributeType=S \
--key-schema \
AttributeName=customerId,KeyType=HASH \
AttributeName=orderId,KeyType=RANGE \
--local-secondary-indexes '[{
"IndexName": "CustomerOrderDateIndex",
"KeySchema": [
{"AttributeName": "customerId", "KeyType": "HASH"},
{"AttributeName": "orderDate", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "ALL"}
}]' \
--billing-mode PAY_PER_REQUEST \
--region us-east-1
# 기존 테이블에 GSI 추가 (PAY_PER_REQUEST 모드 테이블 — ProvisionedThroughput 불필요)
aws dynamodb update-table \
--table-name Orders \
--attribute-definitions \
AttributeName=orderStatus,AttributeType=S \
AttributeName=orderDate,AttributeType=S \
--global-secondary-index-updates '[{
"Create": {
"IndexName": "OrderStatusDateIndex",
"KeySchema": [
{"AttributeName": "orderStatus", "KeyType": "HASH"},
{"AttributeName": "orderDate", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "ALL"}
}
}]' \
--region us-east-1
GSI 추가 후 인덱스 상태를 확인한다. IndexStatus가 ACTIVE가 될 때까지 쿼리에 사용할 수 없다.
aws dynamodb describe-table \
--table-name Orders \
--query 'Table.GlobalSecondaryIndexes[*].{Name:IndexName,Status:IndexStatus}' \
--region us-east-1
인덱스 쿼리 실행
인덱스를 만들었다고 자동으로 사용되지 않는다. 쿼리 시 --index-name을 명시해야 한다.
# LSI 쿼리: 특정 고객의 주문을 날짜 범위로 조회
aws dynamodb query \
--table-name Orders \
--index-name CustomerOrderDateIndex \
--key-condition-expression 'customerId = :cid AND orderDate BETWEEN :start AND :end' \
--expression-attribute-values '{
":cid": {"S": "user-123"},
":start": {"S": "2024-01-01"},
":end": {"S": "2024-12-31"}
}' \
--region us-east-1
# GSI 쿼리: 특정 상태의 주문을 날짜순으로 조회
aws dynamodb query \
--table-name Orders \
--index-name OrderStatusDateIndex \
--key-condition-expression 'orderStatus = :status AND orderDate > :date' \
--expression-attribute-values '{
":status": {"S": "PENDING"},
":date": {"S": "2024-06-01"}
}' \
--region us-east-1
운영에서 자주 겪는 실수: GSI 스로틀링 오진
증상은 이렇다. 기본 테이블 쓰기가 간헐적으로 ProvisionedThroughputExceededException을 던진다. CloudWatch에서 기본 테이블의 WCU 사용률을 보면 여유가 있다. 그래서 테이블 용량을 늘려도 문제가 해결되지 않는다.
실제 원인은 GSI의 WCU 부족이었다. 프로비저닝 모드에서 GSI는 독립적인 쓰기 용량을 가진다. 기본 테이블에 아이템을 쓰면 GSI로의 복제도 GSI의 WCU를 소비한다. GSI WCU가 소진되면 기본 테이블 쓰기 자체가 스로틀링된다. 기본 테이블 메트릭만 보면 원인을 찾을 수 없다.
GSI는 별도 테이블처럼 동작한다. 기본 테이블 용량을 늘렸다고 GSI 용량이 함께 늘어나지 않는다. 두 곳을 독립적으로 모니터링해야 한다.
GSI별 스로틀링 메트릭을 확인하는 방법은 다음과 같다.
aws cloudwatch get-metric-statistics \
--namespace AWS/DynamoDB \
--metric-name WriteThrottleEvents \
--dimensions \
Name=TableName,Value=Orders \
Name=GlobalSecondaryIndexName,Value=OrderStatusDateIndex \
--start-time 2024-01-01T00:00:00Z \
--end-time 2024-01-01T01:00:00Z \
--period 300 \
--statistics Sum \
--region us-east-1
이 메트릭에서 스로틀이 잡히면, 문제는 기본 테이블이 아니라 GSI에 있다. PAY_PER_REQUEST 모드로 전환하거나 GSI의 프로비저닝 용량을 별도로 늘려야 한다.
Projection 설계: 자주 간과되는 비용 요소
인덱스를 만들 때 ProjectionType을 지정한다. ALL, KEYS_ONLY, INCLUDE 중 하나다. ALL로 설정하면 기본 테이블의 모든 속성이 인덱스에 복제된다. 편리하지만 스토리지 비용이 두 배가 된다.
쿼리에서 실제로 필요한 속성만 INCLUDE로 지정하면 스토리지와 읽기 비용을 줄일 수 있다. 단, 인덱스에 없는 속성을 가져오려면 기본 테이블로 추가 조회(fetch)가 발생하고, 이는 읽기 비용과 지연을 증가시킨다. 트레이드오프를 명확히 이해하고 선택해야 한다.
LSI vs GSI 선택 기준 요약 및 다음 단계
DynamoDB 인덱스 설계에서 LSI와 GSI의 선택은 단순한 기능 비교가 아니다. LSI는 테이블 생성 이후 추가할 수 없다는 점이 가장 중요한 제약이다. 설계 단계에서 모든 쿼리 패턴을 미리 파악하지 못했다면, 나중에 유연하게 대응할 수 있는 GSI 중심으로 설계하는 것이 실용적이다.
운영 중인 테이블에 GSI를 추가할 때는 인덱스 빌드 시간 동안 기존 쿼리에 영향이 없는지, 그리고 프로비저닝 모드라면 GSI 용량을 별도로 설정했는지 반드시 확인해야 한다.
- AWS 공식 문서: Local Secondary Indexes
- AWS 공식 문서: Global Secondary Indexes
- AWS 공식 문서: DynamoDB Best Practices
핵심 용어 정리
| 용어 | 설명 |
|---|---|
| LSI (Local Secondary Index) | 기본 테이블과 동일한 파티션 키를 사용하며, 정렬 키만 다르게 지정하는 인덱스. 테이블 생성 시에만 정의 가능. |
| GSI (Global Secondary Index) | 파티션 키와 정렬 키를 자유롭게 지정할 수 있는 인덱스. 언제든지 추가/삭제 가능하며 별도 처리량을 가짐. |
| Projection | 인덱스에 복제할 속성의 범위. ALL, KEYS_ONLY, INCLUDE 중 선택. |
| 강력한 일관성 읽기 | 쓰기 완료 직후 최신 데이터를 반환하는 읽기 방식. LSI에서만 지원. |
| 최종적 일관성 | 쓰기 후 짧은 지연 이후 데이터가 반영되는 읽기 방식. GSI의 기본 동작. |
댓글
댓글 쓰기