본문 바로가기
IT/Serverless Stack

[AWS] 서버리스 스택-3

by 밤톨엽 2022. 3. 24.

해당 글은 [AWS] 서버리스 스택-2와 이어집니다.

이번 글에서는 메모 API 수정, 삭제, 조회 등에 대해서 작성을 하였습니다.


코드 리팩터링

작성할 대부분의 API에 DynamoDB 요청과 비슷한 동작을 구현할 것이므로 공통적인 사항을 모듈화 해보자.

우선 src/util 경로로 폴더를 생성해주고 src/util/dynamodb.ts 파일을 생성하고 아래처럼 코드를 만들어주자.

// src/util/dynamodb.ts
import AWS from 'aws-sdk';
import { DocumentClient } from 'aws-sdk/clients/dynamodb';

const client: DocumentClient = new AWS.DynamoDB.DocumentClient();

export default {
  get: (params: DocumentClient.GetItemInput) => client.get(params).promise(),
  put: (params: DocumentClient.PutItemInput) => client.put(params).promise(),
  query: (params: DocumentClient.QueryInput) => client.query(params).promise(),
  update: (params: DocumentClient.UpdateItemInput) => client.update(params).promise(),
  delete: (params: DocumentClient.DeleteItemInput) => client.delete(params).promise(),
};

DynamoDB의 관련된 기능을 모두 사용할 것이기 때문에 관련 메서드를 미리 구현해놓고 사용하기 쉽게 구성한다.


src/util/handler.ts 경로에 파일을 생성하고 아래처럼 코드를 만들어주자.

// src/util/handler.ts
import { APIGatewayProxyResult, Callback, Context, Handler } from 'aws-lambda';

export default function handler(lambda: Handler) {
  return async function (
    event: any,
    context: Context,
    callback: Callback
  ): Promise<APIGatewayProxyResult> {
    let body, statusCode;

    try {
      // 람다 실행
      body = await lambda(event, context, callback);
      statusCode = 200;
    } catch (e: any) {
      console.error(e);
      body = { error: e.message };
      statusCode = 500;
    }

    // HTTP 응답 return
    return {
      statusCode,
      body: JSON.stringify(body),
    };
  };
}

handler 함수를 공통으로 사용하기 위해 아래처럼 기능하도록 작성했다.

  • handler 함수는 Lambda 함수를 감싸는 용도의 함수 역할
  • Lambda 함수를 인수로 받아옴
  • try/catch 문으로 Lambda 함수를 실행
  • 성공 시 JSON.stringify를 통해 결과와 200 상태 코드를 반환
  • 오류가 있으면 500 상태 코드와 함께 오류 메시지를 반환

src/create.ts 경로에 있는 코드를 아래처럼 수정해주자.

// src/create.ts
import * as uuid from 'uuid';
import handler from './util/handler';
import dynamoDb from './util/dynamodb';

export const main = handler(async (event) => {
  // JSON으로 데이터를 넘겨 'event.body' 에서 파싱이 필요하다
  const data = JSON.parse(event.body);

  // DynamoDB에서 필요한 인자들
  const params = {
    TableName: process.env.TABLE_NAME!,
    Item: {
      userId: '123', // 사용자 ID
      noteId: uuid.v1(), // 메모를 생성할 때 생성되는 고유ID (uuid 라이브러리 사용)
      content: data.content, // event.body 쪽에서 받은 정보
      attachment: data.attachment, // event.body 쪽에서 받은 정보
      createdAt: Date.now(), // 생성된 시간
    },
  };

  await dynamoDb.put(params);

  return params.Item;
});

코드를 수정함으로써 얻는 효과는 아래와 같다.

  • Lambda 함수를 async 함수로 처리하고 단순히 결과를 반환하도록 한다.
  • DynamoDB의 인스턴스를 거의 모든 API에서 사용할 것이므로 공통적으로 처리하는 모듈을 사용한다.
  • 우리는 Lambda 함수의 모든 오류를 공통적으로 처리하도록 한다.
  • 마지막으로 모든 Lambda 함수가 API 엔드포인트를 처리하므로 한 곳에서 HTTP 응답을 처리한다.

메모 조회 기능 추가

이전에 메모 생성 API를 테스트하면서 userId=123인 데이터를 저장했으므로 이제 해당 ID가 지정된 메모를 검색하는 API를 추가해보자.

src/get.ts 경로에 파일을 추가하고 아래처럼 코드를 작성하자.

// src/get.ts
import handler from "./util/handler";
import dynamoDb from "./util/dynamodb";

export const main = handler(async (event) => {
  const params = {
    TableName: process.env.TABLE_NAME!,
    // DyanomDB에서 primaryKey, sortKey 2개가 제공되어야 한다.
    Key: {
      userId: "123", // primaryKey (유저아이디)
      noteId: event.pathParameters.id, // sortKey (메모아이디)
    },
  };

  const result = await dynamoDb.get(params);
  if (!result.Item) {
    throw new Error("Item not found.");
  }

  // 조회한 결과를 돌려준다.
  return result.Item;
});

stacks/ApiStack.ts 파일에 "GET /notes/{id}": "src/get.main" 메모를 조회하는 API 경로를 추가해주자.

// stacks/ApiStack.ts
import * as sst from "@serverless-stack/resources";

export default class ApiStack extends sst.Stack {
  // 다른 스택에서 접근할 수 있도록 선언
  api;

  constructor(scope: sst.App, id: string, props?: any) {
    super(scope, id, props);

    const { table } = props;

    // API 생성 (두번째 인자에 본인 이니셜을 포함해 ID를 넣어주자)
    this.api = new sst.Api(this, "api-sykim", {
      defaultFunctionProps: {
        environment: {
          TABLE_NAME: table.tableName,
        },
      },
      routes: {
        "POST /notes": "src/create.main", // 메모 생성 API
        "GET /notes/{id}": "src/get.main", // 메모 조회 API
      },
    });

    // API가 DynamoDB 테이블에 접근할 수 있도록 권한 설정
    this.api.attachPermissions([table]);

    // API의 EndPoint Url을 노출
    this.addOutputs({
      ApiEndpoint: this.api.url,
    });
  }
}

변경사항 배포

터미널에서 아래 명령어를 통해 AWS에 리소스를 배포하고 테스트를 진행하자

npx sst start

배포가 완료되면 터미널에서 아래와 비슷한 문구가 나와야 한다.

Stack dev-notes-api
  Status: deployed
  Outputs:
    ApiEndpoint: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com

배포 완료 후 직접 HTTP GET 요청을 만들어 ApiEndPoint에 나와있는 주소와 경로 /notes/"메모ID" 경로를 구성하여 테스트를 진행하면 아래처럼 응답이 나와야 한다.
(지난번에 기록해놓은 notesId와 함께 https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes/57708320-aa5b-11ec-8f0f-e37a7bfbc143 로 요청해야 함.)

반드시 본인이 생성한 NoteId 값을 포함해야 한다.
POSTMAN을 사용한다면 https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes/:id 처럼 요청 주소를 적어주면 끝에 ID를 입력할 수 있는 Path Variables를 구성할 수 있다.

POSTMAN get 요청
POSTMAN get 요청

메모 목록 조회 기능 추가

사용자가 가지고 있는 모든 메모를 조회하는 API를 추가해보자.

src/list.ts 경로에 파일을 추가하고 아래처럼 코드를 작성하자.

// src/list.ts
import handler from "./util/handler";
import dynamoDb from "./util/dynamodb";

export const main = handler(async (event) => {
  const params = {
    TableName: process.env.TABLE_NAME!,
    // 'KeyConditionExpression' dynamoDB에서 query에 사용되는 키값을 정의
    // - 'userId = :userId' 이것은 userId 키와 일치하는 값만 조회하게 된다
    // partition key (userId와 일치하는 모든 목록을 조회한다)
    KeyConditionExpression: "userId = :userId",
    // 'ExpressionAttributeValues' 위에 KeyConditionExpresion에 매칭되는 값을 정의
    // - ':userId': 'userId'의 값을 정의한다.
    ExpressionAttributeValues: {
      ":userId": "123",
    },
  };

  const result = await dynamoDb.query(params);

  // 조회된 목록을 반환
  return result.Items;
});

stacks/ApiStack.ts 파일에 "GET /notes": "src/list.main" 메모 목록을 조회하는 API 경로를 추가해주자.

// stacks/ApiStack.ts
import * as sst from '@serverless-stack/resources';

export default class ApiStack extends sst.Stack {
  // 다른 스택에서 접근할 수 있도록 선언
  api;

  constructor(scope: sst.App, id: string, props?: any) {
    super(scope, id, props);

    const { table } = props;

    // API 생성 (두번째 인자에 본인 이니셜을 포함해 ID를 넣어주자)
    this.api = new sst.Api(this, 'api-sykim', {
      defaultFunctionProps: {
        environment: {
          TABLE_NAME: table.tableName,
        },
      },
      routes: {
        'POST /notes': 'src/create.main', // 메모 생성 API
        'GET /notes/{id}': 'src/get.main', // 메모 조회 API
        'GET /notes': 'src/list.main', // 메모 목록 조회 API
      },
    });

    // API가 DynamoDB 테이블에 접근할 수 있도록 권한 설정
    this.api.attachPermissions([table]);

    // API의 EndPoint Url을 노출
    this.addOutputs({
      ApiEndpoint: this.api.url,
    });
  }
}

변경사항 배포

터미널에서 아래 명령어를 통해 AWS에 리소스를 배포하고 테스트를 진행하자

npx sst start

배포가 완료되면 터미널에서 아래와 비슷한 문구가 나와야 한다.

Stack dev-notes-api
  Status: deployed
  Outputs:
    ApiEndpoint: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com

배포 완료 후 직접 HTTP GET 요청을 만들어 ApiEndPoint에 나와있는 주소와 경로 /notes 경로를 구성하여 테스트를 진행하면 아래처럼 응답이 나와야 한다.

POSTMAN HTTP get
POSTMAN HTTP get

메모 수정 기능 추가

사용자가 가지고 있는 메모를 수정하는 API를 추가해보자.

src/update.ts 경로에 파일을 추가하고 아래처럼 코드를 작성하자.

// src/update.ts
import handler from "./util/handler";
import dynamoDb from "./util/dynamodb";

export const main = handler(async (event) => {
  const data = JSON.parse(event.body);
  const params = {
    TableName: process.env.TABLE_NAME!,
    // 'Key' 수정하기 위해 수정해야 할 데이터의 Key를 정의
    Key: {
      userId: "123", // 사용자 ID
      noteId: event.pathParameters.id, // 노트 ID
    },
    // 'UpdateExpression' 업데이트 할 필드 정의
    // 'ExpressionAttributeValues' 업데이트할 값을 정의
    UpdateExpression: "SET content = :content, attachment = :attachment",
    ExpressionAttributeValues: {
      ":attachment": data.attachment || null, // 수정할 파일이름
      ":content": data.content || null, // 수정할 메모 내용
    },
    // 'ReturnValues' 이 설정에 따라 업데이트된 후 어떤 값을 돌려줄 지 설정가능
    ReturnValues: "ALL_NEW",
  };

  await dynamoDb.update(params);

  return { status: true };
});

stacks/ApiStack.ts 파일에 "PUT /notes/{id}": "src/update.main" 메모를 수정하는 API 경로를 추가해주자.

// stacks/ApiStack.ts
import * as sst from '@serverless-stack/resources';

export default class ApiStack extends sst.Stack {
  // 다른 스택에서 접근할 수 있도록 선언
  api;

  constructor(scope: sst.App, id: string, props?: any) {
    super(scope, id, props);

    const { table } = props;

    // API 생성 (두번째 인자에 본인 이니셜을 포함해 ID를 넣어주자)
    this.api = new sst.Api(this, 'api-sykim', {
      defaultFunctionProps: {
        environment: {
          TABLE_NAME: table.tableName,
        },
      },
      routes: {
        'POST /notes': 'src/create.main', // 메모 생성 API
        'GET /notes/{id}': 'src/get.main', // 메모 조회 API
        'GET /notes': 'src/list.main', // 메모 목록 조회 API
        'PUT /notes/{id}': 'src/update.main', // 메모 수정 API
      },
    });

    // API가 DynamoDB 테이블에 접근할 수 있도록 권한 설정
    this.api.attachPermissions([table]);

    // API의 EndPoint Url을 노출
    this.addOutputs({
      ApiEndpoint: this.api.url,
    });
  }
}

변경사항 배포

터미널에서 아래 명령어를 통해 AWS에 리소스를 배포하고 테스트를 진행하자

npx sst start

배포가 완료되면 터미널에서 아래와 비슷한 문구가 나와야 한다.

Stack dev-notes-api
  Status: deployed
  Outputs:
    ApiEndpoint: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com

배포 완료 후 직접 HTTP PUT요청을 만들어 ApiEndPoint에 나와있는 주소와 경로 /notes/"메모ID" 경로를 구성하여 테스트를 진행하면 아래처럼 응답이 나와야 한다.
(지난번에 기록해놓은 notesId와 함께 https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes/57708320-aa5b-11ec-8f0f-e37a7bfbc143 로 요청해야 함.)

반드시 본인이 생성한 NoteId 값을 포함해야 한다.
HTTP PUT 요청을 보낼 때 실제 클라이언트에서 보내는 것처럼 HTTP Body에 아래처럼 JSON 데이터를 만들어서 보내야 한다. 
(이해가 되지 않는다면 반드시 이전에 src/update.ts 파일에 선언한 event.body를 어떻게 사용하는지 살펴봐야 한다.)
{"content":"New World","attachment":"new.jpg"}

POSTMAN HTTP put
POSTMAN HTTP put

메모 삭제 기능 추가

사용자가 가지고 있는 메모를 삭제하는 API를 추가해보자.

src/delete.ts 경로에 파일을 추가하고 아래처럼 코드를 작성하자.

// src/delete.ts
import handler from './util/handler';
import dynamoDb from './util/dynamodb';

export const main = handler(async (event) => {
  const params = {
    TableName: process.env.TABLE_NAME!,
    // 'Key' 삭제할 데이터의 키값 정의
    Key: {
      userId: '123', // 사용자 ID
      noteId: event.pathParameters.id, // 노트 ID
    },
  };

  await dynamoDb.delete(params);

  return { status: true };
});

stacks/ApiStack.ts 파일에 "DELETE /notes/{id}": "src/delete.main" 메모를 수정하는 API 경로를 추가해주자.

// stacks/ApiStack.ts
import * as sst from '@serverless-stack/resources';

export default class ApiStack extends sst.Stack {
  // 다른 스택에서 접근할 수 있도록 선언
  api;

  constructor(scope: sst.App, id: string, props?: any) {
    super(scope, id, props);

    const { table } = props;

    // API 생성 (두번째 인자에 본인 이니셜을 포함해 ID를 넣어주자)
    this.api = new sst.Api(this, 'api-sykim', {
      defaultFunctionProps: {
        environment: {
          TABLE_NAME: table.tableName,
        },
      },
      routes: {
        'POST /notes': 'src/create.main', // 메모 생성 API
        'GET /notes/{id}': 'src/get.main', // 메모 조회 API
        'GET /notes': 'src/list.main', // 메모 목록 조회 API
        'PUT /notes/{id}': 'src/update.main', // 메모 수정 API
        'DELETE /notes/{id}': 'src/delete.main', // 메모 삭제 API
      },
    });

    // API가 DynamoDB 테이블에 접근할 수 있도록 권한 설정
    this.api.attachPermissions([table]);

    // API의 EndPoint Url을 노출
    this.addOutputs({
      ApiEndpoint: this.api.url,
    });
  }
}

변경사항 배포

터미널에서 아래 명령어를 통해 AWS에 리소스를 배포하고 테스트를 진행하자

npx sst start

배포가 완료되면 터미널에서 아래와 비슷한 문구가 나와야 한다.

Stack dev-notes-api
  Status: deployed
  Outputs:
    ApiEndpoint: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com

배포 완료 후 직접 HTTP DELETE 요청을 만들어 ApiEndPoint에 나와있는 주소와 경로 /notes/"메모ID" 경로를 구성하여 테스트를 진행하면 아래처럼 응답이 나와야 한다.
(지난번에 기록해놓은 notesId와 함께 https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes/57708320-aa5b-11ec-8f0f-e37a7bfbc143 로 요청해야 함.)

반드시 본인이 생성한 NoteId 값을 포함해야 한다.
POSTMAN을 사용한다면 https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes/:id 처럼 요청 주소를 적어주면 끝에 노트 ID를 입력할 수 있는 Path Variables를 구성할 수 있다.

POSTMAN HTTP delete
POSTMAN HTTP delete

메모 API 작성을 마무리하며 내용을 개인 Github에 올려주도록 하자.

서버리스 스택-3 정리

  • DynamoDB, Lambda 함수 리팩토링
  • 메모 조회 API 생성
  • 메모 목록 조회 API 생성
  • 메모 수정 API 생성
  • 메모 삭제 API 생성

이번 문서에서는 위와 같이 메모 관련 API를 모두 작성하였습니다.

다음 내용은 사용자 인증 및 API 보안 등에 대해서 [AWS] 서버리스 스택-4 파트에서 정리하도록 하겠습니다.

'IT > Serverless Stack' 카테고리의 다른 글

[AWS] 서버리스 스택-4  (0) 2022.04.06
[AWS] 서버리스 스택-2  (0) 2022.03.22
[AWS] 서버리스 스택-1  (0) 2022.03.21