Best Practices가 되고 싶은 포스팅. (수정 중)
NestJs에서 dynamoDB로 서비스 구축하기
먼저 설계를 위한 개념을 잡기 위해 아래의 유튜브 영상을 시청했다.
인덱스 디자인을 위해서 GSI, LSI 의 동작 방식, 파티셔닝에 대해 공부해야할 것 같아서
데이터 중심 애플리케이션 설계 책의 6. 파티셔닝 을 공부했다.
정리한 github 링크
GSI
글로벌 세컨더리 인덱스.
쓰기 갱신이 어렵고 읽기에 효율적이다. 1개의 테이블 단위로 전역에서 관리되며 인덱스에 걸리는 파티션에만 요청을 보내 읽기가 가능하다.
LSI
로컬 세컨더리 인덱스.
쓰기 갱신이 용이하다. 1개의 파티션 단위로 인덱스가 각 파티션 자체에 저장된다.
읽기 요청시 모든 파티션에 (스캐터/개더 방식)으로 요청을 보내야 한다.
매번 AWS 콘솔에서 dynamoDB를 세팅할 수 없기에 로컬에서 개발하기 위한 환경구축이 필요하다.
위의 블로그의 방법대로 하면 간편하다.
중간에 AWS NoSQL Workbench에서 확인하는 Credential은 코드에서 그대로 사용하게 된다.
docker pull amazon/dynamodb-local
docker run -d -p 8000:8000 amazon/dynamodb-local
AWS NoSQL Workbench 다운받고 실행
로컬 커넥션 생성
Data Modeler에서 테이블 설계
로컬 커넥션에 데이터 모델 커밋
테이블 생성 완료
로컬 커넥션의 View Credentials 에 나오는 정보 복사해두기.
큰 단점: LSI 생성 불가.
pk+sk 조합으로 설계할 거고 속성 이름도 pk, sk로 사용한다.
여기에 어떤 값이 들어갈 것인가가 설계의 핵심히고 typeDorm에서 필수적이다.
우선, typeDORM에서 쓰일 entity라는 개념이 있다.
아이템의 스키마를 지정하는 것이라고 생각하면 될 것 같다.
하나의 테이블에는 여러 엔티티가 들어갈 수 있는데, 그러면 엔티티별로 파티셔닝이 이루어지도록 유도해야 애플리케이션 레벨에서 CRUD에 효율적일 것이다.
그래서 pk는 entity 이름이 디폴트로 들어가도록 설계했다. (ex. pk: 'entity_a')
sk는 uuid로 이루어진 id 값을 붙인다. (ex. sk: 'entity_a#1604b772-adc0-4212-8a90-81186c57f598')
이유는 두 가지 이다.
1. (pk+sk) 조합은 유일성이 보장되어야한다. 복합 기본키이기 때문,
2. update, delete 요청을 entity의 id 값으로 할 것이기 때문.
보조 인덱스는 기존 속성을 그대로 사용할 수도 있고 새로운 속성으로 해시값을 할당할 수 있다.
GSI의 pk는 검색에 필수적으로 사용하게될 속성을 조합한 해시값으로 지정했다.
ex) 속성: 'gsi1pk', 값: 'userId#123#type#100'
애플리케이션 단에서는 userId와 type 속성을 사용하지만 실제 dynamoDB에 물리적으로는 새로운 속성으로 해싱된 값이 저장되고, 인덱스 역할을 하게 된다.
GSI의 sk: createdAt을 사용 'createdAt#1234567890'.
typeDORM에서는 Date.now()를 1000으로 나눈 값을 사용한다.
LSI 는 sk 만 사용할 수 있다.
새로운 속성을 커스텀할 필요 없이 기존 속성을 그대로 사용할 수 있다.
createdAt 속성을 그대로 사용했다.
정렬, 범위검색 등이 자주 필요할 것 같아 createdAt을 그대로 sort key로 사용했다.
이유는
1. 모든 파티션에 요청이 가게될 경우에 효율적인 인덱스이기 때문이다. (스캐터/개더) 전체 조회를 하더라도 시간에 대한 질의는 늘 필요하다. (정렬, 기간 조회)
2. 모든 엔티티에 공통적인 속성이기 때문이다. 엔티티 구분없이 모든 데이터를 범위 질의, 정렬 등이 필요할 때를 고려한다.
공식문서 의 내용을 꼼꼼히 읽고 따라했다.
Table 객체에 value 들은 실제 속성(컬럼) 이름을 지정하는 것이다.
Entity의 options 에 있는 value 들은 index로 지정한 속성의 값으로 들어갈 포맷이다.
테이블 선언
import { INDEX_TYPE, Table } from '@typedorm/common'
export const isntkyuTable = new Table({
name: 'isntkyu',
partitionKey: 'pk',
sortKey: 'sk',
indexes: {
gsi1: {
partitionKey: 'gsi1pk',
sortKey: 'gsi1sk',
type: INDEX_TYPE.GSI
},
lsi1: {
sortKey: 'createdAt',
type: INDEX_TYPE.LSI
}
}
})
엔티티
import {
Attribute,
AutoGenerateAttribute,
AUTO_GENERATE_ATTRIBUTE_STRATEGY,
Entity,
INDEX_TYPE
} from '@typedorm/common'
@Entity<TEST>({
name: 'TEST',
primaryKey: {
partitionKey: 'TEST',
sortKey: 'TEST#{{testId}}'
},
indexes: {
gsi1: {
partitionKey: 'userId#{{userId}}#type#{{type}}',
sortKey: 'createdAt#{{createdAt}}',
type: INDEX_TYPE.GSI
},
lsi1: {
sortKey: {
alias: 'createdAt'
},
type: INDEX_TYPE.LSI
}
}
})
export class TEST {
@Attribute()
pk = 'TEST'
@AutoGenerateAttribute({
strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4
})
testId: string
@Attribute()
userId: number
@Attribute({
default: () => false
})
isDeleted: boolean
@Attribute()
deletedAt: number
@AutoGenerateAttribute({
strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.EPOCH_DATE,
autoUpdate: false
})
createdAt: number
@AutoGenerateAttribute({
strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.EPOCH_DATE,
autoUpdate: true
})
updatedAt: number
}
*주의할 점
속성을 그대로 인덱스로 사용하려면 alias 옵션에 속성명을 넣어줘야 한다.
primary key, gsi, lsi 각 인덱스에서 같은 속성(createdAt)을 그대로 인덱스를 사용하려고 하면 안된다.
여러 인덱스가 같은 속성을 사용하도록 하면 put Item 요청시 Duplicate 에러가 난다. (typedorm 단)
나는 lsi 는 createdAt를 그대로 사용하는 인덱스로 지정했다.
@Attribute() 데코레이터 옵션중에 default 속성이 있는데 string, number, boolean이 들어갈 수 있다.
string, number는 그냥 리터럴 값을 넣어도 되는데, boolean값은 콜백함수를 넣어야 동작함.
soft delete를 위한 속성을 추가했고, hard delete는 dynamoDB의 ttl 을 설정할 것이다.
typeDOrm의 entitymanager를 의존성 주입하기 위해
DynamicModule과 Custom Decorator를 구현해서 사용했다.
공식문서의 내용을 잘 따라하면 될 것 같고
find, count (many select API)와
페이징기능 사용법에 대해서만 설명하겠다.
find, count 사용법
사용법은 같습니다.
첫 번쨰 파라미터는 엔티티 클래스를 넣는다.
두 번째 파라미터는 세 번쨰 파라미터의 queryIndex로 사용할 인덱스에 맞는 속성 조건을 넣는다.
primary key나 다른 속성으로 조회하고자 하면 세 번쨰 파라미터에 queryIndex를 사용하지 않으면 된다.
const { items, cursor } = await this.testEntityMananer.find<Test>(
Test,
{
userId: userId,
type: type,
isDeleted: false
},
{
queryIndex: 'gsi1',
cursor: dto.cursor,
keyCondition: {
GT: timestampOneMonthAgo
},
limit: limit,
orderBy: QUERY_ORDER.DESC
}
)
일반적인 페이지네이션 조회 요청이다.
const { items, cursor } = await this.testEntityMananer.find<Test>(
Test,
{
userSeq: dto.userId,
type: dto.type
},
{
queryIndex: 'gsi1',
cursor: dto.cursor,
keyCondition: {
GT: dto.timestamp
},
limit: dto.limit,
orderBy: QUERY_ORDER.DESC
}
)
쿼리옵션의 cursor 값으로 undefined을 넘겨주면 첫페이지의 데이터가 나온다
마지막 페이지의 응답에는 cursor 속성이 없다.(undefined)
다음페이지를 가르킬 cursor 객체는 primary key와 사용한 인덱스(gsi1) 의 속성들이 들어간다.
queryIndex를 사용하지 않던 사용하던 간에 dynamoDB의 query 질의에는 필터 옵션이 사용 가능하다.
AWS 콘솔에는 필터
라고 써있고 조건을 걸 수 있다.
typeDORM에서 필터를 사용하려면 where 절을 사용하면 된다.
index 조건은 keyCondition
filter는 where
const { items, cursor } = await this.testEntityMananer.find<Test>(
Test,
{
//...
},
{
//...,
where: {
}
}
)
...