scan()
query()
1MB 제한)1MB
라는 제한은 dynamoDBClient .scan(), query()
메소드 뿐만 아니라 Lambda 함수의 요청 본문 크기(json) 제한에도 적용된다.Lambda 함수가 전송할 수 있는 응답 JSON의 최대 크기는 1MB입니다.
⚠️ 2021-09-15, 실제로는 최대 6MB도 성공- AWS Document
페이지네이션을 할 때 하나의 API로 통합하여 모든 rows를 한번에 리턴하려하는 것은 좋지 않다.
❓ 한번에 다 구해서 클라이언트에게 한꺼번에 보내주면 안되나??
👉 응 안돼~ 람다 제한 (1 MB || 6MB) 라 어차피 한번에 못내보내~
사이즈 초과시 413 Payload Too Large
에러가 발생한다.
// 이렇게 한번에 모든 데이터를 Pagination 하여 이어붙이는 방식은
// Lambda 응답 크기 제한 때문에 1MB 데이터만 정상 응답되고 초과분은 사라진다.
const result = await dynamoDBClient.scan(scanParameter).promise();
const resultItems : any[] = [];
resultItems.push(result.Items);
while (result.LastEvaluatedKey) {
Object.assign(scanParameter, {ExclusiveStartKey: result.LastEvaluatedKey});
const nextPageResult = await dynamoDBClient.scan(scanParameter).promise();
// + Items 이어붙이기
resultItems.push(nextPageResult.Items);
result.LastEvaluatedKey = nextPageResult.LastEvaluatedKey;
result.ScannedCount = nextPageResult.ScannedCount;
}
return response.status(200).json({
records: resultItems,
});
LastEvaluatedKey
를 리턴한다. 이 값은 1MB 중 가장 마지막 데이터의 키이다. (페이지 구분을 위한 값이다.)const result = await dynamoDBClient.scan(scanParameter).promise();
// 1MB 짜리 데이터중 가장 마지막 인덱스의 키가 담겨있다.
const cursor = result.LastEvaluatedKey;
LastEvaluatedKey
를 ExclusiveStartKey
로 그대로 담아 쿼리를 날리면 그 다음 페이지 해당하는 부분만 반환한다. (마찬가지로 1MB 크기 내에서) const scanParameter = {
// 중략..
// 커서를 담아서 쿼리를 날린다.
ExclusiveStartKey: cursor
};
return response.status(200).json({
records: result.Items,
// 마지막 페이지일 경우 undefined, 아닐 경우 Primary Key 가 담겨있다.
cursor: result.LastEvaluatedKey
});
cursor
를 주고받을 때 직렬화와 인코딩이 필요한데
그 이유는 다음과 같다.
js에서 직렬화/역직렬화 국룰은 json 이 아닐까 싶다.
인코딩은 base64
, 직/역직렬화는 JSON
을 사용했다.
function base64Encoding(obj: object) : string {
return Buffer.from(JSON.stringify(obj), 'utf-8').toString('base64');
}
function base64Decoding(str: string) : object {
return JSON.parse(Buffer.from(str, 'base64').toString('utf-8'));
}
export {base64Encoding, base64Decoding};
클라이언트에게 데이터베이스 스키마 구조를 노출시키는 것은 바람직하지 않다.
LastEvaluatedKey 를 클라이언트에게 돌려줄 때 base64로 인코딩한 것 만으로는 충분하지 않다. base64 인것을 알아내면 복호화가 너무 쉽기 때문이다.
⚠️ DynamoDB Schema 정보가 노출될 수 있다.
(‼️ 물론, DB Schema 를 알아도 되는 사람만이 사용하는 관리자, 운영용 페이지라면 이 과정은 필수가 아닌 선택이다.)
[예시]
{
"primary_key": "pk11"
"name" : "falcon"
"datetime": "2021-06-15",
"sequence_number": 40
}
이처럼 DB의 테이블 스키마 정보를 노출시키지 않고 안전하게 LastEvaluatedKey
만 전달하기 위해 암호화한다.
여기서는 대칭키 암호화 기법인 AES를 사용했다.
어느 알고리즘을 사용할지는 개발자가 선택해야한다.
import crypto from 'crypto';
export default class AES {
// key size must be 32 bytes when to use AES 256
readonly #API_KEY: string
// algorithm 도 process.env 로 가릴 필요 있음.
readonly #ALGORITHM: string = process.env.CRYPTOGRAPHIC_ALGORITHM!;
// Lambda Scale-out 하기 전엔 이 값이 변하지 않음.
readonly #INITIAL_VECTOR = crypto.randomBytes(16);
constructor(apiKey: any) {
this.#API_KEY = apiKey;
};
public encrypt(plainText: any) : string {
const cipher = crypto.createCipheriv(this.#ALGORITHM, this.#API_KEY, this.#INITIAL_VECTOR);
// data must be a Buffer || TypedArray || DataView , inputEncoding : 'UTF-8', outputEncoding :
const bufferedData : string = cipher.update(JSON.stringify(plainText), 'utf8', 'base64');
const encryptedData : string = bufferedData.concat(cipher.final('base64'));
return encryptedData;
}
public decrypt(encryptedText: string) : string {
const decipher = crypto.createDecipheriv(this.#ALGORITHM, this.#API_KEY, this.#INITIAL_VECTOR);
const bufferedData2 = decipher.update(encryptedText, 'base64', 'utf-8');
const decryptedData = bufferedData2.concat(decipher.final('utf-8'));
return decryptedData;
}
}