67장. DynamoDB 인덱스 — LSI · GSI
이 장에서 말하고자 하는 것
앞 장에서 우리는 DynamoDB가
“파티션 키 + 정렬 키” 로 데이터를 꺼낸다는 걸 봤다.
그런데 운영을 하다 보면 같은 데이터에 대해
다른 키로 조회하고 싶을 때 가 생긴다.
orders 테이블
Primary Key: (user_id, created_at)
질문 1: "u-1 사용자의 주문 목록" → Primary Key로 가능
질문 2: "status='paid' 인 주문 목록" → Primary Key로 불가능
질문 3: "상품 id로 주문 검색" → 불가능
이걸 푸는 도구가
인덱스 (Index)
다.
DynamoDB는 두 종류의 인덱스를 제공한다.
- LSI (Local Secondary Index)
- GSI (Global Secondary Index)
1. 큰 그림
테이블의 Primary Key = "주된 조회 경로"
인덱스(LSI / GSI) = "추가 조회 경로"
인덱스는 원본 테이블의 읽기 전용 사본 처럼 동작한다.
DynamoDB가 자동으로 동기화해준다.
2. LSI — Local Secondary Index
같은 파티션 키를 공유하면서
다른 정렬 키 로 한 번 더 보고 싶을 때.
원본: (user_id, created_at)
LSI: (user_id, total)
"u-1의 주문을 금액 큰 순으로" → LSI로 Query
특징:
- 파티션 키는 원본과 같다
- 테이블 만들 때만 생성 가능 (나중에 추가 불가)
- 같은 파티션 키 안에서 최대 10GB
- 사용 빈도가 점점 줄어들고 있다 (GSI가 더 유연)
3. GSI — Global Secondary Index
완전히 다른 파티션 키 + 정렬 키 로 한 번 더 본다.
원본: (user_id, created_at)
GSI: (status, created_at)
"status='paid' 인 주문을 최신순으로" → GSI로 Query
특징:
- 파티션 키도 정렬 키도 자유롭게 선택
- 언제든 추가 · 삭제 가능
- 별도의 처리량 / 비용
- 비동기 복제 (약간의 지연 가능)
운영에서 추가 조회 경로가 필요하면 거의 항상 GSI
4. 인덱스에 무엇을 담을지
GSI를 만들 때 프로젝션(projection) 을 고른다.
| 프로젝션 | 담는 데이터 | 비용 |
|---|---|---|
| KEYS_ONLY | 키 + 인덱스 키만 | 가장 쌈 |
| INCLUDE | 키 + 지정 속성 | 보통 |
| ALL | 모든 속성 | 가장 비쌈, 가장 편함 |
매번 원본 테이블을 다시 GetItem 해야 한다면 ALL을 검토 — IO 절감
5. 인덱스 비용 모델
GSI는
- 별도의 쓰기 용량 (원본에 쓸 때 인덱스에도 쓰기)
- 별도의 읽기 용량
- 별도의 스토리지
를 가진다.
인덱스 한 개 추가 = 쓰기 처리량이 ~2배가 될 수 있음
설계 단계에서 인덱스 수를 줄이는 게 중요하다.
6. “Single Table Design” 의 핵심
DynamoDB 고급 패턴 중 하나.
한 테이블 안에 여러 종류의 엔티티를 담고
GSI로 다양한 접근 경로를 만든다
PK = "USER#u-1" SK = "PROFILE" → 사용자 프로필
PK = "USER#u-1" SK = "ORDER#o-1" → 주문
PK = "USER#u-1" SK = "ORDER#o-2" → 주문
PK = "ORDER#o-1" SK = "META" → 주문 메타
이렇게 키를 설계해 “한 번에 사용자 + 모든 주문” 같은 조회를 단일 Query로 끝낸다.
초보자에게 권장하기는 어려운 패턴이지만,
DynamoDB가 진가를 발휘하는 구간이다.
7. 우리 서비스에서
[DynamoDB "orders"]
PK: user_id
SK: created_at
GSI: "by-status"
PK: status (paid, shipped, cancelled)
SK: created_at
GSI: "by-product"
PK: product_id
SK: created_at
- 기본 조회: 사용자별 주문
- “결제 완료 주문 큐” → GSI by-status
- “이 상품 누가 샀나” → GSI by-product
8. 직접 확인해보기 — CLI
GSI 추가
aws dynamodb update-table \
--table-name orders \
--attribute-definitions AttributeName=status,AttributeType=S AttributeName=created_at,AttributeType=S \
--global-secondary-index-updates \
'[{"Create":{"IndexName":"by-status","KeySchema":[{"AttributeName":"status","KeyType":"HASH"},{"AttributeName":"created_at","KeyType":"RANGE"}],"Projection":{"ProjectionType":"ALL"}}}]'
GSI로 Query
aws dynamodb query \
--table-name orders \
--index-name by-status \
--key-condition-expression "#s = :s" \
--expression-attribute-names '{"#s": "status"}' \
--expression-attribute-values '{":s": {"S": "paid"}}'
9. 코드로는 이렇게 생겼다 — Terraform
resource "aws_dynamodb_table" "orders" {
name = "orders"
billing_mode = "PAY_PER_REQUEST"
hash_key = "user_id"
range_key = "created_at"
attribute {
name = "user_id"
type = "S"
}
attribute {
name = "created_at"
type = "S"
}
attribute {
name = "status"
type = "S"
}
attribute {
name = "product_id"
type = "S"
}
global_secondary_index {
name = "by-status"
hash_key = "status"
range_key = "created_at"
projection_type = "ALL"
}
global_secondary_index {
name = "by-product"
hash_key = "product_id"
range_key = "created_at"
projection_type = "ALL"
}
}
10. 이렇게 쓰면 망한다 — 안티패턴
안티패턴 1. 모든 컬럼마다 GSI를 만든다
쓰기 비용이 곱절로 늘어난다.
정말 필요한 조회 경로만 인덱스로
안티패턴 2. GSI에 카디널리티 낮은 키 (예: gender)
hot partition으로 이어진다.
안티패턴 3. GSI의 일관성에 강한 기대를 한다
GSI는 비동기 복제다 — 약간 지연될 수 있다.
강한 일관성이 필요한 조회는 원본 테이블 Query
안티패턴 4. LSI를 새로 시작하는 테이블에 우선적으로 쓴다
LSI는 제약이 많다 (생성 후 추가 불가, 10GB 한도).
GSI 가 거의 항상 더 유연하다.
11. 한 줄로 정리
GSI는 DynamoDB에서 추가 조회 경로를 만드는 도구이며,
비용·지연을 의식하면서 “정말 필요한 인덱스만” 두는 게 핵심이다
12. 이 장의 핵심 정리
- LSI는 같은 파티션 키 + 다른 정렬 키.
- GSI는 완전히 다른 키 조합 — 더 유연하다.
- 인덱스는 원본의 읽기 전용 사본처럼 동작한다.
- 인덱스 하나가 쓰기 처리량을 ~2배로 늘릴 수 있다.
- GSI는 비동기 복제다 — 강한 일관성이 필요하면 원본을 쓴다.
- Single Table Design은 강력하지만 학습 곡선이 있다.