클라우드 컴퓨팅 & NoSQL/Vert.x & Node.js

빠르게 훝어 보는 node.js - #7 mongoose ODM 을 이용한 MongoDB 연동

Terry Cho 2014. 4. 9. 00:43

빠르게 훝어보는 node.js

#7- mongoose ODM 을 이용한 MongoDB 연동

조대협 (http://bcho.tistory.com)


계정본이 http://bcho.tistory.com/1094 올라와 있습니다.


Mongoose ODM을 이용한 MongoDB의 연동

MongooseMongoDB 기반의 nodejs ODM (Object Data Mapping) 프레임웍이다. 앞에서 mongo-native에 대해서 알아봤는데, 그렇다면 mongoose는 무엇인가? 쉽게 생각하면 mongo-native JDBC 드라이브러를 이용한 데이타 베이스 프로그래밍이고, mongoose는 자바의 JPA/Hibernate/MyBatis와 같은 OR Mapper와 같은 개념이다.

mongodb 내의 데이타를 node.js내의 객체로 정의해준다. ODM 개념을 이용하여, MVC 개념들을 조금더 쉽게 구현할 수 있도록 도와 주며, 특히 javascript가 가지고 있는 한계성중인 하나인 모호성을 보완해준다. mongodb json을 저장할때, collection에 들어가는 데이타의 형태(스키마)가 없기 때문에 자유도가 높기는 하지만 반대로 RDBMS에서 정의된 스키마 개념이 없이 때문에 어떤 컬럼이 있는지 코드에서로만은 파악하기가 어려울 수 있다. 이런점을 보완하는 개념이 mongoose의 스키마의 개념인데, 직접 코드를 살펴보자

다음 예제는 HTML에서 이름과 메모를 받아서 DB에 저장하고 조회 하는 예제이다.



<!DOCTYPE html>

<html>

<head>

    <title></title>

</head>

<body>

    <form name="memo" method="post" action="/insert">

        name <input type="text" name="username"/>

        <br>

        message <input type="text" name="memo"/>

        <button type="submit" >Submit</button>

    </form>

</body>

</html>

먼저 실행에 앞서서 npm install mongoose를 이용해서 mongoose 모듈을 설치해야 한다.

다음은 express /app.js 파일이다.

var express = require('express');

var routes = require('./routes');

var user = require('./routes/user');

var http = require('http');

var path = require('path');

 

mongoose를 사용하기 위해서 해당 모듈을 import한다.

var mongoose = require('mongoose');

 

다음으로 Schema를 정의하는데, 이 스키마는 username memo라는 필드를 가지고 있으며 각각의 필드는 String 데이타 타입을 갖는다.

var MemoSchema= mongoose.Schema({username:String,memo:String});

이 스키마를 이용해서 아래와 같이 모델을 정의하는데, 첫번째 인자는 이 모델이 mongodb에 저장될 Collection이름(테이블명)이 되고, 두번째 인자는 이 모델을 정의하는데 사용할 스키마(앞에서 정의한)를 지정한다.

참고

 

mongoose에서는 다양한 데이타 타입을 이용하여 계층화된 스키마를 정의하는 게 가능하다. 아래는 http://mongoosejs.com/docs/schematypes.html 에 정의된 예제 스키마 중의 하나이다.

var schema = new Schema({

  name:    String,

  binary:  Buffer,

  living:  Boolean,

  updated: { type: Date, default: Date.now }

  age:     { type: Number, min: 18, max: 65 }

  mixed:   Schema.Types.Mixed,

  _someId: Schema.Types.ObjectId,

  array:      [],

  ofString:   [String],

  ofNumber:   [Number],

  ofDates:    [Date],

  ofBuffer:   [Buffer],

  ofBoolean:  [Boolean],

  ofMixed:    [Schema.Types.Mixed],

  ofObjectId: [Schema.Types.ObjectId],

  nested: {

    stuff: { type: String, lowercase: true, trim: true }

  }

})

 

 

var Memo = mongoose.model('MemoModel',MemoSchema); // MemoModel : mongodb collection name

다음으로 express를 사용하기 위한 기본 설정을 아래와 같이 하고

var app = express();

 

// all environments

app.set('port', process.env.PORT || 3000);

app.set('views', path.join(__dirname, 'views'));

app.set('view engine', 'ejs');

app.use(express.logger('dev'));

app.use(express.json());

app.use(express.urlencoded());

app.use(express.methodOverride());

app.use(app.router);

app.use(express.static(path.join(__dirname, 'public')));

 

HTTP/POST로 들어오는 요청을 mongodb에 저장하는 로직을 구현한다.

 

app.post('/insert', function(req,res,err){

    var memo = new Memo({username:req.body.username,memo:req.body.memo});

    memo.save(function(err,silence){

if(err){

            console.err(err);

            throw err;

}

res.send('success');

    });

});

Memo 모델 클래스를 이용해서 memo 객체를 만드는데, username은 앞의 index.html의 폼에서 입력받은 username 값을, memo form에서 입력받은 memo 값으로 memo 객체를 생성한다.

저장하는 방법은 간단하다. memo객체의 save라는 메서드를 호출하고, 비동기 IO이기 때문에, callback 함수를 바인딩 하였다. callback함수에서는 데이타 저장 처리가 끝나면, res.send success 메세지를 출력한다.

다음으로 mongoose mongodb에 연결하고, http server를 기동시켜 보자

mongoose.connect('mongodb://localhost/terrydb',function(err){

    if(err){

        console.log('mongoose connection error :'+err);

        throw err;

    }

    http.createServer(app).listen(app.get('port'), function(){

      console.log('Express server listening on port ' + app.get('port'));

    });

});

mongoose.connect를 이용하여 mongodb에 접속한다. 이때, 접속 URL을 써주면 된다. 여기서는 localhost terrydb를 사용하도록 정의하였다. 여기서 지정한 옵션 이외에도, 포트 #, connection pool 설정등 다양한 옵션을 적용할 수 있다. 자세한 내용은 mongoose 문서 http://mongoosejs.com/docs/connections.html 를 참고하기 바란다.

connect 메서드는 두번째 인자로 callback 함수를 받는데, 이 예제에서는 callback함수에서 http server를 기동하였다. 이는 mongodb가 연결된 다음에 서비스가 가능하기 때문에, mongodb 연결후에 request를 받기 위해서 callback에서 http server를 기동한 것이다.

그러면 실행을 하고 결과를 보자. http://localhost:3000으로 접속하여 위에서 나타난 index.html 폼에 데이타를 넣고, robomongo를 이용해서 그 결과를 살펴보면 아래와 같이 값이 들어간 것을 확인할 수 있다.



자아, mongoose를 이용해서 데이타를 저장하였다. 그러면 추가로 데이타를 조회하는 기능을 구현해 보자

다음은 http://localhost:3000/users/{username}이 들어오면 {username}값의 데이타를 조회해주는 함수이다.

app.get('/users/:username', function(req,res,err){

    var memos = new Memo();

    Memo.findOne({'username':req.params.username},function(err,memo){

        if(err){

            console.err(err);

            throw err;

        }

        console.log(memo);

        res.send(200,memo);

    });

});

app.get('/users/:username' 에서 :username 을 이용하여 URL Parameter username을 받고

Memo 모델의 findOne이라는 메서드를 이용해서 데이타를 가져왔다., findOne query 조건에 부합하는 데이타중에 하나만 리턴하는 함수이다첫번째 인자가 검색 조건인데 여기서는 데이타베이스에서 필드가 username인 필드의 값이 앞에 URL에서 받은 username과 일치하는 레코드를 받도록 하였다.

두 번째 인자는 데이타가 리턴되었을때 수행되는 callback함수 인데, callback 함수 두번째 인자인 memo에 리턴되는 데이타가 저장된다. memo객체로는 JSON 데이타가 리턴되고, JSONres.send(200,memo); 을 이용하여, 리턴하였다. 이 코드를 추가한 후에, 실행해보면 다음과 같은 결과를 얻을 수 있다.



이번에는 memomodels 전체 테이블을 쿼리 해보자 (select * from memomodels)

app.get('/users', function(req,res,err){

    var memos = new Memo();

    Memo.find().select('username').exec(function(err,memos){

        if(err){

            console.err(err);

            throw err;

        }

        console.log(memos);

        res.send(memos);

    });

});

Memo.find()를 하면되는데, 예제에서 .select(‘username’)을 추가하였다. 이 메서드는 select username from memomodels 라고 생각하면 된다. 즉 쿼리해온 값중에서 특정 필드값만을 리턴하도록 하는 것이다.

이 함수를 추가해서 실행해보면 다음과 같은 결과를 얻을 수 있다.



 

아래는 http://mongoosejs.com/docs/queries.html 에서 참고한 샘플인데, where문을 이용한 검색 조건 지정에서 부터, select해 오는 개수, Sorting order, 특정 필드만 가지고 오는 등의 다양한 쿼리 조건을 지정한 예제이다.

Person

.find({ occupation: /host/ })

.where('name.last').equals('Ghost')

.where('age').gt(17).lt(66)

.where('likes').in(['vaporizing', 'talking'])

.limit(10)

.sort('-occupation')

.select('name occupation')

.exec(callback);

 

이외에도 다양한 쿼리 조건을 지정할 수 있으니 자세한 내용은 http://mongoosejs.com/docs/queries.html 를 참고하기 바란다.

 

데이타 validation 하기

mongoose의 유용한 기능중의 다른 하나가 Schema에 대한 validation이다. Schema를 정의할 때, 데이타 타입이나 기타 규칙을 정해놓고, 이 규칙을 벗어나면 에러 처리를 해주게 하는 것인데, 웹 개발등에서는 워낙 일반적인 내용이니 구체적인 개념 설명은 생략하겠다.

Validator를 사용하는 방법은 앞에 구현한 코드 부분에서 Schema를 정의 한 부분을 다음과 같이 변경 한다.

// define validator

function NameAlphabeticValidator(val){

    return val.match("^[a-zA-Z\(\)]+$");

}

function MemoLengthValidator(val){

    if(val.length>10) return null;

    return val;

}

 

// schema definition with validation

var MemoSchema= mongoose.Schema({

                    username:{type:String,validate:NameAlphabeticValidator}

                    ,memo:{type:String,validate:[

                                    {validator:MemoLengthValidator,msg:'memo length should be less than 10'},

                                    {validator:NameAlphabeticValidator,msg:'PATH `{PATH}` should be alphabet only. Current value is `{VALUE}` '}

                                    ]}

    });

var Memo = mongoose.model('MemoModel',MemoSchema); // MemoModel : mongodb collection name

 

먼저 validation rule을 정의해야 하는데, validation rule을 함수로 구현하면 된다.

이를 validator라고 하는데, NameAlphabeticValidator는 들어오는 인자가 영문일 경우에만 PASS하고, 숫자나 특수 문자가 들어오면 오류 처리를 한다. 다음으로 정의한 MemoLengthValidator의 경우에는 문자열의 길이가 10자 이상인 경우에 에러 처리를 한다.

이렇게 정의한 validator를 스키마 정의시 각 데이타 필드에 지정하면 된다. username에는 위와 같이 validator NameAlphabeticValidator를 적용하였다.

다음으로 memo에는 동시에 NameAlphabeticValidator MemoLengthValidator 두 개를 동시에 적용하였는데, 적용할때 msg 인자로 validation이 실패했을 때 리턴해주는 메세지도 함께 지정하였다.

이 메세지 부분에서 NameAlphabeticValidator 에서 발생하는 에러 메세지를 주의 깊게 보면 {PATH} {VALUE} 가 사용된 것을 볼 수 있는데, {PATH}는 이 에러가 발생하는 JSON 필드명을 그리고 {VALUE}는 실제로 입력된 값을 출력한다. 테스트를 해보면



message부분에 특수문자와 숫자를 넣었다. 실행 결과는 아래와 같이 에러 메세지들이 console로 출력되는데, 아래서 보는 바와 같이 message 부분에 {PATH} {VALUE}가 각각 memo Memo-1 값으로 대체 된것을 확인할 수 있다



 

mongoose는 어디에 쓰는게 좋을까?

mongo native가 있고, mongoose가 있는데, 그러면 각각을 어디에 쓰느냐? 이 질문은 JDBC JPA를 언제 쓰느냐? 와 같은 질문과 같지 않을까 싶다.

mongoose를만든 커미터에 따르면 mongodb-native모듈이 mongoose보다 빠르다고 한다. 즉 조금 더 유연한 mongodb에 대한 access가 필요하고 높은 성능을 요구할 경우에는 mongodb-native를 사용하고, 정형화 되고 스키마 정의를 통한 명시성 확보가 필요하며 validation등을 효율적으로 하고자 할때 mongoose를 사용하는 것이 좋다. 실제 프로그램에서는 위의 용도에 맞게 두 프레임웍을 섞어 쓰는 것이 좋다