DynamoDB Pagination

Falcon·2021년 6월 14일
2

aws

목록 보기
12/33
post-thumbnail

🎯 Pagination 언제 할까?

  • 1회의 쿼리로 모든 값을 가져올 수 없을 때 (scan() query() 1MB 제한)
  • 오로지 순차적으로 값을 가져올 때
  • 총 크기에 상관없이 가져오고자 할 때 (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 에러가 발생한다.

잘못된 방식 (while) 예시


// 이렇게 한번에 모든 데이터를 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,
});

시나리오

  • DynamoDBClient 메소드는 쿼리 결과가 1MB를 초과할 때 Primary Key를 담은 LastEvaluatedKey 를 리턴한다. 이 값은 1MB 중 가장 마지막 데이터의 키이다. (페이지 구분을 위한 값이다.)
const result = await dynamoDBClient.scan(scanParameter).promise();

// 1MB 짜리 데이터중 가장 마지막 인덱스의 키가 담겨있다.
const cursor = result.LastEvaluatedKey;
  • LastEvaluatedKeyExclusiveStartKey 로 그대로 담아 쿼리를 날리면 그 다음 페이지 해당하는 부분만 반환한다. (마찬가지로 1MB 크기 내에서)
    const scanParameter = {
	// 중략..
	// 커서를 담아서 쿼리를 날린다.
        ExclusiveStartKey: cursor
    };
  • 클라이언트에게 cursor 를 반환한다.
return response.status(200).json({
  records: result.Items,
  // 마지막 페이지일 경우 undefined, 아닐 경우 Primary Key 가 담겨있다.
  cursor: result.LastEvaluatedKey
});

구현 방법

cursor 를 주고받을 때 직렬화와 인코딩이 필요한데
그 이유는 다음과 같다.

  1. cursor 는 원래 일반 오브젝트 타입이다.
  2. 일반 오브젝트 타입을 Request URL 에 그대로 넣을 수 없다.
  3. 따라서 문자로 인코딩이 필요하다.
💡 직렬화 & 인코딩 (서버 -> 클라이언트)
역직렬화 & 디코딩하자. (클라이언트 -> 서버)

Step 1. 직렬화 + 인코딩

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};

Step2. 🔒 암호화

클라이언트에게 데이터베이스 스키마 구조를 노출시키는 것은 바람직하지 않다.

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;
    }
}

암호화 알고리즘 선택시 고려사항

  • 대칭키 암호화를 사용할 것인가 비대칭키 암호화를 사용할 것인가?
  • 사용자에게 API Key 전달을 어떻게 할 것인가?
  • Client 에게 Key 를 반드시 숨길 필요가 있는가?

🔗 Reference

wrong pagenation

profile
I'm still hungry

0개의 댓글