[KoaJS] 4. Mongoose (1)

2017. 10. 13. 16:53다시 하자 Back-End/Koa framework

반응형

이번 포스팅에서는 Mongoose  모듈을 이용하여 클라이언트가 요청한 데이터를 MongoDB에 저장해보자. 지난번 Router 포스팅에서 첨부한 파일을 이용할 예정이니 참고하기 바란다.



지난 포스팅 보기 : [KoaJS] 3. Router




1. Mongoose




그림 1. Mongoose



Mongoose는 MongoDB를 이용한 객체 모델링을 제공해주는 NPM 모듈로써, ODM (Object Document Modeling 혹은 Mapper)의 대표적인 모듈 중 하나다. Mongoose를 이해하기 위해서 먼저 MongoDB에 대해 간단히 알아보자.



MongoDB



일반적으로 우리가 알고 있는 데이터 베이스는 SQL (구조화 질의 언어) 을 이용하여 데이터를 조작한다. SQL을 이용하는 데이터 베이스는 대부분 테이블끼리 관계 (1:1, 1:N, N:N)를 맺고 있는데, 이러한 형태를 관계형 데이터 베이스라고 한다. 관계형 데이터 베이스를 관리하는 시스템을 RDBMS라고 하며 Oracle-SQL, MS SQL-Server, My-SQL, PostgreSQL, SQLite 등이 있다.



RDBMS의 장점은 스키마를 이용하여 테이블을 생성하므로 정형화된 데이터를 관리할 수 있고, 데이터의 무결성을 보장한다. 반면, 스키마를 수정하기 어렵기 때문에 테이블을 수정하는데 어려움이 따르며 이는 확장성이 떨어지는 단점을 가지고 있다. 또한, 클라우드 환경이 발전하고 수많은 형태의 데이터가 네트워크 망에서 돌아다니고 있는 현재의 환경에서 다양한 형태의 데이터를 저장할 수 없다는 단점이 있다.



이번 포스팅부터 사용 할 MongoDB는 NoSQL 데이터 베이스로써 데이터 조작을 위해 SQL문이 필수인 RDBMS와 달리 SQL이 반드시 필요하지 않으며, 스키마가 없는 구조로 데이터를 관리하기 때문에 다양한 형태의 데이터를 저장할 수 있다. 또한 여러 개의 데이터 베이스를 하나로 묶는 클러스터 기능을 이용하여 확장하기 쉽고, 대용량의 데이터를 빠르게 처리할 수 있으며 분산처리에도 장점을 가진다.



단점으로는 스키마가 필요하지 않기 때문에 데이터의 정형화를 보장할 수 없으며, (실제로 하나의 데이터 베이스에 다른 형태의 데이터가 들어갈 수 있다.) 여러 개의 컬렉션에서 데이터를 조회 할 때, RDBMS의 경우 테이블의 Join을 이용하여 한번에 조회 할 수도 있지만 NoSQL에서는 불가능하다. NoSQL DBMS로는 MongoDB, RedisDB (Memcache), Cassandra 등이 있다.



NoSQL은 데이터를 저장하는 형태에 따라 여러 개로 나눌 수 있으며, MongoDB는 Document의 형태로 데이터를 저장한다. Document는 파이썬에서는 Dict 타입으로, 자바스크립트에서는 객체로, JSON 형태로 1:1로 맵핑이 가능하다. 따라서, MongoDB로 데이터를 조작 할 때 이해하기 쉽고, 따로 파싱하는 작업이 필요없어 빠르게 개발 할 수 있다는 장점이 있다.



Mongoose를 사용하는 이유?



NoSQL의 가장 큰 특징 중 하나는 Schemaless 한 데이터 관리 방식이다. 스키마가 없다는 이야기는 데이터의 정형화를 보장할 수 없다는 이야기가 된다. 즉, 하나의 컬렉션 (RDBMS의 테이블 개념)에 여러 형태의 Document가 들어 갈 수 있다. 따라서 서비스를 관리하거나 개발하는 입장에서는 여간 골치 아픈일이 아닐 수 없다.



Mongoose는 MongoDB에서 정형화된 데이터를 보장하기 위해 스키마를 사용할 수 있도록 해주는 NodeJS 모듈이다. Mongoose를 이용하면 스키마를 작성 할 수 있고, 작성된 스키마를 독립적인 모델로 관리할 수도 있다. 또한, 스키마 수정이 어려운 RDBMS와 달리 스키마의 수정이 쉽기 때문에 확장과 수정이 용이하다. Mongoose가 만능은 아니지만 어느 정도의 범위 (NoSQL의 한계는 존재한다. Join 같은 경우가 대표적인 예.) 에서는 NoSQL과 RDBMS의 장점 모두를 누릴 수 있다.



Mongoose로 MongoDB 연동



우리의 Todolist 프로젝트에서 Mongoose를 사용하기 위해서는 프로젝트 디렉터리에 모듈을 설치해야한다. 아래의 명령어로 Mongoose 모듈을 프로젝트에 추가하자.



npm install --save mongoose



Mongoose 설치가 완료 되었다면 Todolist 프로젝트에 Mongoose 객체를 생성하여 MongoDB와 연결해야만 한다. 참고로 MongoDB는 NPM 모듈이 아니기 때문에 따로 설치를 해야만 한다. 아래의 URI로 접속하여 운영체제 환경에 맞는 MongoDB를 미리 설치해주자.




MongoDB Community Server 다운로드 (클릭)



MongoDB까지 준비가 완료되면 Todolist 프로젝트에 Mongoose로 MongoDB와 연결해보자. Todolist 서비스가 구동될 때 DB에 연결되어야 하기 때문에 서비스의 시작점인 src/index.js 파일 상단 부분에 아래와 같이 코드를 추가한다. (파일 구조를 모르겠다면 Koa framework 포스팅을 처음부터 보는 것을 추천한다.)



const Koa = require('koa');

const Path = require('path');

const Serve = require('koa-static');

const Router = require('koa-router');

const Template = require('koa-ejs');

const Mongoose = require('mongoose');

const BodyParser = require('koa-bodyparser');


const TodolistRouter = require('./todolist');


Mongoose.Promise = global.Promise;


Mongoose.connect('mongodb://localhost/todolist').then(

  (res) => {

    console.log('MongoDB와 연결되었습니다.');

  },

  () => {

    console.error('MongoDB와 연결에 실패하였습니다. MongoDB 서버를 확인하세요.');    

  }

);


...



Mongoose.connect 메서드가 Promise 객체의 형태로 되어 있다는 것에 주목하자. Mongoose의 최신 버전부터는 Promise 문법을 지원한다. 따라서 DB 연결이 성공하면 첫 번째 파라미터인 Resolve가 실행 되고, 실패하면 Reject가 실행되는 구조를 볼 수 있다. 서비스를 실행하면 터미널에 아래와 같은 문자열이 출력되는 것을 볼 수 있다.





그림 2. Mongoose로 MongoDB 서버와 연결



2. Schema



Mongoose 모듈을 이용하면 ODM 스키마를 정의할 수 있다. 우리 프로젝트에서 Mongoose의 스키마를 이용하여 Todo 모델을 만들어보자. Todolist 프로젝트의 src 디렉터리 내부에 models 디렉터리를 생성하고, todolist.js 파일을 추가하자.





그림 3. Todolist 프로젝트 구조



다음 src/models/todolist.js 파일에 아래와 같이 코드를 추가한다.



const Mongoose = require('mongoose');

const Schema = Mongoose.Schema;


const todoSchema = new Schema({


  todo : String,

  dt_limited : {

    dt_start : Date,

    dt_end : Date

  },

  dt_created : {

    type : Date,

    default : Date.now

  },

  dt_updated : {

    type : Date,

    default : Date.now

  },

  finished : {

    type : Boolean,

    default : false

  }


});


exports.todoModel = Mongoose.model('Todolist', todoSchema);



스키마는 Mongoose의 하위 클래스인 Schema로 새로운 객체를 생성하여 정의한다. 이 스키마는 MongoDB에서 데이터를 관리할 때 같은 형태의 Document를 보장한다. Mongoose의 스키마는 MongoDB의 하나의 컬렉션으로 연결되며, 내부에 정의된 멤버들은 Document 구조를 정의한다.


Mongoose에서는 개발자가 스키마를 정의하기 위한 다양한 기능을 제공하는데 우리는 여기서 일부만 사용 할 예정이다. 자세하게 알고 싶거나 다른 기능들이 필요하다면, 아래의 링크를 통하여 좀 더 자세히 확인 할 수 있다.



Mongoose Schema에서 Document 멤버를 정의할 때 사용 가능한 타입 및 종류 (클릭)



스키마 정의가 완료되었다면 이전 포스팅에서 만들었던 Koa-Router와 연결하여 CRUD (Create, Read, Update, Delete) 기능을 구현 해보자. 아래의 코드를 src/todolist/restapi.js 파일에 작성하자.



1) getTodoList



const objectId = require('mongoose').Types.ObjectId;

const { todoModel } = require('../models/todolist');


exports.getTodoList = async (ctx) => {


  const { todo_id } = ctx.params;


  try{


    if(todo_id){


      const obj_todo = await todoModel.findOne({'_id' : objectId(todo_id)});


      ctx.body = obj_todo;


    }

    else{


      const arr_todolist = await todoModel.find({}).exec();


      ctx.body = arr_todolist;

    }


  }

  catch(error){


    ctx.status = 500;

    ctx.body = `Todo 데이터를 조회할 수 없습니다. 서비스를 확인하세요.`;


  }


};



제일 먼저 조회 기능을 만들어보자. 조회 기능은 Get 메서드를 이용하여 구현하며 두가지 기능을 제공한다. URI에 파라미터로 todo_id 를 받으면 하나의 데이터만 조회하여 리턴하고, 파라미터가 없으면 MongoDB에 저장된 모든 데이터를 리턴한다. 코드에서 초록색으로 표시된 부분은 async / await 를 이용하여 비동기 방식으로 조회하는 부분이다. DB 조회 기능은 CRUD 기능 중에서 가장 오랜 시간을 소비하는 기능이기 때문에 비동기로 동작하는 것이 프로그램의 안전성에 좋다.



앞에서 만든 Mongoose 모델을 최상단에 const { todoModel } = require('../models/todolist'); 로 명시하여 모델을 restapi.js 모듈에서 사용할 수 있도록 하였으며, 불러온 모델 객체의 findOne(), find() 메서드를 가지고 데이터를 조회한다.


이 때, 주의해야 할 점은 각 메서드마다 리턴하는 데이터가 다르다는 것이다. 쿼리를 리턴하는 find() 메서드는 반드시 exec() 메서드로 실제 DB에 조회를 요청 해야만 값을 받을 수 있지만, findOne()과 같이 바로 값을 리턴하는 메서드는 exec()를 호출할 필요가 없다. Mongoose의 모델 메서드들이 리턴하는 값은 파라미터에 따라 달라지기도 하기 때문에 사용하기 전에 API 문서에서 반드시 확인하고 사용하자.



2) createTodolist



exports.createTodolist = async (ctx) => {


  const { body } = ctx.request;


  // 파라미터 존재여부 체크


  try{


    if(body){


      // 스키마 객체 생성 및 데이터 추가

      const todo = new todoModel();


      todo.todo = body.todo;

      todo.sublist = body.sublist;

      todo.finished = body.finished;

      todo.dt_limited = body.dt_limited || {};


      const newTodo = await todo.save();


      ctx.status = 200;

      ctx.body = newTodo;


    }

    else{


      ctx.throw(400, 'Todolist 데이터가 전달되지 않았습니다.');


    }


  }

  catch(err){


    ctx.throw(400, err);


  }


};



다음은 등록 기능을 구현해보자. 등록 기능은 Post 메서드를 사용하며 클라이언트가 전달한 데이터를 MongoDB에 저장한다. 주의 할 점은 Post와 Put 메서드는 클라이언트가 데이터를 전달할 때 form.body로 전달한다는 것이다. 저번 포스팅에서 Post로 전달되는 데이터를 받기위해 Koa-bodyparse 모듈을 사용한다는 것을 알아본 바 있다. Koa-bodyparse를 이용하면 클라이언트가 전달한 데이터를 const { body } = ctx.request; 코드로 받을 수 있다.


조회 기능에서는 Mongoose 모델을 직접 사용했지만, 데이터를 추가하거나 수정하기 위해서는 모델 객체를 새로 생성해야 한다. const todo = new todoModel(); 로 모델 객체를 생성하고, 스키마의 구조와 일치하도록 모델 객체의 멤버에 값을 추가한다. 마지막으로 모델 객체의 save() 메서드를 이용하여 데이터를 MongoDB에 저장한다.



3) modifyTodolist



exports.modifyTodolist = async (ctx) => {


  try{


    const { body } = ctx.request;

    const { todo_id } = ctx.params;


    if(todo_id){


      // 유효성 검사

      const res_query = await todoModel.findById(todo_id);


      if(!res_query){


        ctx.throw(404, '요청하신 Todo 데이터는 존재하지 않습니다.');


        return;


      }


      const query = {

        '_id' : objectId(todo_id)

      };


      const updateTodo = {

        'todo' : body.todo,

        'dt_updated' : new Date(),

        'dt_limited' : body.dt_limited,

        'finished' : body.finished

      };


      const status= await todoModel.findOneAndUpdate(query, updateTodo);


      ctx.status = 200;

      ctx.body = status;


    }

    else{


      ctx.throw(400, 'Todo 데이터의 ID는 필수항목 입니다.')


    }


  }

  catch(err){


    ctx.throw(400, err);


  }


};



다음은 수정 기능을 구현해보자. 수정 기능의 경우 등록 기능과 코드가 거의 동일하지만, 수정하고자 하는 데이터를 찾아야 하는 단계가 추가로 필요하다. MongoDB의 데이터는 Document 구조로 등록 되는데, 각 Document에는 식별 할 수 있는 유일한 id 값을 가지고 있다. 이 id 값은 objectId 라는 특수한 타입으로 되어 있으며, 조회 시에도 objectId로 변환하는 과정이 필요하다. Mongoose 모델의 findById() 메서드를 이용하면, 문자열을 objectId로 자동 변환 후에 조회를 해주기 때문에 편리하다.



만약, 코드에서 문자열을 objectId로 형변환을 해야되는 경우라면 최상단에 선언했던 const objectId = require('mongoose').Types.ObjectId;를 이용하여 objectId 타입으로 변환 할 수 있다. 위의 코드에 이러한 예제 코드가 잘 나와있다.



특정 데이터를 찾는 것과 동시에 수정하려면 Mongoose의 findOneAndUpdate() 메서드를 사용한다. 이 메서드는 두 개의 파라미터를 전달받는데 하나는 특정 데이터를 찾기 위한 쿼리이고, 마지막 하나는 수정 할 데이터를 전달한다. 수정이 정상적으로 이루어진다면 수정되기 전의 데이터를 리턴한다.

 


4) deleteTodolist



exports.deleteTodolist = async (ctx) => {


  // 전달된 Todo ID 확인

  const { todo_id } = ctx.params;


  try{


    if(todo_id){


      const res_delete = await todoModel.findByIdAndRemove(todo_id).exec();


      if(!res_delete){


        ctx.status = 204;


      }

      else{


        ctx.status = 500;

        ctx.body = "요청한 Todolist 데이터를 찾을 수 없습니다. 제거되었거나 ID가 잘못 전달 되었습니다.";


      }


    }


  }

  catch(err){


    ctx.throw(400, err);


  }


};



마지막으로 삭제 기능을 구현해보자. 삭제 기능은 특정 데이터를 삭제하는 기능으로 todo_id 라는 파라미터를 받는다. 예상 하겠지만, todo_idMongoDB Document의 _id 값이다. Mongoose 모델의 findByIdAndRemove() 메서드를 이용하여 데이터를 찾고 삭제할 수 있다.



모든 코드의 구현이 완료되었다. 이제 우리가 만든 Todolist 서비스를 실행하여 Postman으로 기능을 테스트 해보자.



3. Postman 테스트



Nodemon으로 서비스를 실행하고, 등록 기능먼저 테스트를 진행 해보자. (데이터가 없으면 조회해도 나오지 않으므로.)



nodemon src/index.js



Post 메서드로 데이터를 전달해야 하기 때문에 Postman에서 아래의 스크린샷 처럼 헤더를 작성하고, Send 버튼을 클릭한다.




그림 4. Todolist 등록



데이터가 제대로 들어갔는지 확인하기 위해서 Postman의 HTTP 메서드를 Get로 변환하고 Send 버튼을 클릭 해보자.




그림 5. 등록된 Todolist 조회



데이터가 제대로 등록이 되었다! 자, 그럼 Post 메서드로 여러 개의 데이터를 등록하고 Get 메서드로 다시 조회해보자. 등록된 데이터들이 모두 출력되는 것을 볼 수 있다.



Builder 
Postman 
Team Library 
Runner 
GET v 
19:43.441Z 
Import 
localhost:999Wtodolist/ 
Headers (4) 
IN SYNC 
No Environment 
Params 
Send 
Save 
Size: 600 B 
Body 
Pretty 
2. 
3 
6 
8 
9 
13 
16 
18 
19 
Cookies 
status: 200 0K 
Time: 9 ms 
"_id": 
"598ffcf90B7e053af1c83885' 
'todo": "Koa-framework 
"finished" : 
false, 
"dt updated": 
dt_created" : 
fed": 
"2017-08-13Tø,7 
"_id": 
'todo": 
"598ffd8f0B7e053af1c83886" , 
"finished": true, 
"dt updated": 
dt_created" : 
"dt—timi fed": 
"2017-08-13Tø,7:


그림 6. 등록된 Todolist 전체 조회



다음은 데이터를 수정해보자. 조회할 데이터의 _id 값을 복사하고, HTTP 메서드를 PUT으로 바꾼 후 URI 뒤에 복사한 _id 값을 붙이자. 다음 데이터를 조금 수정하고 Send 버튼을 눌러보자.



Builder 
Postman 
Team Library 
Runner 
PUT v 
Authorization 
O 
form-data 
Import 
IN SYNC 
localhost:9999/tod olist,'598ffd8f097e053af183886 
No Environment 
Params 
Send 
Headers (1) 
Body • 
Pre-request Script 
O 
x-v.wvv-form-urlencoded 
raw 
O 
binary 
Tests 
JSON (application/json) V 
2 
5 
Body 
"todo": " 
"dt_t imi ted" . 
"2017-10-13", 
"finished": true 
Cookies 
Headers (4) 
status: 200 0K 
Time: 7 ms 
Save 
Cookies Code 
Size: 604 B 
Connection keep-alive 
Content-Length 456 
Content-Type applicationljson; charset=utf-8


그림 7. Todolist 수정



데이터가 제대로 변경되었다. 마지막으로 HTTP 메서드를 DELETE로 바꾸고 Send 버튼을 눌러보자.



Builder 
Postman 
Team Library 
Runner 
DELETE v 
Authorization 
Cookies 
Pretty 
2. 
3 
6 
8 
9 
Import 
IN SYNC 
localhost:9999/tod olist,'598ffd8f097e053af183886 
No Environment 
Params 
Send 
Headers (1) 
Headers (4) 
"_id": 
Body • 
No Auth 
Pre-request Script 
Tests 
status: 200 0K 
Time: 9 ms 
Save 
Cookies Code 
Size: 600 B 
"598ffcf90B7e053af1c83885" , 
'todo": "Koa-framework 
"finished". false, 
"dt updated": 
dt_created" : 
"dt—timi fed": 
"_id": 
"598ffd8f0B7e053af1c83886" ,



그림 8. Todolist 삭제



다시 GET 으로 조회하면 데이터가 삭제된 것을 볼 수 있다. 와우~! 서비스가 점점 모양을 갖추고 있다 !!



Builder 
Postman 
Team Library 
Runner 
GET v 
Authorization 
Cookies 
Pretty 
3 
6 
8 
9 
lø 
11 
Import 
localhost:999Wtodolist/ 
IN SYNC 
No Environment 
Params 
Send 
Headers (1) 
Headers (4) 
Pre-request Script 
No Auth 
Tests 
status: 200 0K 
Time: 8 ms 
Save 
Cookies Code 
Size: 376 B 
"_id": 
"598ffcf90B7e053af1c83885" , 
'todo": "Koa-framework 
"finished". false, 
"dt updated": 
dt_created" : 
fed":


그림 9. 삭제된 Todolist



다음 포스팅에서는 Mongoose 모델로 전역 메서드와 사용자 정의 메서드를 만드는 방법과 Joi 모듈을 이용하여 클라이언트가 전달한 데이터의 유효성을 검사하는 방법에 대해 알아보자. 현재 포스팅까지 진행한 파일은 아래의 주소에서 확인 할 수 있다.



https://github.com/kim1124/Mini_project.git


반응형

'다시 하자 Back-End > Koa framework' 카테고리의 다른 글

[KoaJS] 3. Router  (0) 2017.09.04
[KoaJS] 2. KoaJS 개발을 위한 환경설정  (0) 2017.08.09
[KoaJS] 1. KoaJS 시작하기  (0) 2017.08.03