AWS 노트

AWS SDK로 TypeScript에서 Parameter Store의 파라미터를 참조하기 (비동기처리와 async, await, promise)

Jonchann 2020. 4. 25. 19:50

저번 글에서 틀렸던 것

저번 글: Prameter Store를 이용해 CDK stack에서 하드코드 없애기

 

저번 글 후보 3에 대해 aws-sdk@aws-cdk/aws-ssm을 같이 사용해야 TypeScript에서 파라미터를 참조할 수 있다고 했는데 실제로 구현해보니 그건 아니었다.
boto3와 같이 aws-sdk만 있으면 되었다.

aws-sdk.SSM().getParameter()

결론부터 말하자면 파라미터는 아래와 같이 불러온다.

import AWS = require("aws-sdk");

const ssm = new AWS.SSM();

export async function getParameterString(service: string, key: string): Promise<string> {
    const param = {
        Name: `/[project_name]/${service}/${key}`,
        WithDecryption: true
    };

    var ssmParam = await ssm.getParameter(param).promise();

    let secureParameter = 'unknown';
    if (typeof ssmParam.Parameter !== 'undefined' && typeof ssmParam.Parameter.Value !== 'undefined') {
        secureParameter = ssmParam.Parameter.Value;
    };

    return secureParameter;
}

그런데 함수는 async, ssm.getParameter앞 뒤로는 awaitpromise에 반환 타입은 Promise<string>이라 반환 값을 출력해도 내부 값이 보이질 않는다.
그래서 찾아보니 이 모두가 비동기처리를 동기적으로 처리하기 위한 장치라 한다.

TypeScript에서 비동기처리를 동기적으로 처리하는 법

비동기처리와 Callback

먼저 비동기처리란, 코드가 적힌 순서대로 앞 코드 연산이 끝나면 다음 코드 연산을 처리하고 하는 것이 아니라 앞 코드 연산을 계속하면서 다음 코드 연산을 해버리는 것인데 JavaScript는 싱글 스레드(하나의 call stack에 모든 처리를 쌓아놓음)라 비동기처리를 해야만 한다.


때문에 이미 함수는 return해서 끝나버렸는데 반환해야 할 값은 아직 연산이 끝나지 않아 undefined가 반환되는 일이 있다.
특히 setTimeout이나 API 요청을 보내고 응답을 기다릴 때 코드가 순서대로 처리되지 않는다.

 

이처럼 비동기처리를 하면 동시 다발적으로 코드를 처리하는 것이 가능하기 때문에 효율적이지만 (음식점에서 나는 스테이크 시키고 나중에 주문한 사람은 샐러드를 시켰을 때 샐러드가 먼저 나오는 상황인거랑 똑같다) Python만 사용해본 사람으로서는(물론 Python에도 비동기처리 라이브러리가 있긴 하지만 난 사용해본 적 없다) 이게 무슨 말도 안되는 상황인가 싶은거다.

 

이를 막기 위해서 Promise기능이 생기기 전에는 callback(다른 함수의 연산이 끝날 때까지 특정 코드가 처리되지 않도록 대기)에 callback을 하는 이른바 callback헬이었다고 한다.

(아래 쓴 코드가 문법이 맞는지는 잘 모르겠다지만 이런 거였지 않을까 하고 써 봤다..)

function makeBurger(beef, bread) {
    getBeef(beef, function(patty) {
        cookPatty(patty, function(cookedPatty) {
            getBuns(bread, function(buns) {
                putBeefBetweenBuns(buns, cookedPatty, function(burger) {
                    return burger;
                })
            })
        })
    })
};

async, await, promise

이제는 callback지옥 대신 async, await, promise을 사용하면 비동기처리가 동기적으로 처리된다.
참고로 await는 단독으로 사용될 수 없고 무조건 async함수 내에서만 사용해야 한다.

function async makeBurger(beef, bread): Promise<any> {
    let patty = await getBeef(beef).promise();
      patty  = await cookPatty(patty).promise();
      let buns = await getBuns(bread).promise();
      let burger = await putBeefBetweenBuns(buns, patty).promise();
    return burger;
}

function async serveBurger(beef, bread): Promise<any> {
    const servedBurger = await makeBurger(beef, bread);
    return servedBurger;
}

구글링을 해 보면 많은 글에서 asyncawaitpromise와 달리 보는데 여기에 대해서는 아직 잘 모르겠다(async, await없이 promise를 다루는 법을 모른다는 뜻).

 

thencatch를 이용해 Python의 try, except처럼 사용하는 것이라는 것만 알고 있는데 asyncawait를 더 많이 사용하는 이유는 promise 코드가 더 지저분해서라는 것 같다.

const servedBurger = makeBurger(beef, bread).then((burger) => {
    return burger;
}).catch((err) => {
    console.log("You should not eat this burger.")
});

awaitpromise는 동기적 처리에 대해 훨씬 확실하게 보장하는 것이라 하는데 Promise 생성자(Constructor)일 경우에만 뒤에 .promise()메소드를 사용할 수 있었고 Promise 객체(Object)일 경우에는 사용할 수 없었다.

한 번 async, await, promise를 쓰기 시작하면 끝까지 써야한다

Promise<T>를 반환한 함수를 다른 함수 혹은 클래스에서 호출하거나 변수로 다른 곳에서 호출할 경우에는 아래와 같이 async함수로 만들어야 한다.

즉, 변수 형태로 호출하거나 함수는 함수이되 async함수가 아닌 경우, Promise 객체 내부 내용을 확인할 수 없거나 비동기처리로 인해 return부분이 먼저 실행되면서 Promise { < Pending > }이 반환되어 다음 작업이 불가해질 것이다.

import { getParameterString } from './parameterStore';

export async function stgRedshiftConfig() {
    return {
        masterPassword: await getParameterString("redshift", "password"),
        masterUsername: await getParameterString("redshift", "user"),
        dbName: await getParameterString("redshift", "database"),
        availabilityZone: await getParameterString("redshift", "availabilityzone")
    };
};

참고로 Promise 객체는 3 가지 단계가 있어, Pending, Fulfilled, Reject로 상태가 나뉜다.


방금 말한 Pending은 '아직 처리중'이라는 뜻이라 성공 여부도 확실치 않은 상태이다.
Fulfilled는 완료 된 상태 즉 성공을 뜻하고 Reject는 완료되었으나 에러 등으로 인해 처리가 실패한 경우를 가리킨다.

 

Promise를 사용하는 가장 최종 단계(top-level)에서는 async로 감싸지 않아도 await Promise.resolve();만으로 Promise객체를 정상적으로 읽어올 수 있다고 하는데: Top-level await

top-level이 모듈이어야 하는 등의 제약이 있다(아직 사용법은 잘 모르겠다).

Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher.ts(1378)

aws-sdk.SSM().getParameter()는 Promise 생성자

aws-sdk의 파라미터 호출 함수는 Promise 생성자이기 때문에 아래와 같이 awaitpromise로 감싸주었던 것이다.

    var ssmParam = await ssm.getParameter(param).promise();

가장 위 코드를 다시 보면 가져온 파라미터를 문자열로 반환하고 있기 때문에 반환 값은 Promise<string>인데 asyncawait로 감싼 함수라면 Promise 객체라 할지라도 <T>부분으로 처리를 하기 때문에 일반적으로 문자열을 처리할 때와 동일한 작업이 가능하다.

 

계속 말해왔듯이 Redshift에 접속하기 위해 TypeScript에서 파라미터를 호출하는 경우에는 아래와 같이 끝까지 async함수의 형태를 지켜줘야 한다.

import { getParameterString } from './parameterStore';

export async function stgRedshiftConfig() {
    return {
        masterPassword: await getParameterString("redshift", "password"),
        masterUsername: await getParameterString("redshift", "user"),
        dbName: await getParameterString("redshift", "database"),
        availabilityZone: await getParameterString("redshift", "availabilityzone")
    };
};
import AWS = require("aws-sdk");
import { stgRedshiftConfig } from "./redshiftConfig";
const Redshift = new AWS.Redshift();

export async function createStgRedshift() {
    const parameterConfig = await stgRedshiftConfig();

    const params = {
        MasterUserPassword: parameterConfig.masterPassword,
        MasterUsername: parameterConfig.masterUsername,
        AvailabilityZone: parameterConfig.availabilityZone,
        DBName: parameterConfig.dbName,
        Encrypted: false,
        EnhancedVpcRouting: false,
        IamRoles: [
            "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME"
        ],
        Port: 5439,
        PubliclyAccessible: true,
        VpcSecurityGroupIds: ["SECURE_GROUP_ID", "SECURE_GROUP_ID"]
    };

    Redshift.createCluster(params, function (err, data) {
        if (err) console.log(err, err.stack);
        else console.log(data);
    });
}