11.3 Node.js(express)와 MongoDB 연동 RESTful API - 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-example && cd mongoose-example
$ npm init --yes
$ npm install --save --save-exact express body-parser mongoose

package.json은 아래와 같다.

{
  "name": "mongoose-example",
  "version": "1.0.0",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "body-parser": "1.15.2",
    "express": "4.14.0",
    "mongoose": "4.5.10"
  }
}

3.2. 디렉터리 구조

mongoose-example/
├── models/
│   └── user.js
├── routes/
│   └── user.js
├── app.js
└── pakage.json

4. Connection

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

var express    = require("express");
var bodyParser = require("body-parser");
var mongoose   = require("mongoose");
var app  = express();
var port = process.env.port || 3000;

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// CONNECT TO MONGODB SERVER
var db = mongoose.connection;

db.on("error", console.error.bind(console, "connection error:"));
db.once("open", function() {
  console.log("connect successfully");
});

mongoose.connect("mongodb://localhost/mydatabase");

mongoose.connection의 error, open 등의 이벤트를 활용하여 접속 실패, 접속 성공 시의 handling이 가능하다.

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/user.js에 아래의 코드를 작성한다.

var mongoose = require("mongoose");

// Define Schemes
var userSchema = new mongoose.Schema({
  name: String,
  username: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  admin: Boolean,
  location: String,
  meta: {
    age: Number,
    website: String
  }
},
{
  timestamps: true
});

// Create Model & Export
module.exports = mongoose.model("User", userSchema);

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는 명시적으로 정의하지 않아도 insert()나 save() 메서드 호출시 자동으로 추가된다.

Schema는 Model 생성 시 인자로서 전달되어지며 더이상 사용되지 않는다.

5.2. Model

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

아래는 model 생성의 예이다.

var User = mongoose.model("User", userSchema);

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

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

var userSchema = new mongoose.Schema({..}, { collection: "my-collection-name" });

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

var lee = new User({
  name: "Lee",
  username: "user01",
  password: "1234",
  ...
});

// or

var lee = new User();
lee.name = "Lee";
lee.username = "user01";
lee.password = "Lee";
...

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

...
module.exports = mongoose.model("User", userSchema);

7. Statics model methods & Document instance methods

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

7.1. Statics model methods(Statics)

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

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

var User = mongoose.model("User", userSchema);

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

Statics model methods

7.2. Document instance methods(Methods)

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

var User = mongoose.model("User", userSchema);

// model instance (= document)
var lee = new User({
  name: "Lee",
  username: "user01",
  password: "1234"
});

// Document instance method
lee.save(function(err) {
  if (err) throw err;
  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
/api/users POST 단일 또는 복수의 user 생성
/api/users GET 모든 user 조회
/api/users/:id GET _id로 user 조회
/api/users/username/:username GET username으로 user 조회
/api/users/:id PUT _id로 user 조회 후 수정
/api/users/username/:username PUT username으로 user 조회 후 수정
/api/users DELETE 모든 user 삭제
/api/users/username/:username DELETE username으로 user 조회 후 삭제

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

var express = require("express");
var router = express.Router();

var User = require("../models/user");

// Create
router.post("/users", function(req, res) {
});

// Find All
router.get("/users", function(req, res, next) {
});

// Find One
router.get("/users/username/:username", function(req, res) {
});

// Find By ID
router.get("/users/:id", function(req, res) {
});

// GET A USER, THEN UPDATE
router.put("/users/:id", function(req, res) {
});

// FIND BY ID AND UPDATE
router.put("/users/:id", function(req, res) {
});

// FIND AND UPDATE
router.put("/users/username/:username", function(req, res) {
});

// REMOVE ALL
router.delete("/users/", function(req, res) {
});

// FIND AND REMOVE
router.delete("/users/username/:username", function(req, res) {
});

module.exports = router;

8.1. Create

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

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

// Create
router.post("/users", function(req, res) {
  User.create(req.body, function (err, users) {
    if(err) return res.status(500).send(err);
    res.send("User Create successfully:\n" + users);
  });
});

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

// Create
router.post("/users", function(req, res) {
  var user = new User(req.body);

  user.save(function(err, doc){
    if(err) return res.status(500).send(err);
    res.send("User saved successfully:\n" + doc);
  });
});

8.2. Read

// Find All
router.get("/users", function(req, res) {
  User.find({ }, function(err, users) {
    if(err)           return res.status(500).send(err);
    if(!users.length) return res.status(404).send({ err: "User not found" });
    res.send("User find successfully:\n" + users);
  });
});

// Find One
router.get("/users/username/:username", function(req, res) {
  User.findOne({ "username" : req.params.username }, function(err, user) {
    if(err)   return res.status(500).send(err);
    if(!user) return res.status(404).send({ err: "User not found" });
    res.send("User findOne successfully:\n" + user);
  });
});

// Find By ID
router.get("/users/:id", function(req, res) {
  User.findById(req.params.id, function(err, user) {
    if(err)   return res.status(500).send(err);
    if(!user) return res.status(404).send({ err: "User not found" });
    res.send("User findById successfully:\n" + user);
  });  
});

8.3. Update

// FIND BY ID AND UPDATE
router.put("/users/:id", function(req, res) {
  User.findByIdAndUpdate(req.params.id, req.body, {new: true}, function(err, user) {
    if(err) return res.status(500).send(err);
    res.send("User findByIdAndUpdate successfully:\n" + user);
  });
});

// FIND AND UPDATE
router.put("/users/username/:username", function(req, res) {
  User.findOneAndUpdate({ username: req.params.username }, req.body, {new: true}, function(err, user) {
    if(err) return res.status(500).send(err);
    res.send("User findOneAndUpdate successfully:\n" + user);
  });
});

8.4. Delete

// REMOVE ALL
router.delete("/users/", function(req, res) {
  User.remove({ }, function(err) {
    if(err) return res.status(500).send(err);
    res.send("User all deleted successfully");
  });
});

// FIND AND REMOVE
router.delete("/users/username/:username", function(req, res) {
  User.remove({ username: req.params.username }, function(err) {
    if(err) return res.status(500).send(err);
    res.send("User deleted successfully");
  });
});
Back to top
Close