지난 튜토리얼에서는 Serverless 프레임워크를 사용하여 아주 간단한 API 를 만들어주었습니다. 이번 튜토리얼에서는, Serverless 를 조금 더 활용해서 MongoDB 에 연동한 RESTful CRUD (Create, Remove, Update, Delete) API 를 만들어보겠습니다.
이 튜토리얼은, 지난번에 진행하던 프로젝트에서 이어서 진행하도록 하겠습니다. 만약에 지난 튜토리얼을 진행하지 않았다면 해당 튜토리얼을 먼저 진행하고 이 튜토리얼을 읽어주세요. 추가적으로, Node.js 에 익숙하지 않다면 이 튜토리얼이 조금! 어려울 수도 있습니다.
이 튜토리얼에서 사용되는 코드는 Github Repo에서 참고 할 수 있으니 참고하세요.
여러개의 함수 만들기
우선, 우리의 서버리스 애플리케이션에서 관리 할 여러가지의 함수를 만들어주겠습니다.
serverless.yml 파일을 열어보세요.
serverless.yml
service: hello-serverless
provider:
name: aws
runtime: nodejs6.10
stage: dev
region: ap-northeast-2
functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: get
기존에 있던 서버리스 설정을 보시면, hello 라는 함수에 handler 값이 handler.hello 로 설정되어 있지요? 이 의미는, handler.js 파일에서 hello 를 호출하라는 의미입니다.
자, 우리는 story 라는 파일을 만들고, 해당 함수에 CRUD 에 해당하는 4가지 함수를 만들어주겠습니다.
기존의 handler.js 를 지우시고, src 라는 디렉토리를 만드세요. 그리고, 그 디렉토리에 stories.js 파일을 생성하세요.
src/stories.js
const createResponse = (status, body) => ({
statusCode: status,
body: JSON.stringify(body)
});
// 스토리 만들기
exports.createStory = (event, ctx, cb) => {
cb(null, createResponse(200, { message: 'create' }));
};
// 여러개의 스토리 리스팅
exports.readStories = (event, ctx, cb) => {
cb(null, createResponse(200, { message: 'list' }));
};
// 특정 스토리 읽기
exports.readStory = (event, ctx, cb) => {
cb(null, createResponse(200, { message: 'read' }));
};
// 스토리 수정
exports.updateStory = (event, ctx, cb) => {
cb(null, createResponse(200, { message: 'update' }));
};
// 스토리 삭제
exports.deleteStory = (event, ctx, cb) => {
cb(null, createResponse(200, { message: 'delete' }));
};
response 객체를 직접 계속해서 만드는게 귀찮으니, 따로 createResponse 라는 함수를 만들어 주었고, 생성, 리스팅, 조회, 수정, 삭제에 해당하는 함수를 만들어서 내보내주었습니다.
그 다음엔, serverless.yml 파일을 수정하여 각 함수에 API 주소를 연결시켜줍시다.
serverless.yml
service: hello-serverless
provider:
name: aws
runtime: nodejs6.10
stage: dev
region: ap-northeast-2
functions:
createStory:
handler: src/stories.createStory
events:
- http:
path: stories
method: post
readStories:
handler: src/stories.readStories
events:
- http:
path: stories
method: get
readStory:
handler: src/stories.readStory
events:
- http:
path: stories/{id}
method: get
updateStory:
handler: src/stories.updateStory
events:
- http:
path: stories/{id}
method: patch
deleteStory:
handler: src/stories.deleteStory
events:
- http:
path: stories/{id}
method: delete
예를 들어서, createStory 는 src/stories.js 파일에 있는 createStory 함수이니, handler 의 값으로는 src/stories.createStory 와 같은 형태로 작성을 하시면 됩니다.
createStory 와 readStories 를 제외한 함수들은, 특정 스토리를 읽거나, 수정하거나, 삭제하는 기능입니다. 따라서, API 경로가 stories 가 아닌 stories/{id} 형태로 되어있는데요, 이 의미는 id 값을 URL 파라미터로 받아오겠다는 의미입니다. 즉, /stories/507f1f77bcf86cd799439011 이러한 형태로 특정 스토리에 대한 작업을 할 수 있는 것이죠.
id 로 전달 된 값은 함수 내부에서 event.pathParameters.id 를 통하여 조회 할 수 있습니다.
코드를 저장하고, sls deploy
명령어를 입력하시면, 여러 함수가 배포된 것을 확인 할 수 있습니다.
npm 에서 모듈 설치하기
이번엔, npm 에서 모듈을 설치해서 사용해보겠습니다. 우선 npm init
명령어를 통하여 package.json 을 만들어주겠습니다. CLI 에서 물어보는 질문에 모두 Enter 키를 누르시면 됩니다.
$ npm init
그 다음에는, mongoose 라는 라이브러리를 설치하겠습니다. 이 라이브러리는, MongoDB ODM 으로서, MongoDB 연동을 하게 될 때 편하게 작업 할 수 있게 해주는 라이브러리 입니다.
$ npm i --save mongoose
우리의 애플리케이션에서 MongoDB 를 사용하려면, 데이터베이스 서버가 필요하겠지요? 여러분이 직접 구축을 하셔도 되지만, 그냥 학습용이라면 이 작업이 조금 번거로울 수도 있습니다. 그렇다면, mLab 혹은 MongoDB Atlas 에서 무료 호스팅을 받을 수도 있습니다.
아쉽게도, 아직 두 서비스 모두 한국서버는 지원하지 않아서, 해당 서비스들은 가벼운 용도로만 사용하시는것을 추천합니다.
이 튜토리얼에선, 제가 미리 만들어둔 공개 데이터베이스와 계정을 사용하겠습니다. 직접 만들게 되신다면, 이 가이드를 참고하세요.
MongoDB 연결하기
우리가 만든 함수에서 mongoose 를 사용하기 위해서 따로 해야 할 작업은 없습니다. 일반 Node.js 애플리케이션에서 모듈을 불러와서 사용 할 때와 같이, 그냥 require 를 통해서 불러와주시면 됩니다.
일반 Node.js 에서 mongoose 를 사용 할 때와 다른 점은, 함수를 호출 할 때마다 MongoDB 에 연결을 해주어야 합니다.
API 가 호출 될 때마다 MongoDB 에 접속요청을 하는 것은 성능적으로 낭비 일 수 있습니다. 다행히도, 함수가 시작되고 일정 시간 안에 새로운 요청이 들어온다면 MongoDB 접속정보를 재활용하는 방법이 있습니다. 이는, 나중에 다뤄보도록 하겠습니다.
src/stories.js
const mongoose = require('mongoose');
const connect = () => {
return mongoose.connect('mongodb://serverless:serverless@ds239128.mlab.com:39128/serverless');
};
const createResponse = (status, body) => ({
statusCode: status,
body: JSON.stringify(body)
});
// 스토리 만들기
exports.createStory = (event, ctx, cb) => {
ctx.callbackWaitsForEmptyEventLoop = false;
connect().then(
() => {
cb(null, createResponse(200, { message: 'connected to db' }));
}
).catch(
(e) => {
cb(null, createResponse(500, { error: e }));
}
)
};
(...)
위 코드에서 ctx.callbackWaitsForEmptyEventLoop = false;
를 함수의 상단에서 입력을 해주었는데요, 이걸 작성한 이유는, 람다에서의 콜백은, 기본적으로는 이벤트 루프 안의 모든 작업이 비워질때까지, 즉, 애플리케이션에서 진행중인 모든 작업이 끝날때 까지 콜백이 호출되지 않습니다. 우리가 mongoose 로 데이터베이스 서버에 접속을 하였기 때문에 콜백이 처리되지 않게 되므로 해당 설정을 해주면, 콜백이 제대로 처리됩니다.
추가적으로, 나중에 이 설정을 통하여 기존에 만들었던 접속정보를 재활용 할 수도 있게 됩니다.
Story 모델 만들기
Story 데이터를 읽고, 쓰고, 수정하고, 삭제하기 위하여 Story 모델을 만들도록 하겠습니다.
src/models/Story.js
const mongoose = require('mongoose');
const StorySchema = new mongoose.Schema({
title: String,
body: String
});
const Story = mongoose.model('Story', StorySchema);
module.exports = Story;
CRUD 구현하기
자, 이제 우리가 방금 만든 모델을 사용하여 CRUD 를 구현하겠습니다.
데이터 생성 – createStory
src/stories.js
const mongoose = require('mongoose');
const Story = require('./models/Story');
const connect = () => {
return mongoose.connect('mongodb://serverless:serverless@ds239128.mlab.com:39128/serverless');
};
const createResponse = (status, body) => ({
statusCode: status,
body: JSON.stringify(body)
});
// 스토리 만들기
exports.createStory = (event, ctx, cb) => {
ctx.callbackWaitsForEmptyEventLoop = false;
const { title, body } = JSON.parse(event.body);
connect().then(
() => {
const story = new Story({ title, body });
return story.save();
}
).then(
story => {
cb(null, createResponse(200, story));
}
).catch(
e => cb(e)
);
};
(...)
이렇게 createStory 함수를 완성하고, sls deploy
를 입력하여 배포를 하세요. 그 다음엔, PostMan 같은 API 테스팅 도구로 다음과 같이 POST 요청을 해보세요.
POST /stories
{
"title": "제목",
"body": "이야기"
}
혹은, 다음과 같이 터미널로 요청을 해보셔도 됩니다.
$ curl -X POST \
https://n7zvdgkmu5.execute-api.ap-northeast-2.amazonaws.com/dev/stories \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-d '{
"title": "제목",
"body": "이야기"
}'
지금은 함수가 실행 될 때마다 MongoDB 에 요청을 하도록 설정되었으므로, 요청시간이 1000ms 전후로 걸릴 것입니다. 자, 코드를 조금 더 수정해서 마지막으로 사용했던 데이터베이스 접속정보를 재사용해보겠습니다.
접속정보 재사용하기
우선, connect 함수에서 만약에 기존에 연결중이던게 있으면 새로 연결하지 않도록 설정합니다.
src/stories.js
const mongoose = require('mongoose');
const Story = require('./models/Story');
let connection = null;
const connect = () => {
// 연결 되어있으면 기존것을 연결시키고
if (connection && mongoose.connection.readyState === 1) return Promise.resolve(connection);
// 없으면 새로 연결함
return mongoose.connect('mongodb://serverless:serverless@ds239128.mlab.com:39128/serverless').then(
conn => {
connection = conn;
return connection;
}
);
};
(...)
그리고, 이미 데이터베이스가 설정 된 상태에서 새로 모델을 정의하게 되면 오류가 발생합니다. 따라서, Story 모델 파일도 변경을 해주겠습니다.
src/models/Story.js
const mongoose = require('mongoose');
const StorySchema = new mongoose.Schema({
title: String,
body: String
});
// Cannot overwrite model once compiled. 이슈 해결
global.Story = global.Story || mongoose.model('Story', StorySchema);
module.exports = global.Story;
이제 다시 배포를 하고 요청을 하고나면, 처리기간이 단축 된 것을 확인 하실 수 있습니다.
현재 200ms 수준으로 단축된것을 확인하실 수 있을텐데요, 나중에 여러분이 MongoDB 서버를 AWS EC2 로 돌리시게 된다면, 더욱 단축 할 수 있을 것입니다.
그럼, 나머지 기능들도 구현해줍시다.
데이터 리스팅 – readStories
여러개의 스토리 데이터를 불러오는 readStories 함수를 완성해주겠습니다.
src/stories.js
// 여러개의 스토리 리스팅
exports.readStories = (event, ctx, cb) => {
ctx.callbackWaitsForEmptyEventLoop = false;
connect().then(
// 역순으로, 최대 20개 리스팅
() => Story.find().sort({ _id: -1 }).limit(20).lean().exec()
).then(
stories => cb(null, createResponse(200, stories))
);
};
여기서 .lean() 을 한 이유는 plain JSON 객체를 받아오기 위함 입니다.
배포 후 제대로 작동하는지 확인하세요.
GET /stories
이제, 특정 스토리에 하는 작업을 완성하겠습니다.
특정 스토리 읽기 – readStory
_id 값을 사용하여 특정 데이터를 조회하는 API 를 만들겠습니다.
src/stories.js
// 특정 스토리 읽기
exports.readStory = (event, ctx, cb) => {
ctx.callbackWaitsForEmptyEventLoop = false;
connect().then(
// 역순으로, 최대 20개 리스팅
() => Story.findById(event.pathParameters.id).exec()
).then(
story => {
if (!story) {
return cb(null, { statusCode: 404 });
}
cb(null, createResponse(200, story));
}
);
};
배포를 하고 요청을 해보세요.
id 파라미터는 우리가 아까 리스팅 API 를 호출 했을 때 나타났던 _id 값 (예: 5a8823a57b1e40000137a0f2) 을 넣어주시면 됩니다.
GET /stories/5a8823a57b1e40000137a0f2
특정 스토리 삭제 – deleteStory
이번엔, 특정 스토리를 삭제하는 기능을 구현하겠습니다.
src/stories.js
// 스토리 삭제
exports.deleteStory = (event, ctx, cb) => {
ctx.callbackWaitsForEmptyEventLoop = false;
connect().then(
// 역순으로, 최대 20개 리스팅
() => Story.remove({ _id: event.pathParameters.id }).exec()
).then(
() => cb(null, createResponse(204, null))
);
};
특정 스토리 업데이트
마지막 API 인 특정 스토리 업데이트 함수를 구현해주겠습니다. 이 API 에선 event 의 body 값도 사용하고, pathParameters 값도 사용합니다.
src/stories.js
// 스토리 수정
exports.updateStory = (event, ctx, cb) => {
ctx.callbackWaitsForEmptyEventLoop = false;
const update = JSON.parse(event.body);
connect().then(
// id 로 찾아서 업데이트
() => Story.findOneAndUpdate({ _id: event.pathParameters.id }, update, { new: true }).exec()
).then(
story => {
if (!story) {
return cb(null, { statusCode: 404 });
}
cb(null, createResponse(200, story));
}
);
};
드디어! 모든 작업이 끝났습니다.
정리
우리는 Serverless 프레임워크를 통하여 RESTful CRUD API 를 개발해보았습니다. Node.js 를 사용해서 express, koa 등을 사용해본 적이 있다면, 조금 불편했을 것입니다. 예를 들어서, 라우트를 하나하나 다른 함수로 설정해야 된다는점, 모든 함수에서 데이터베이스를 연결해야된다는점 등등…
이어질 튜토리얼에서는, 서버리스 애플리케이션 개발을 조금 더 즐겁고 편하게 개발하기 위해서 다음 사항들을 다뤄보겠습니다:
- ESLint 설정하기
- Babel, Webpack 설정하기
- serverless-http 를 통하여 Koa, Express 등의 Node.js 웹 프레임워크와 함께 사용하기