11.3 Node.js(express)와 MongoDB 연동 RESTful API - Mongoose

Mongoose

mongoose logo

1. Introduction

Mongoose는 Node.js와 MongoDB를 위한 ODM(Object Data Mapping) library이다. Java 기반의 Hibernate. iBatis 등의 ORM(Object Relational Mapping)과 유사한 개념이다.

ODM의 사용은 코드 구성이나 개발 편의성 측면에서 장점이 많다. 호환성이 없는 프로그래밍언어(JavaScript) Object와 MongoDB의 데이터를 Mapping하여 간편한 CRUD를 가능하게 한다.

필요에 따라 확장 및 변경이 가능한 자체 검증(Validation)과 타입 변환(Casting)이 가능하며 Express와 함께 사용하면 MVC Concept 구현이 용이하다.

2. RESTful API

REST는 Representational state transfer의 약자로 World Wide Web과 같은 분산 하이퍼미디어 시스템에서 운영되는 소프트웨어 아키텍처 스타일이다.

HTTP 프로토콜을 의도에 맞게 정확히 활용하여 디자인하도록 유도하고 있기 때문에 디자인 기준이 명확해지며, 의미적인 범용성을 지니므로 중간 계층의 컴포넌트들이 서비스를 최적화하는 데 도움이 된다. REST의 기본 원칙을 충실히 지킨 서비스 디자인을 “RESTful” 이라고 표현한다.

REST에서 가장 중요하며 기본적인 규칙은 아래 두 가지이다.

  • URI는 정보의 자원을 표현해야 한다.
  • 자원에 대한 행위는 HTTP Method로 표현한다.

HTTP Method 사용의 예는 아래와 같다.

Verb Action Path Used for
GET index /books 모든 서적 리스트 조회
GET retrieve /books/:id 특정 서적 조회
POST create /books 신규 서적 생성
PUT replace /books/:id 특정 서적 갱신(존재하지 않으면 생성)
PATCH update /books/:id 특정 서적 갱신
DELETE delete /books 모든 서적 삭제
DELETE delete /books/:id 특정 서적 삭제

3. Setting

3.1. Install

mongoose-example 디렉터리를 생성하고 npm을 사용하여 Mongoose 모듈을 install한다.

$ mkdir mongoose-exam && cd mongoose-exam
$ npm init -y
$ npm install express dotenv mongoose

package.json의 scripts를 다음과 같이 수정한다.

{
  "name": "mongoose-exam",
  "version": "1.0.0",
  "scripts": {
    "start": "node app"
  },
  "dependencies": {
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "mongoose": "^5.12.5"
  }
}

3.2. 디렉터리 구조

mongoose-exam/
├── models/
│   └── todo.js
├── routes/
│   └── todos.js
├── .env
├── app.js
└── pakage.json

4. Connection

환경변수를 파일로 관리하기 위해 dotenv를 사용한다. mongoose에 접속하기 위한 계정과 비밀번호를 환경변수로 관리하기 위해 dotenv를 사용하기로 한다.

.env파일에 아래와 같이 환경변수를 작성한다.

# port number
PORT=4500
# MongoDB URI
# MONGO_URI=mongodb://localhost:27017/<db-name>
MONGO_URI=mongodb://localhost:27017/todos

루트 디렉터리에 app.js를 생성한다. mongoose 모듈을 require하고 connect 메소드로 MongoDB에 connect한다.

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');

const app = express();

const { PORT, MONGO_URI } = process.env;

app.use(express.static('public'));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// CONNECT TO MONGODB SERVER
mongoose
  .connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('Successfully connected to mongodb'))
  .catch(e => console.error(e));

app.listen(PORT, () => console.log(`Server listening on port ${PORT}`));

5. Schema & Model

5.1. Schema

RDBMS의 Schema는 데이터베이스를 구성하는 레코드의 크기, 키(key)의 정의, 레코드와 레코드의 관계, 검색 방법 등을 정의한 것이다.

Mongoose의 Schema는 MongoDB에 저장되는 document의 Data 구조 즉 필드 타입에 관한 정보를 JSON 형태로 정의한 것으로 RDBMS의 테이블 정의와 유사한 개념이다.

MongoDB는 Schema-less하다. 이는 RDMS처럼 고정 Schema가 존재하지 않는다는 뜻으로 같은 Collection 내에 있더라도 document level의 다른 Schema를 가질 수 있다는 의미이다.

이는 자유도가 높아서 유연한 사용이 가능하다는 장점이 있지만 명시적인 구조가 없기 때문에 어떤 필드가 어떤 데이터 타입인지 알기 어려운 단점이 있다. 이러한 문제를 보완하기 위해서 Mongoose는 Schema를 사용한다.

models/todo.js에 아래의 코드를 작성한다.

const mongoose = require('mongoose');

// Define Schemes
const todoSchema = new mongoose.Schema({
  todoid: { type: Number, required: true, unique: true },
  content: { type: String, required: true },
  completed: { type: String, default: false }
},
{
  timestamps: true
});

// Create Model & Export
module.exports = mongoose.model('Todo', todoSchema);

Mongoose Schema는 다음의 Data type을 지원한다.

Data Type Description
String 표준 자바스크립트와 Node.js의 문자열 type
Number 표준 자바스크립트와 Node.js의 숫자 type
Boolean 표준 자바스크립트와 Node.js의 불리언 type
Buffer Node.js의 binary type(이미지, PDF, 아카이브 등)
Date ISODate format data type(2016-08-08T12:52:30:009Z)
Array 표준 자바스크립트와 Node.js의 배열 type
Schema.types.ObjectId 12byte binary 숫자에 대한 MongoDB 24자리 hex 문자열(501d86090d371bab2c0341c5)
Schema.types.Mixed 모든 유형의 데이터

primary-key인 _id는 document 내에서 유일함이 보장되는 12 byte binary data이다. 명시적으로 정의하지 않아도 insert()나 save() 메소드 호출시 자동으로 추가된다.

Schema는 Model 생성 시 인자로 전달한 후, 더이상 사용되지 않는다.

5.2. Model

model() 메소드에 문자열과 schema를 전달하여 model을 생성한다. model은 보통 대문자로 시작한다.

아래는 model 생성의 예이다.

const Todo = mongoose.model('Todo', todoSchema);

첫번째 인자는 해당 collection의 단수적 표현을 나타내는 문자열이다. 실제 collection의 이름은 ‘Todos’로 자동 변환되어 사용된다.

collection의 이름을 명시적으로 지정하고자 할 때는 schema 생성시 option을 지정한다.

const todoSchema = new mongoose.Schema({..}, { collection: 'my-collection-name' });

model은 생성자이므로 instance를 생성할 수 있는데 이것은 개별 document를 나타낸다. instance 생성시 생성자에 초기값을 전달하거나 instance 생성 후 속성과 값을 추가하여 document를 생성한다.

const todo = new Todo({
  todoid: 1,
  content: 'MongoDB',
  completed: false,
  ...
});

// or

const todo = new Todo();
todo.todoid = 1;
todo.content = 'MongoDB';
todo.completed = false;
...

models/todo.js의 마지막 라인에서 model을 생성하고 export한다.

...
module.exports = mongoose.model('Todo', todoSchema);

Schema와 Model 그리고 document의 관계를 그림으로 표현하면 아래와 같다.

mongoose-scheme-model-doc

Schema와 Model 그리고 document의 관계

7. Statics model methods & Document instance methods

Statics model methods와 Document instance methods는 혼동하기 쉬우므로 주의가 필요하다.

위 그림에서 살펴본 바와 같이 정의된 스키마로 모델을 생성하고 모델로 도큐먼트를 생성한다. 이때 모델은 자바스크립트의 생성자 함수와 같이 인스턴스(도큐먼트)를 생성하는 역할을 담당한다. 모델의 메소드를 Statics model methods라 부르고 모델이 생성한 인스턴스인 도큐먼트의 메소드를 Document instance methods라 한다. 이는 자바스크립트의 static 메소드와 프로토타입 메소드와 동일한 개념이다.

7.1. Statics model methods(Statics)

model(위의 경우 Todo)이 호출한 메소드는 Statics model method(Statics)이다.

특정 document가 존재하지 않거나 필요가 없는 경우에 유용하다.

const Todo = mongoose.model('Todo', todoSchema);

// Statics model methods
Todo.find({ }, function(err, todo) {
  if(err) throw err;
  console.log(todo);
});

// or

Todo.find({})
  .then(todo => console.log(todo))
  .catch(err => console.log(err))

Statics model methods

7.2. Document instance methods(Methods)

model의 instance(위의 경우 todo)가 호출한 메소드는 Document instance method(Methods)가 된다.

var Todo = mongoose.model('Todo', todoSchema);

// model instance (= document)
var todo = new Todo({
  todoid: 1,
  content: 'MongoDB',
  completed: false
});

// Document instance method
todo.save
  .then(() => console.log('Saved successfully'));

7.3. Custom Statics & Methods

Built-in Statics model methods, Document instance methods 이외에도 사용자 정의 메소드를 추가할 수 있다.

Instance methods

Schema의 methods 프로퍼티에 사용자 정의 메소드를 추가한다.

// define a schema
var animalSchema = new Schema({ name: String, type: String });

// assign a function to the 'methods' object of our animalSchema
animalSchema.methods.findSimilarTypes = function(cb) {
  return this.model('Animal').find({ type: this.type }, cb);
};

Animal model의 instance는 findSimilarTypes method를 사용할 수 있다.

var Animal = mongoose.model('Animal', animalSchema);
var dog = new Animal({ type: 'dog' });

dog.findSimilarTypes(function(err, dogs) {
  console.log(dogs); // woof
});

Statics

Schema의 statics 프로퍼티에 사용자 정의 메소드를 추가한다.

// assign a function to the 'statics' object of our animalSchema
animalSchema.statics.findByName = function(name, cb) {
  return this.find({ name: new RegExp(name, 'i') }, cb);
};

var Animal = mongoose.model('Animal', animalSchema);
Animal.findByName('fido', function(err, animals) {
  console.log(animals);
});

8. CRUD

RESTful API Reference는 아래와 같다.

Route Method Description
/todos POST todo 생성
/todos GET 모든 todo 조회
/todos/todoid/:todoid GET todoid으로 todo 조회
/todos/todoid/:todoid PUT todoid으로 todo 조회 후 수정
/todos/todoid/:todoid DELETE todoid으로 todo 조회 후 삭제

models/todo.js에 아래의 코드를 작성한다.

const mongoose = require('mongoose');

// Define Schemes
const todoSchema = new mongoose.Schema({
  todoid: { type: Number, required: true, unique: true },
  content: { type: String, required: true },
  completed: { type: String, default: false }
},
{
  timestamps: true
});

// Create new todo document
todoSchema.statics.create = function (payload) {
  // this === Model
  const todo = new this(payload);
  // return Promise
  return todo.save();
};

// Find All
todoSchema.statics.findAll = function () {
  // return promise
  // V4부터 exec() 필요없음
  return this.find({});
};

// Find One by todoid
todoSchema.statics.findOneByTodoid = function (todoid) {
  return this.findOne({ todoid });
};

// Update by todoid
todoSchema.statics.updateByTodoid = function (todoid, payload) {
  // { new: true }: return the modified document rather than the original. defaults to false
  return this.findOneAndUpdate({ todoid }, payload, { new: true });
};

// Delete by todoid
todoSchema.statics.deleteByTodoid = function (todoid) {
  return this.remove({ todoid });
};

// Create Model & Export
module.exports = mongoose.model('Todo', todoSchema);

routes/todos.js에 아래의 코드를 작성한다.

const router = require('express').Router();
const Todo = require('../models/todo');

// Find All
router.get('/', (req, res) => {
  Todo.findAll()
    .then((todos) => {
      if (!todos.length) return res.status(404).send({ err: 'Todo not found' });
      res.send(`find successfully: ${todos}`);
    })
    .catch(err => res.status(500).send(err));
});

// Find One by todoid
router.get('/todoid/:todoid', (req, res) => {
  Todo.findOneByTodoid(req.params.todoid)
    .then((todo) => {
      if (!todo) return res.status(404).send({ err: 'Todo not found' });
      res.send(`findOne successfully: ${todo}`);
    })
    .catch(err => res.status(500).send(err));
});

// Create new todo document
router.post('/', (req, res) => {
  Todo.create(req.body)
    .then(todo => res.send(todo))
    .catch(err => res.status(500).send(err));
});

// Update by todoid
router.put('/todoid/:todoid', (req, res) => {
  Todo.updateByTodoid(req.params.todoid, req.body)
    .then(todo => res.send(todo))
    .catch(err => res.status(500).send(err));
});

// Delete by todoid
router.delete('/todoid/:todoid', (req, res) => {
  Todo.deleteByTodoid(req.params.todoid)
    .then(() => res.sendStatus(200))
    .catch(err => res.status(500).send(err));
});

module.exports = router;

8.1. Create

Static method인 create()를 사용하여 post data로 전달받은 todo 정보를 저장한다.

create() 메소드의 첫번째 매개변수에는 object 또는 array를 전달할 수 있다. array에 여러 개의 todo 정보를 담아 한번에 저장할 수 있어 유용하다.

post data로 전달받은 todo 정보를 바탕으로 todo를 생성한 후 instance method save()를 사용하여도 된다.

8.2. Read

8.3. Update

8.4. Delete

9. app.js의 완성

app.js에 라우터를 추가하여 아래와 같이 완성한다.

// ENV
require('dotenv').config();
// DEPENDENCIES
const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');

const app = express();
const port = process.env.PORT || 4500;

// Static File Service
app.use(express.static('public'));
// Body-parser
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// Node의 native Promise 사용
mongoose.Promise = global.Promise;

// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, { useMongoClient: true })
  .then(() => console.log('Successfully connected to mongodb'))
  .catch(e => console.error(e));

// ROUTERS
app.use('/todos', require('./routes/todos'));

app.listen(port, () => console.log(`Server listening on port ${port}`));

Reference

Back to top
Close