AWS DynamoDB 사용해보기
2019-12-17 00:26:06

DynamoDB란?

AWS에서 개발한 key-value형 NoSQL 데이터베이스

사용하면서 느꼈었던 장점

  • 별도로 설치나 버전 관리 등을 할 일이 없어서 편리 했었다.
  • 저장 공간 용량이 제한이 없기 때문에 신경 안 썼었다.
  • 사용량 만큼 요금이 부과 된다.

사용하면서 느꼈었던 단점

  • 여러 데이터를 삽입할 때 (bulk insert) 개수 제한이 있어서 조금 불편 했었다.
  • 보조 인덱스 생성 시 데이터 수가 많지 않은데 꽤 느린 편이다.

테이블

DynamoDB를 사용 하기전에 테이블이 존재 하기 때문에 생성을 해 줘야 한다.

웹 콘솔 기준 테이블 작성 시 나오는 항목을 아는 대로 정리를 해 본다.

테이블은 한 번 만들면 수정이 안 되므로 어떤 데이터를 다룰지에 따라 구성이 다르기 때문에

신중히 생각을 한 다음 만들어야 한다.

테이블 이름

말 그대로 해당 테이블의 이름을 작성 해야 한다.

테이블명은 해당 리전에서는 중복된 이름으로는 만들 수 없기 때문에 고유해야 한다.

예료 서울 리전에서 users란 테이블을 생성 시 서울에서는 user란 테이블을 새로 못 만든다.

서울 리전에서 users가 있지만 다른 리전(도쿄)같은 경우에는 users 테이블 생성 가능

기본 키 (파티션 키)

DynamoDB는 key-value의 구조다.

기본 키는 key에 해당 하기 때문에 고유한 데이터가 들어올 항목으로 만들어야 한다.

데이터를 삽입 할 때 필수적으로 들어 가야 한다.

예로 이전에 쓰던 서비스에서는 유저 테이블을 생성 시 기본 키는 user_id로 만들었었다.

정렬 키

정렬 키는 어떠한 데이터를 다룰 지에 따라 선택 / 미선택을 하면 된다.

내가 느끼기엔 second key 같은 느낌인데 예로 유저의 행동을 모두 기록하는 테이블을 생성 시

기본 키만 생성한 경우에는 user_id가 a의 행동은 1회만 삽입을 할 수 있다.

왜냐면 다음 번 삽입 시 user_id에 a가 등록되어 있기 때문이다.

이러한 경우에 정렬 키를 추가하여 사용하면 가능 한데 예로 정렬 키를 timestamp로 만들어서

삽입을 하면 아래의 테이블처럼 사용을 할 수 있게 된다.

user_id(기본 키) timestamp(정렬 키) action
a 1 ‘login’
a 2 ‘logout’

대신 정렬 키를 추가 하게 되면 이후 삽입, 조회, 삭제 요청 시 정렬 키의 데이터를 필수적으로

넣어야 한다. 그러므로 꼭 필요한 경우에만 구성에 포함 하자.

보조 인덱스

key-value 구조이기 때문에 기본적으로 조회 시 기본 키 또는 기본 + 정렬 키로만 검색이

가능한데 예로 아래의 테이블에서 유저의 login한 액션만 조회를 하고 싶을 때

보조 인덱스를 추가해서 이용을 하면 된다.

user_id(기본 키) timestamp(정렬 키) action
a 1 ‘login’
a 2 ‘logout’
b 3 ‘login’

보조 인덱스도 마찬가지로 기본 키 그리고 선택 사항으로 정렬 키를 추가할 수 있다.

읽기/쓰기 용량 모드

읽기 용량 / 쓰기 용량 설정에 따라 사용 가격의 구성이 정해 진다.

이 용량은 사용 회수라 생각을 하고 설정을 했었다. 유저가 많아져서 사용자가 많아 지면

훅 올라가고, 사용자가 떨어 지면 자연스레 내려가고…

읽기 용량은 올려도 가격이 많이 변화가 없는데 쓰기 용량은 좀 만 올려도 가격이 뻥튀기 된다.

처음 DynamoDB 사용할 땐 온디멘드 모드가 없어서 용량 크기에 좀 불안 했었는데

추가가 되어서 서비스 때 변경을 했었다. 온디멘드 최고!

오토스케일링

사용 수가 많아지면 설정에 따라 알아서 늘어나고 하는데 이 부분은 크게 신경을 안 써봤다.

온디멘드라면 건드릴 일도 없으니 패스

CRUD

Node.js로 진행해보겠다.

해당 프로젝트 폴더에서 npm을 통해 aws-sdk를 다운 받는다.

1
npm i aws-sdk -S

Create

Put

하나의 row를 삽입 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
TableName: 'users', // 테이블명
Item: {
user_id: 'a', // 기본 키
ts: Date.now(), // 정렬 키, 존재 하지 않으면 생략 가능
action: 'login', // 이후로는 추가하고 싶은 데이터들
// ...
},
};

dynamo.put(params).promise().then((result) => {
console.log(result); // {} 삽입 성공 시 넘어오는 데이터 X
}).catch((e) => {
console.log('err', e);
});

BatchWrite

한 번에 row를 여러 개 넣을 수 있는 방법, 그러나 25개 이하만 가능 하다..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
RequestItems: {
users: [ // 배열의 이름은 테이블명이다.
{
PutRequest: {
Item: { // put과 마찬가지로 기본 키, 정렬 키, 이하 추가하고 싶은 항목
user_id: 'b',
ts: Date.now(),
action: 'logout',
},
},
},
{
PutRequest: {
Item: {
user_id: 'a',
ts: Date.now(),
action: 'charge',
},
},
},
{
PutRequest: {
Item: {
user_id: 'd',
ts: Date.now(),
action: 'login',
},
},
}
],
},
};

dynamo.batchWrite(params).promise().then((result) => {
console.log(result); // { UnprocessedItems: {} }
}).catch((e) => {
console.log('err', e);
});

Read

Get

하나의 row를 가지고 온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
TableName: 'users', // 테이블명
Key: {
user_id: 'a', // 기본 키
ts: 1597722895588, // 정렬 키, 존재 하지 않으면 생략 가능
},
};

dynamo.get(params).promise().then((result) => {
console.log(result);
/*
{ Item: { ts: 1597722895588, user_id: 'a', action: 'login' } }

데이터가 존재 하지 않을 시에는 {}
*/
}).catch((e) => {
console.log('err', e);
});

BatchGet

한 번에 여러 row를 가지고 올 수 있는 방법, 이 역시 개수 제한 100개다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
RequestItems: {
users: { // 테이블명
Keys: [
{
user_id: 'a', // 기본 키
ts: 1597722895588, // 정렬 키, 없으면 생략 가능
},
{
user_id: 'b',
ts: 1597723408876,
}
],
},
},
};

dynamo.batchGet(params).promise().then((result) => {
console.log(result);
/*
하나 라도 데이터가 존재 시
{ Responses: { users: [ [Object], [Object] ] }, UnprocessedKeys: {} }

모두 없을 시
{ Responses: { users: [] }, UnprocessedKeys: {} }

*/
}).catch((e) => {
console.log('err', e);
});

Query

조건을 넣어서 검색을 할 수 있다.

기본 키의 경우에는 equal(=)만 되고 정렬 키가 있는 경우 부등호(>, >= 등등)가 가능 하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
TableName: 'users', // 테이블명
KeyConditionExpression: '#x = :x and #y > :y', // 조건문
ExpressionAttributeNames: { // #은 키의 alias라 생각하면 됨.
'#x': 'user_id', // x는 user_id
'#y': 'ts', // y는 타임스탬프
},
ExpressionAttributeValues: { // :은 값의 alias
':x': 'a', // 검색할 user_id
':y': 10000, // 검색할 ts
}
};

dynamo.query(params).promise().then((result) => {
console.log(result);
/*
데이터가 1개라도 있을 시에는
{
Items: [
{ ts: 1597722895588, user_id: 'a', action: 'login' },
{ ts: 1597723408876, user_id: 'a', action: 'charge' }
],
Count: 2,
ScannedCount: 2
}
존재 하지 않을 시에는
{ Items: [], Count: 0, ScannedCount: 0 }
*/
}).catch((e) => {
console.log('err', e);
});

보조인덱스를 이용한 쿼리

위에 적은 거 처럼 action을 보조 인덱스로 만들어서 검색을 해 본다.

기본 키는 action 정렬 키는 생략 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
TableName: 'users', // 테이블명
IndexName: 'action-index', // 보조 인덱스명
KeyConditionExpression: '#x = :x', // 조건문
ExpressionAttributeNames: { // #은 키의 alias라 생각하면 됨.
'#x': 'action', // x는 action
},
ExpressionAttributeValues: { // :은 값의 alias
':x': 'login', // 검색할 action
}
};

dynamo.query(params).promise().then((result) => {
console.log(result);
/*
{
Items: [
{ ts: 1597723408876, action: 'login', user_id: 'd' },
{ ts: 1597727204965, action: 'login', user_id: 'z' }
],
Count: 2,
ScannedCount: 2
}
*/
}).catch((e) => {
console.log('err', e);
});

Scan

데이터를 모두 검색 할 수 있는 방법 혹여나 해서 Filter는 검색하기 위한 조건이 아닌

모든 데이터를 읽은 후에 조건대로 걸러지는 것이기 때문에 검색 속도와는 전혀 상관이 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
TableName: 'users', // 테이블명, 이하 모든 프로퍼티는 생략 가능
// Limit: 100, // 개수 제한
// FilterExpression: '#action = :action', // 필터링
// ExpressionAttributeNames: {
// '#action': 'action'
// },
// ExpressionAttributeValues: {
// ':action': 'login'
// },
};

dynamo.scan(params).promise().then((result) => {
console.log(result);
/*
{
Items: [
{ ts: 1597723408876, user_id: 'b', action: 'logout' },
{ ts: 1597723408876, user_id: 'd', action: 'login' },
{ ts: 1597722895588, user_id: 'a', action: 'login' },
{ ts: 1597723408876, user_id: 'a', action: 'charge' }
],
Count: 4,
ScannedCount: 4
}
*/
}).catch((e) => {
console.log('err', e);
});

Update

데이터를 수정하는 방법, 기본 키와 정렬 키를 제외한 항목만 수정이 가능 하다.

UpdateExpression 설정에 따라 조금 다르다.

Set

흔히 쓰는 기존의 X 값을 Y 값으로 변경할 때 쓴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
TableName: 'users', // 테이블명
Key: { // 기본 키, 정렬 키(미선택 시 생략 가능)
user_id: 'a',
ts: 1597723408876
},
UpdateExpression: 'set #x = :x', // 수정할 항목 alias
ExpressionAttributeNames: {
'#x': 'action'
},
ExpressionAttributeValues: {
':x': 10
},
ReturnValues: 'ALL_NEW', // 수정한 데이터 받아오기, 생략 가능
};

dynamo.update(params).promise().then((result) => {
console.log(result); // { Attributes: { ts: 1597723408876, action: 10, user_id: 'a' } }
}).catch((e) => {
console.log('err', e);
});
ADD

수정할 항목이 숫자 값이고 N만큼 증가시킬 필요가 있을 때 사용 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
TableName: 'users', // 테이블명
Key: { // 기본 키, 정렬 키(미선택 시 생략 가능)
user_id: 'a',
ts: 1597723408876
},
UpdateExpression: 'add #x :x', // 수정할 항목 alias
ExpressionAttributeNames: {
'#x': 'action'
},
ExpressionAttributeValues: {
':x': 1
},
ReturnValues: 'ALL_NEW', // 업데이트한 데이터 받아오기, 생략 가능
};

dynamo.update(params).promise().then((result) => {
console.log(result); // { Attributes: { ts: 1597723408876, action: 11, user_id: 'a' } }
}).catch((e) => {
console.log('err', e);
});
Remove

특정 항목을 삭제할 때 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
TableName: 'users', // 테이블명
Key: { // 기본 키, 정렬 키(미선택 시 생략 가능)
user_id: 'a',
ts: 1597723408876
},
UpdateExpression: 'remove #x', // 수정할 항목 alias
ExpressionAttributeNames: {
'#x': 'action'
},
ReturnValues: 'ALL_NEW', // 업데이트한 데이터 받아오기, 생략 가능
};

dynamo.update(params).promise().then((result) => {
console.log(result); // { Attributes: { ts: 1597723408876, user_id: 'a' } }
}).catch((e) => {
console.log('err', e);
});

Delete

Delete

하나의 row를 삭제할 때 쓴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
TableName: 'users', // 테이블명
Key: { // 기본 키, 정렬 키(미선택 시 생략 가능)
user_id: 'a',
ts: 1597723408876
},
};

dynamo.delete(params).promise().then((result) => {
console.log(result); // {}
}).catch((e) => {
console.log('err', e);
});

BatchWrite

한 번에 여러 row를 삭제하는 방법, 마찬가지로 개수 제한 25개

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
RequestItems: {
users: [ // 테이블명
{
DeleteRequest: { // 이 부분이 여러 개 삽입할 때와 다르다
Key: {
user_id: 'a', // 기본 키
ts: 1597722895588, // 정렬 키, 없으면 생략 가능
},
},
},
{
DeleteRequest: {
Key: {
user_id: 'b',
ts: 1597723408876,
},
},
}
],
},
};

dynamo.batchWrite(params).promise().then((result) => {
console.log(result);
}).catch((e) => {
console.log('err', e);
});

트랜잭션

DynamoDB를 처음 사용 했을 때는 못 봤었는데 얼마 안 되어서 추가가 되었다.

실무에서는 써 보진 않음…😅

TransactGetItems, TransactWriteItems 2 가지가 존재 하는데

전자는 읽기 관련 트랜잭션, 후자는 쓰기, 수정, 삭제와 관련된 트랜잭션을 걸 수 있다.

TransactGetItems

유저 테이블에서 유저 정보를 읽어 온 후 유저가 보유한 아이템 테이블을 읽어보려 한다.

유저 테이블에서는 user_id가 문자열인데 현재 데이터 타입이 숫자라서 다르기 때문에

2번째는 실행이 되질 않고 트랜잭션 실패라는 에러를 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const getParams = {
TransactItems: [
{
Get: {
TableName: 'users',
Key: {
user_id: 1,
}
}
},
{
Get: {
TableName: 'items',
Key: {
user_id: 1,
}
}
},
],
}

dynamo.transactGet(getParams).promise().then((r) => {
console.log(JSON.stringify(r, null, 2));
}).catch((e) => {
console.log(e);
/*
TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [None, ValidationError]
*/
});

TransactWriteItems

가입 시 유저 정보를 기록 하고 아이템 테이블에 초기 값으로 포션을 지급하려 한다.

위와 마찬가지로 유저 테이블에서는 user_id가 문자열인데 현재 데이터 타입이 숫자라서

다르기 때문에 2번째는 실행이 되질 않고 트랜잭션 실패라는 에러를 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const AWS = require('aws-sdk');

AWS.config.update({
region: 'ap-northeast-2', // 서울 리전
accessKeyId: 'IAM 엑세스 키',
secretAccessKey: 'IAM 시크릿 엑세스 키',
});

const dynamo = new AWS.DynamoDB.DocumentClient();

const params = {
TransactItems: [
{
Put: {
TableName: 'users',
Item: {
user_id: 1,
}
}
},
{
Put: {
TableName: 'items',
Item: {
user_id: 1,
potion: 10,
},
}
},
],
}

dynamo.transactWrite(params).promise().then((r) => {
console.log(r);
}).catch((e) => {
console.log(e);
});

참고