블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

빠르게 훝어 보는 node.js - mongoose 스키마와 유용한 기능


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

지난번 mongoose 에 대한 간략한 소개 글 http://bcho.tistory.com/1094 에 이어서 오늘은 mongoose 스키마와, 기타 유용한 기능에 대해서 소개하고자 한다.

 쿼리

 

간단한 삽입,삭제,수정,조회 쿼리이외에 조금 향상된 쿼리를 살펴보자.

자세한 쿼리 사용 방법은 http://mongoosejs.com/docs/documents.html 참고하면 된다.

 

가지 쿼리들을 살펴보면

 

 

var mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/mydb');

var userSchema = mongoose.Schema({

      userid: String,

      sex : String,

      city : String,

      age : Number

});

 

var User = mongoose.model('users',userSchema);

 

// select city from users where userid='terry'

User.findOne({'userid':'terry'}).select('city').exec(function(err,user){

      console.log("q1");

      console.log(user+"\n");

      return;

});

 

 

// select * from users where city='seoul' order by userid limit 5

User.find({'city':'seoul'}).sort({'userid':1}).limit(5).exec(function(err,users){

      console.log("q2");

      console.log(users+"\n");

      return;

});

 

// using JSON doc query

// select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({'city':'seoul', 'age':{$gt:10 , $lt:29}})

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q3");

           console.log(users+"\n");

           return;

});

 

//using querybuilder

//select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({})

      .where('city').equals('seoul')

      .where('age').gt(10).lt(29)

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q4");

           console.log(users+"\n");

           return;

});

 

Figure 20 mongoose 쿼리 예제

 

코드 부분에서는

var mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/mydb');

var userSchema = mongoose.Schema({

      userid: String,

      sex : String,

      city : String,

      age : Number

});

 

var User = mongoose.model('users',userSchema);

 

mongoose 모듈을 로딩하고, mongodb 연결한다. 다음 스키마를 정의 한다. userid, sex, city 문자열로 가지고, age라는 필드를 숫자로 갖는다. 스키마 정의가 끝났으면 스키마를 이용해서 users 컬렉션에 대해서 User 모델 객체를 생성한다.

 

첫번째 쿼리를 살펴보자

// select city from users where userid='terry'

User.findOne({'userid':'terry'}).select('city').exec(function(err,user){

      console.log("q1");

      console.log(user+"\n");

      return;

});

 

첫번째 쿼리는 userid terry 도큐먼트를 쿼리해서 그중에서 city 라는 이름의 필드만 리턴하는 쿼리이다.

실행 결과는 다음과 같다.

 

q1

{ city: 'seoul', _id: 56e62f2c1a2762d26afa6053 }

 

두번째 쿼리를 살펴보자

// select * from users where city='seoul' order by userid limit 5

User.find({'city':'seoul'}).sort({'userid':1}).limit(5).exec(function(err,users){

      console.log("q2");

      console.log(users+"\n");

      return;

});

 

다음은 실행결과이다.

 

q2

{ age: 18,

  city: 'seoul',

  sex: 'female',

  userid: 'cath',

  _id: 56e62f351a2762d26afa6054 },{ age: 23,

  city: 'seoul',

  sex: 'female',

  userid: 'stella',

  _id: 56e62f3c1a2762d26afa6055 },{ age: 29,

  city: 'seoul',

  sex: 'male',

  userid: 'terry',

  _id: 56e62f2c1a2762d26afa6053 },{ age: 27,

  city: 'seoul',

  sex: 'female',

  userid: 'yuna',

  _id: 56e62f411a2762d26afa6056 }

 

users 컬렉션에서 city 필드가 seoul 도큐먼트를 조회한후 결과를 sort({‘userid’:1}) 이용하여 userid 따라 오름차순으로 정렬한 후에, limit(5) 이용하여 결과중 5개만 리턴한다.

 

세번째 쿼리를 보면

// using JSON doc query

// select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({'city':'seoul', 'age':{$gt:10 , $lt:29}})

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q3");

           console.log(users+"\n");

           return;

});                                 

 

users 컬렉션에서 city seoul이고, age 10보다 크고, 29보다 작은 도큐먼트를 조회한 sort 이용하여 내림 차순으로 정렬을 , select 이용하여 userid age 필드만 리턴한다

다음은 쿼리 실행결과이다.

 

q3

{ age: 23, userid: 'stella', _id: 56e62f3c1a2762d26afa6055 },{ age: 18, userid: 'cath', _id: 56e62f351a2762d26afa6054 }

 

마지막 네번째 쿼리를 보면

//using querybuilder

//select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({})

      .where('city').equals('seoul')

      .where('age').gt(10).lt(29)

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q4");

           console.log(users+"\n");

           return;

});

 

세번째 쿼리와 내용을 같으나, 검색 조건을 find() 안에서 json으로 정의하지 않고, 밖에 where문으로 빼서 정의했다. 이를 쿼리 빌더라고 하는데, mongoose where문에 따라서 자동으로 쿼리를 생성해준다.

.where('city').equals('seoul') city필드가 seoul 도큐먼트를 조회하고

.where('age').gt(10).lt(29) age 10보다 크고, 29보다 작은 도큐먼트를 조회하도록 한다.

다음은 쿼리 수행 결과이다.

 

q4

{ age: 23, userid: 'stella', _id: 56e62f3c1a2762d26afa6055 },{ age: 18, userid: 'cath', _id: 56e62f351a2762d26afa6054 }

 

예제 코드는 https://github.com/bwcho75/nodejs_tutorial/tree/master/mongoosequeryexample 저장되어 있다.

 

 데이타 유효성 검증

 

mongoose 가지고 있는 유용한 기능중의 하나가 validator라는 데이타 유효성 검증이다.

모델 객체에 들어갈 데이타 형뿐 아니라, 데이타의 규약등을 지정할 있는 기능인데, 예를 들어 문자열의 길이나, 특수문자 지원 여부등을 검증할 있다.

 

앞에서 만들었던 mongo.js에서 userSchema 부분을 다음과 같이 수정해보자

var mongoose = require('mongoose');

//define validator

function NameAlphabeticValidator(val){

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

}

function StringLengthValidator(val){

    if(val.length>10) return null;

    return val;

}

 

//define scheme

var userSchema = mongoose.Schema({

      userid: {type:String,validate:NameAlphabeticValidator},

      sex : String,

      city : {type:String,validate:[

                                    {validator:NameAlphabeticValidator,msg:'city should be alphabet only'},

                                    {validator:StringLengthValidator,msg:'city length should be less than 10'}

                                   ]

              }

      });

Figure 21 mongoose validator 예제

 

개의 validator 정의하였다. 하나는 알파벳만 허용하는 NameAlphabeticValidator이고, 다른 하나는 문자열의 길이가 10 이하인 것만 통과 시키는 StringLengthValidator이다.

Validator 정의는 간단하게 function(value)형태로 정의한다. 검증하고자 하는 값이 value라는 인자를 통해서 들어오고, 만약 검증을 통과하면 값을 리턴하면 되면, 실패하면 null 리턴하면 된다.

 

선언된 validator 스키마에 적용해보자.

validator 적용하는 방법은 스키마에서 필드의 데이타 타입을 지정하는 부분에서 위와 같이 데이타 타입을 지정한 , 뒷부분에 validate라는 키워드를 이용하여, 앞서 정의한 validator 명을 지정해주면 된다.

 

userid: {type:String,validate:NameAlphabeticValidator},

 

또는 다음과 같이 하나의 데이타 필드에 배열[] 이용하여 동시에 여러개의 validator 적용할 있다.

다음 코드는 city 필드에 NameAlphabeticValidatorStringLengthValidator 두개를 동시에 적용한 코드이다.

city : {type:String,validate:[

                                    {validator:NameAlphabeticValidator,msg:'city should be alphabet only'},

                                    {validator:StringLengthValidator,msg:'city length should be less than 10'}

                                   ]

 

validator 지정할때 위의 예제와 같이 msg 같이 정의하면, 데이타에 대한 유효성 검증이 실패했을때 나는 메세지를 정의할 있다.

 

다음은 예제에서 city이름에 10자이상의 문자열을 넣는 화면이다.

validator 의해서 유효성 검증이 실패하고, console.log 에러 메세지가 출력된 내용이다.



Figure 22 city 필드에 10자가 넘는 문자열을 입력하는 화면

 

다음은 validator 의해서 city 필드의 유효성 검사가 실패하고, console.log 에러 메세지가 출력된 화면이다.

 



Figure 23 validator에 의해서 city 필드 유효성 검증이 실패한 결과

 

이렇게 validator 만들어 사용하는 이외에도, mongoose에서는 데이타 타입별로 미리 정해놓은 validator 들이 있다.

예를 들어 Number 타입의 경우 min,max 예약어를 이용하여 타입 정의시 값의 유효 범위를 지정해놓을 있다.

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

String 경우 RegularExpression 이용해서 문자열의 형태를 지정할 있고, maxlength 이용하여 전체 문자열 길이에 대한 제약을 있다.  데이타 타입별로 미리 정의되어 있는 validator http://mongoosejs.com/docs/schematypes.html 참고하기 바란다.

 

 SetterGetter, Default


mongoose에서는 스키마의 필드에 대해서 Setter Getter 저장할 있고, 데이타를 저장하지 않았을 경우에는 디폴트 값을 지정할 있다.

Setter 데이타 객체에 데이타를 저장할때, 실행되는 메서드로 데이타를 저장하기전 변환하는 역할을 수행할 있다.

아래 코드를 보자

 

var mongoose = require('mongoose');

 

// setter function

function upperCase (val) {

        return val.toUpperCase();

      }

 

var HelloSchema = new mongoose.Schema(

           { name : { type:String, default:'Hello Terry',set:upperCase} }

           );

 

// default test

var Hello = mongoose.model('hello',HelloSchema);

var hello = new Hello();

 

console.log(hello);

 

// setter test

hello.name="Upper case setter example";

console.log(hello);

 

Figure 24 mongoose setter 예제

 

{ name : { type:String, default:'Hello Terry',set:upperCase} } 코드 부분을 보면 “default”라는 키워드로 “Hello Terry” 라는 값을 지정하였다. name 필드는 별도의 값이 지정되지 않으면 “Hello Terry”라는 문자열을 디폴트 값으로 갖는다.

 

다음 set:upperCase , Setter 지정하는 부분으로, Setter “set:{Setter 함수}” 명으로 지정한다. 여기서 사용된 Setter 위에 코드에서 정의한 upperCase 라는 함수로, 값을 지정하면 문자열의 모든 알파벳을 대문자로 바꿔서 저장한다.

 

위의 예제 실행 결과를 보자

 

{ name: 'Hello Terry', _id: 56f94e5da92daa3a977d8525 }

{ name: 'UPPER CASE SETTER EXAMPLE',

  _id: 56f94e5da92daa3a977d8525 }

Figure 25 mongoose setter 예제 실행 결과

 

처음에는 아무 값도 지정하지 않았기 때문에 name 필드에 디폴트 값인 “Hello Terry” 저장된다.

다음으로, hello.name="Upper case setter example"; 저장을 했지만, 지정된 Setter 의해서, name 모든 알파벳이 대문자로 변환되어 { name: 'UPPER CASE SETTER EXAMPLE',_id: 56f94e5da92daa3a977d8525 } 저장된것을 확인할 있다.

 

Setter이외에 저장된 데이타를 조회할때, 변환하는 Getter 역시 지정이 가능하다.

다음 코드를 보자

 

var mongoose = require('mongoose');

 

// setter function

function lowercase (val) {

        return val.toLowerCase();

      }

 

var HelloSchema = new mongoose.Schema(

           { name : { type:String,get:lowercase} }

           );

 

// gettert test

var Hello = mongoose.model('hello',HelloSchema);

var hello = new Hello();

hello.name="LOWER case setter example";

console.log(hello);

console.log(hello.name);

 

Figure 26 mongoose getter 예제

 

Getter 지정은 스키마에서 타입 지정시 “get:{Getter 함수명}” 식으로 지정하면 된다. 위의 예제에서는

{ name : { type:String,get:lowercase} }

같이 lowercase 함수를 Getter 지정하였다.

 

예제에 대한 실행 결과를 보면 다음과 같다.

 

 

{ _id: 56f94f4314540b3d97fe17b3,

  name: 'LOWER case setter example' }

lower case setter example

 

Figure 27 mongoose getter 예제 실행 결과

 

실제로 데이타 객체내에 name 필드에 저장된 값은 name: 'LOWER case setter example' 이지만, hello.name으로 해당 필드의 내용을 조회했을 경우 getter 지정된 lowercase 함수를 통해서 모두 소문자로 변환된 lower case setter example

 문자열을 리턴하는 것을 확인할 있다.

 

이렇게 직접 getter setter 대한 함수를 정의할 있지만, mongoose에는 모든 문자열을 소문자로 변경하는 lowercase setter, 문자열 앞뒤의 공백을 없애주는 trim setter 등이 기본적으로 제공된다.

 

Lowercase setter 사용예

var s = new Schema({ email: { type: String, lowercase: true }})

 

trim setter 사용예

var s = new Schema({ name: { type: String, trim: true }})

 

데이타 타입별로 미리 제공되는 Setterd Getter http://mongoosejs.com/docs/schematypes.html 참고하기 바란다.

 

 스키마 타입

 

앞서서 mongoose 스키마에 대해서 설명하였는데, 조금 자세하게 살펴보자 스키마에서는 필드에 대한 데이타 타입을 정의할 있는데, 다음과 같다.

 

스키마 타입

설명

예제

String

문자열

‘Hello’

Number

숫자

135090

Date

날짜

ISODate("1970-06-09T15:00:00.000Z")

Buffer

바이너리 타입 (파일등을 저장할때 사용됨)

파일등의 바이너리 데이타

Mixed

특별한 형을 가지지 않고 아무 JSON 문서나 있음

‘any’:{ ‘data’:’this is any data….’}

Objectid

mongoDB objectid

ObjectId("56f8d0b63ef9d003961e5f3f")

Array

배열

[‘Hello’ , ‘Terry’ ]

Figure 28 mongoose 스키마 타입

설명을 돕기 위해서 예제를 보자.

다음과 같은 형태 데이타를 표현하기 위한 스키마를 저장할 것이다.

사용자의 정보를 저장하는 Profile이라는 형태의 스키마이다.

 

{

    "_id" : ObjectId("56f93d08253b92b296080587"),

    "meta" : {

        "book" : "architecture design",

        "company" : "cloud consulting"

    },

    "birthday" : ISODate("1970-06-09T15:00:00.000Z"),

    "address" : {

        "_id" : ObjectId("56f8d0b63ef9d003961e5f40"),

        "zipcode" : 135090,

        "city" : "youngin",

        "state" : "Kyungki"

    },

    "name" : "terry",

    "recommend" : [

        "I want to recommend terry",

        "He is good guy"

    ],

    "image" : {

        "data" : { "$binary" : "/9j/4AAQSkZJ (중략) Rg ", "$type" : "00" },

        "contentsType" : "image/png"

    },

    "__v" : 0

}

Figure 29 사용자 프로파일 JSON 도큐먼트 예제

 

이름, 생년월일, 주소, 그리고 사용자에 대한 추천글과, 사용자에 대한 이미지 파일을 저장하는 스키마이다.

이를 스키마로 지정하면 다음과 같다.

 

// define scheme

var addressSchema = new mongoose.Schema({

      zipcode : Number,

      city : String,

      state : String

});

 

 

var profileSchema = new mongoose.Schema({

      name : String,

      address : addressSchema,

      birthday : Date,

      meta : mongoose.Schema.Types.Mixed,

      image : {

           data : Buffer,

           contentsType : String

      },

      recommend : [String]

});

 

Figure 30 mongoose를 이용하여 schema.js 예제에서 사용자 프로파일 스키마를 정의한 부분

 

주소를 저장하기 위한 스키마는 addressSchema, 숫자로된 zipcode, 문자열로 city state 필드를 갖는다

·         name은 문자열로 이름을 저장한다.

·         address는 서브 도큐먼트 타입으로, 앞에서 정의한 addressSchema 형을 참조한다.

·         birthday는 날짜 타입이고, 

·         meta는 메타 정보를 저장하는 필드인데, Mixed 타입이다. Mixed 타입은 앞에서도 설명하였듯이, 아무 JSON 도큐먼트나 들어갈 수 있다.

·         다음으로 imageJSON 타입으로 안에, 사진 파일을 저장하기 위해서 Buffer 형으로 data  필드를 갖고, 사진 포맷 저장을 위해서 contentsType이라는 타입을 갖는다.

·         마지막으로 recommend 필드는 사용자에 대한 추천 문자열을 배열로 갖는다.

 

서브 도큐먼트 vs 임베디드 도큐먼트 vs Mixed 타입

 

스키마를 보면, 스키마 내에 JSON 도큐먼트를 갖는 필드가 address,meta,image 3 가지가 있다. 타입의 차이점은 무엇일까?

먼저 addresss 서브 도큐먼트 (sub document) 타입으로 mongodb 저장하면 도큐먼트 형으로 저장이 되고, _id 필드를 갖는다. 부모 도큐먼트 (여기서는 profileSchema) 종속 되는 도큐먼트 형태로, 단독으로는 업데이트가 불가능하고 반드시 부모 도큐먼트 업데이트시에만 업데이트가 가능하다. 이러한 서브 도큐먼트 타입은 같은 타입의 서브 도큐먼트가 반복적으로 사용될때 타입 객체를 사용할때 사용하면 좋다.

 

다음 image 필드와 같이 스키마내에 JSON 도큐먼트 포맷을 그대로 저장하는 방식을 embeded 방식이라고 하는데, 서브 도큐먼트와는 다르게 _id 필드가 붙지 않는다. 간단하게 JSON 도큐먼트를 내장할때 사용한다.

마지막으로 meta 필드의 경우 Mixed 타입을 사용했는데, 아무 포맷의 JSON 문서가 들어갈 있다. 컬렉션 내에서 해당 필드의 JSON 도큐먼트 포맷이 각기 다를때 사용할 있으며, 포맷을 정의하지 않기 때문에 유연하게 사용할 있다.

 

스키마를 정의했으면 이제 값을 넣어서 저장해보자

 

// create model

var Profile = mongoose.model('profiles',profileSchema);

var Address = mongoose.model('address',addressSchema);

var p = new Profile();

 

// populate model

p.name = "terry";

 

// address

var a = new Address();

a.zipcode = 135090;

a.city = "youngin";

a.state = "Kyungki";

p.address = a;

 

// birthday

p.birthday = new Date(1970,05,10);

 

// meta

p.meta = { company : 'cloud consulting', book : 'architecture design'};

 

// image

p.image.contentsType='image/png';

var buffer = fs.readFileSync('/Users/terry/nick.jpeg');

p.image.data = buffer;

 

// recommend

p.recommend.push("I want to recommend terry");

p.recommend.push("He is good guy");

 

p.save(function(err,silece){

      if(err){

           cosole.log(err);

           return;

      }

      console.log(p);

});

 

Figure 31 mongoose를 이용하여 schema.js 예제에서 데이타를 저장하는 부분

 

 

값을 저장하기 위해서 모델 객체를 생성한후 Profile 대한 데이타 객체 p Address 대한 데이타 객체 a 생성하였다.

값을 저장할때는 “{데이타 객체명}.필드=형태로 저장한다.

Address 저장을 위해서 데이타 객체인 a zipcode,city,state 값을 저장한후에, p.address = a 이용해서, address 필드의 값을 채워 넣는다.

p.birthday Date형이기 때문에, new Date() 메서드를 이용해서, Date 객체를 생성하여 데이타를 저장한다.

p.meta Mixed 타입으로 직접 JSON 도큐먼트를 지정하여 저장한다.

p.image 임베디드 도큐먼트 타입으로, p.image.data, p.image.contentsType 각각의 필드에 값을 저장한다. 이때 data 필드는 Buffer 타입으로, 예제에서는 /Users/terry/nick.jpeg 라는 파일을 저장 하였다. fs.readFileSync 이용하여 인코딩 지정없이 파일을 읽게 되면, 파일 데이타를 Buffer 객체로 반환해주는데, 값을 p.image.data 지정하여 저장하였다.

그리고 마지막으로, p.recommend String 배열로, push 메서드를 이용하여 데이타를 추가 하였다.

 

데이타 객체에 모든 값이 저장되었으면 이를 mongodb 저장하기 위해서 p.save 메서드를 이용하여 저장한다.

 

다음 데이타를 수정하는 방법을 알아보자. 앞의 예제에서 저장된 Profile 도큐먼트의 _id '56f93d08253b92b296080587'  라고 하자. 아래 예제는 Profile 컬렉션에서 _id '56f93d08253b92b296080587' 도큐먼트를 찾아서 birthday 2( Date.setMonth(1) 2월이다. 0부터 시작한다.) 바꿔서 save 메서드를 이용해서 저장하는 예제이다.

 

var mongoose = require('mongoose');

var fs = require('fs');

mongoose.connect('mongodb://localhost:27017/mydb');

 

// define scheme

var addressSchema = new mongoose.Schema({

      zipcode : Number,

      city : String,

      state : String

});

 

 

var profileSchema = new mongoose.Schema({

      name : String,

      address : addressSchema,

      birthday : Date,

      meta : mongoose.Schema.Types.Mixed,

      image : {

           data : Buffer,

           contentsType : String

      },

      recommend : [String]

});

 

// create model

var Profile = mongoose.model('profiles',profileSchema);

var Address = mongoose.model('address',addressSchema);

var p = new Profile();

 

Profile.findOne({_id:'56f93d08253b92b296080587'},function(err,p){

      console.log(p);

      p.birthday.setMonth(1);

      p.save(function(err,silece){

           if(err){

                 cosole.log(err);

                 return;

           }

           console.log(p);

      });

});

 

 

Figure 32 mongoose에서 데이타를 조회하여 Date 필드를 업데이트 하는 예제

 

저장된 데이타를 robomongo 이용해서 mongodb에서 확인해보면 다음과 같다.



Figure 33 예제 실행 결과, Date 필드 수정 내용이 반영되지 않은 결과

 

기대했던 결과와는 다르게, birthday 2월로 바뀌지 않고, 처음에 생성했던 6월로 되어 있는 것을 있다.

mongoose save 메서드는 필드의 값이 변환된 것만 자동으로 인식하여 save 저장하는데, 몇몇 타입의 경우 자동으로 변경된 값을 인식하지 못한다.

Date, Mixed 필드가 그러한데, 경우에는 mongoose 에게 해당 필드의 값이 변경되었음을 강제적으로 알려줘서 변경된 값을 인식하여 저장하게 해야 한다.

이때 markedModified(“필드명”) 메서드를 사용한다. 아래 코드는 markedModified 이용하여 birthday 필드가 변경되었음을 명시적으로 알려주고, 값을 저장하도록 변경한 코드이다.

 

Profile.findOne({_id:'56f93d08253b92b296080587'},function(err,p){

      console.log(p);

      p.birthday.setMonth(1);

      p.markModified('birthday');

      p.save(function(err,silece){

           if(err){

                 cosole.log(err);

                 return;

           }

           console.log(p);

      });

});

 

Figure 34 markedModified를 이용하여 Date 필드가 수정되었음을 명시적으로 알려주도록 코드를 수정한 내용

 

위의 코드를 수정한 다음 다시 mongodb 저장된 데이타를 보면 다음과 같다.



Figure 35 markedModified 반영후, Date 필드가 정상적으로 반영된 결과

 

성공적으로 birthday 월이 2월로 변경된것을 확인할 있다.

스키마 타입 관련 예제 코드는 https://github.com/bwcho75/nodejs_tutorial/tree/master/mongooseschemeexample 참고하기 바란다.

 

나중에 시간되면, population index 보강 예정

저작자 표시 비영리
신고

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


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



 

Mongoosemongodb 기반의 node.jsODM (Object Data Mapping) 프레임웍이다.

이미 앞에서 monk를 이용한 mongodb 연결 방식에 대해서 알아보았는데, mongoose는 무엇인가? 쉽게 생각하면 monk를 자바에서 JDBC 드라이버로 직접 데이타 베이스에 연결하여 SQL을 실행한 거라면, monggoseHibernate/Mybatis와 같이 OR Mapper의 개념이다. 직접 SQL을 실행하는 것이 아니라 node.js의 자바스크립트 객체를 mongodb의 도큐먼트와 맵핑 시켜 주는 개념이다.

이런ODM의 개념은 웹 프로그램에서 주로 사용되는 MVC (Model View Controller)의 개념을 모델 객체를 실제 데이타 베이스 데이타에 맵핑 시킴으로써 쉽게 구현해주게 한다. 또한 자바스크립트가 가지고 있는 데이타에 대한 모호성을 극복하게 해주는데, mongodb는 특성상 데이타에 대한 스키마가 없기 때문에 자유도가 높아서 유연하게 사용이 가능하지만 반대로 RDBMS에서 정의된 스키마나 명시적인 테이블 구조가 없기 때문에, 어떤 컬럼이 어떤 데이타 타입으로 있는지를 알기가 어렵다. 이러한 문제를 보완하기 위해서 mongoose는 스키마라는 개념을 사용한다.

 

앞에 monk로 만든 예제를 mongoose로 변환한 예제를 살펴보도록 하자.

이 예제는 express 4.X 버전을 기준으로 하니 express generator를 통해서 express 프로젝트를 먼저 생성하자.

 

mongoose 설치

프로젝트 생성이 끝났으면 “npm install mongoose” 명령어를 이용하여, mongoose 모듈을 설치하고, 다른 서버에 배포할 것을 염두하여 package.json에 의존성을 추가한다.

{

  "name": "mongooseExpress",

  "version": "0.0.0",

  "private": true,

  "scripts": {

    "start": "node ./bin/www"

  },

  "dependencies": {

    "body-parser": "~1.13.2",

    "cookie-parser": "~1.3.5",

    "debug": "~2.2.0",

    "express": "~4.13.1",

    "jade": "~1.11.0",

    "mongoose": "~4.4.9",

    "morgan": "~1.6.1",

    "serve-favicon": "~2.3.0"

  }

}

 

 

mongoDB 연결

다음으로 app.js에서, mongoose 모듈을 로딩하고, mongoDB에 연결한다.

var app = express();

 

// view engine setup

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

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

 

// uncomment after placing your favicon in /public

//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));

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

app.use(bodyParser.json());

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

app.use(cookieParser());

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

 

//mongoose configuration

 

var mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/mydb');

 

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

app.use('/', mongo)

 

app.js에서 위와 같은 require를 이용하여 mongoose 모듈을 로딩하고

mongoose.connect명령어를 이용하여, 몽고 DB연결한다. 연결 문자열은 monk와 같게

mongodb://{mongodb ip 주소}:{포트}/{DB}

이다. 위의 예제는 localhost 27017 포트의 mongodbmydbDB에 연결하는 문자열이다.

클러스터된 mongodb 연결 및 timeout등 다양한 설정을 지원하기 때문에, 설정에 대해서는 http://mongoosejs.com/docs/connections.html 를 참고하기 바란다.

 

다음으로, /로 들어오는 요청을 /routes/mongo.js 에서 처리할 수 있도록 app.use / 경로를 mongo 모듈로 맵핑 시킨다.

 

스키마 정의

mongoose에는 스키마라는 개념이 존재하는데, 스키마는 mongoDB에 저장되는 도큐먼트의 데이타 형태를 정의한다. 쉽게 말해서 RDBMS에서 일종의 테이블 정의라고 이해 하면된다.

 

이 예제에서는 HTTP 호출을 처리하는 routes/mongo.js에 스키마를 정의하였다. 아래 코드를 보자.

아래 코드는 routes/mongo.js의 첫 부분이다.

 

var express = require('express');

var router = express.Router();

 

 

var mongoose = require('mongoose');

//define scheme

var userSchema = mongoose.Schema({

      userid: String,

      sex : String,

      city : String

});

 

 

require 구문을 이용해서 mongoose 모듈을 로드하였고,

mongoose.Schema 명령을 이용해서 userSchema를 정의하였다.

Schema 정의 부분을 보면, userSchemauserid, sex, city 3개의 필드로 정의되고, 각 필드는 String 데이타 타입을 사용한다. 구체적인 스키마 타입에 대해서는 뒤에서 설명한다.

 

데이타 핸들링

그러면 데이타를 읽고,쓰고,삭제하고,수정하는 방법을 알아보자. 앞의 monk 예제와 같이 /insert /list /update /delete /get에 대한 라우팅 메서드를 구현해보자

 

앞의 코드에 이어서 insert 부분을 먼저 살펴보자

             

 

// create model with mongodb collection & scheme

var User = mongoose.model('users',userSchema);

 

router.post('/insert', function(req, res, next) {

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

     

      var user = new User({'userid':userid,'sex':sex,'city':city});

      user.save(function(err,silence){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Inserted");

            

         });

});

 

 

가장 중요한 부분은 먼저 앞에서 만든 userSchema를 가지고 모델 객체를 생성해야 한다. 모델 객체 생성 방법은

mongoose.model(‘{mongodb에서 Collection }’, {스키마객체명})

이다. 이 포맷에 따라서 다음과 같이 User라는 모델 객체를 생성하였다.

 

var User = mongoose.model('users',userSchema);

 

User라는 모델 객체는 쉽게 생각하면 mongodbCollection을 대변하는 node.js상의 객체라고 생각하면 된다.

새로운 user 정보를 추가하기 위해서 post 메서드에서 User 모델로 새로운 user 객체를 생성한다. 객체를 생성할때는 JSON을 이용해서 스키마 포맷에 맞춰서 저장하고자 하는 키와 값을 다음과 같이 저장해주면된다.

 

var user = new User({'userid':userid,'sex':sex,'city':city});

 

그리고 간단하게 user 객체를 save 메서드를 이용해서 저장해주면 mongodb 저장된다.

 

다음은 삭제 부분을 보자

router.post('/delete', function(req, res, next) {

      var userid = req.body.userid;

     

      var user = User.find({'userid':userid});

      user.remove(function(err){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Removed");

            

         });

});

 

userid POST 받은 후에 User.find 명령을 이용하여 ‘userid’필드가 POST에서 전달 받은 userid 필드를 검색하여 삭제하고자 하는 도큐먼트를 찾아낸 후에remove 메서드를 이용해서 삭제한다.

 

다음은 업데이트 부분이다. userid 도큐먼트를 조회해서, 내용을 업데이트 하는 로직이다.

router.post('/update', function(req, res, next) {

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

      User.findOne({'userid':userid},function(err,user){

 

           if(err){

               console.log(err);

               res.status(500).send('update error');

               return;

          }

           user.sex = sex;

           user.city = city;

           user.save(function(err,silence){

                  if(err){

                     console.log(err);

                     res.status(500).send('update error');

                     return;

                  }

                  res.status(200).send("Updated");

                 

              });

      });

});

 

 

User.findOne 이용해서 userid 도큐먼트를 조회한 후에, 콜백으로 리턴되는 user 객체의 sex,city 값을 변경한후에

user.save 이용하여 저장하였다.

 

다음은 전체 리스트를 조회하는 부분이다.

router.get('/list', function(req, res, next) {

      User.find({},function(err,docs){

           if(err) console.log('err');

           res.send(docs);

      });

});

 

 find 메서드를 사용하면 되는데, find 메서드의 인자로 검색 조건을 넘기면 된다. 전체 검색이기 때문에 별도의 조건없이 {} 사용하였다.

 

다음은 userid로 도큐먼트를 조회하는 부분이다.

전체리스트와 유사하지만 findOne 메서드를 이용하여 userid에 해당하는 사용자 도큐먼트를 조회하여 출력한다.

router.get('/get', function(req, res, next) {

      db = req.db;

      var userid = req.query.userid

      User.findOne({'userid':userid},function(err,doc){

           if(err) console.log('err');

            res.send(doc);

      });

});

 

module.exports = router;

 

 

route 처리 로직에 요청을 보내는 html 파일을 만들자. 이 파일은 앞서 monk 예제에서 만든 파일과 동일하다.

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>Insert title here</title>

</head>

<body>

 

<h1> Mongoose Test Example</h1>

<form method='post' action='/insert' name='mongoform' >

      user <input type='text' size='10' name='userid'>

      <input type='submit' value='delete' onclick='this.form.action="/delete"' >

      <input type='button' value='get' onclick='location.href="/get?userid="+document.mongoform.userid.value' >

      <p>

      city <input type='text' size='10' name='city' >

      sex <input type='radio' name='sex' value='male'>male

      <input type='radio' name='sex' value='female'>female

      <p>

      <input type='submit' value='insert' onclick='this.form.action="/insert"' >

      <input type='submit' value='update' onclick='this.form.action="/update"' >

      <input type='button' value='list'  onclick='location.href="/list"' >

     

</form>

</body>

</html>

 

/public/mongoose.html이라는 파일로 저장을 하자.

모든 코드 작성이 끝났다. 이를 실행해보면 다음과 같이 데이타를 넣을 수 있는 입력창이 나온다.

 



Figure 1 mongoose 예제에서 데이타를 입력하는 화면

 

데이타를 입력하고, insert를 이용하여 데이타를 저장한후 다시 get 버튼을 눌러서 데이타를 조회해보면 다음과 같이 useriddavid인 도큐먼트가 조회되는 것을 확인할 수 있다.



Figure 2 mongoose 예제에서 useriddavid인 도큐먼트를 조회한 결과

 

쿼리

 

간단한 삽입,삭제,수정,조회만 앞에서 살펴보았는데, 이외에도 다양한 조회 기능을 제공한다. 자세한 쿼리 사용 방법은 http://mongoosejs.com/docs/documents.html 를 참고하면 된다.

몇 가지 쿼리들을 살펴보도록 하자. 다음 예제를 살펴보자

 

 

var mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/mydb');

var userSchema = mongoose.Schema({

      userid: String,

      sex : String,

      city : String,

      age : Number

});

 

var User = mongoose.model('users',userSchema);

 

// select city from users where userid='terry'

User.findOne({'userid':'terry'}).select('city').exec(function(err,user){

      console.log("q1");

      console.log(user+"\n");

      return;

});

 

 

// select * from users where city='seoul' order by userid limit 5

User.find({'city':'seoul'}).sort({'userid':1}).limit(5).exec(function(err,users){

      console.log("q2");

      console.log(users+"\n");

      return;

});

 

// using JSON doc query

// select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({'city':'seoul', 'age':{$gt:10 , $lt:29}})

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q3");

           console.log(users+"\n");

           return;

});

 

//using querybuilder

//select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({})

      .where('city').equals('seoul')

      .where('age').gt(10).lt(29)

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q4");

           console.log(users+"\n");

           return;

});

 

 

코드 첫 부분에서는

var mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/mydb');

var userSchema = mongoose.Schema({

      userid: String,

      sex : String,

      city : String,

      age : Number

});

 

var User = mongoose.model('users',userSchema);

 

mongoose 모듈을 로딩하고, mongodb에 연결한다. 다음 스키마를 정의 한다. userid, sex, city를 문자열로 가지고, age라는 필드를 숫자로 갖는다. 스키마 정의가 끝났으면 이 스키마를 이용해서 users 컬렉션에 대해서 User 모델 객체를 생성한다.

 

첫번째 쿼리를 살펴보자

// select city from users where userid='terry'

User.findOne({'userid':'terry'}).select('city').exec(function(err,user){

      console.log("q1");

      console.log(user+"\n");

      return;

});

 

첫번째 쿼리는 useridterry인 도큐먼트를 쿼리해서 그중에서 city 라는 이름의 필드만 리턴하는 쿼리이다.

실행 결과는 다음과 같다.

 

q1

{ city: 'seoul', _id: 56e62f2c1a2762d26afa6053 }

 

두번째 쿼리를 살펴보자

// select * from users where city='seoul' order by userid limit 5

User.find({'city':'seoul'}).sort({'userid':1}).limit(5).exec(function(err,users){

      console.log("q2");

      console.log(users+"\n");

      return;

});

 

다음은 실행결과이다.

 

q2

{ age: 18,

  city: 'seoul',

  sex: 'female',

  userid: 'cath',

  _id: 56e62f351a2762d26afa6054 },{ age: 23,

  city: 'seoul',

  sex: 'female',

  userid: 'stella',

  _id: 56e62f3c1a2762d26afa6055 },{ age: 29,

  city: 'seoul',

  sex: 'male',

  userid: 'terry',

  _id: 56e62f2c1a2762d26afa6053 },{ age: 27,

  city: 'seoul',

  sex: 'female',

  userid: 'yuna',

  _id: 56e62f411a2762d26afa6056 }

 

users 컬렉션에서 city 필드가 seoul인 도큐먼트를 조회한후 결과를 sort({‘userid’:1}) 을 이용하여 userid에 따라 오름차순으로 정렬한 후에, limit(5)를 이용하여 결과중 5개만 리턴한다.

 

세번째 쿼리를 보면

// using JSON doc query

// select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({'city':'seoul', 'age':{$gt:10 , $lt:29}})

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q3");

           console.log(users+"\n");

           return;

});                                 

 

users 컬렉션에서 cityseoul이고, age 10보다 크고, 29보다 작은 도큐먼트를 조회한 후 sort를 이용하여 내림 차순으로 정렬을 한 후, select를 이용하여 useridage 필드만 리턴한다

다음은 쿼리 실행결과이다.

 

q3

{ age: 23, userid: 'stella', _id: 56e62f3c1a2762d26afa6055 },{ age: 18, userid: 'cath', _id: 56e62f351a2762d26afa6054 }

 

마지막 네번째 쿼리를 보면

//using querybuilder

//select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({})

      .where('city').equals('seoul')

      .where('age').gt(10).lt(29)

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q4");

           console.log(users+"\n");

           return;

});

 

세번째 쿼리와 내용을 같으나, 검색 조건을 find() 안에서 json으로 정의하지 않고, 밖에 where문으로 빼서 정의했다. 이를 쿼리 빌더라고 하는데, mongoosewhere문에 따라서 자동으로 쿼리를 생성해준다.

.where('city').equals('seoul') city필드가 seoul 도큐먼트를 조회하고

.where('age').gt(10).lt(29) age 10보다 크고, 29보다 작은 도큐먼트를 조회하도록 한다.

다음은 쿼리 수행 결과이다.

q4

{ age: 23, userid: 'stella', _id: 56e62f3c1a2762d26afa6055 },{ age: 18, userid: 'cath', _id: 56e62f351a2762d26afa6054 } 

 


저작자 표시 비영리
신고

monk 모듈을 이용한 mongoDB 연결


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


mongoDB 기반의 개발을 하기 위해서 mongoDB를 설치한다. https://www.mongodb.org/ 에서 OS에 맞는 설치 파일을 다운로드 받아서 설치한다.

설치가 된 디렉토리에 들어가서 설치디렉토리 아래 ‘./data’ 라는 디렉토리를 만든다. 이 디렉토리는 mongoDB의 데이타가 저장될 디렉토리이다.

 

mongoDB를 구동해보자.

% ./bin/mongod --dbpath ./data



Figure 1 mongoDB 구동화면


구동이 끝났으면 mongoDB에 접속할 클라이언트가 필요하다. DB에 접속해서 데이타를 보고 쿼리를 수행할 수 있는 클라이언트가 필요한데, 여러 도구가 있지만 많이 사용되는 도구로는 roboMongo라는 클라이언트가 있다.

https://robomongo.org/download 에서 다운로드 받을 수 있다. OS에 맞는 설치 파일을 다운로드 받아서 설치 후 실행한다.

 

설치 후에, Create Connection에서, 로컬호스트에 설치된 mongoDB를 연결하기 위해서 연결 정보를 기술하고, 연결을 만든다





Figure 2 robomongo에서 localhost에 있는 mongodb 연결 추가

 

주소는 localhost, 포트는 디폴트 포트로 27017를 넣으면 된다.

 

환경이 준비가 되었으면 간단한 테스트를 해보자. 테스트 전에 기본적인 개념을 숙지할 필요가 있는데, mongoDBNoSQL 계열중에서도 도큐먼트DB (Document DB)에 속한다. 기존 RDBMS에서 하나의 행이 데이타를 표현했다면, mogoDB는 하나의 JSON 파일이 하나의 데이타를 표현한다. JSON을 도큐먼트라고 하기 때문에, 도큐먼트 DB라고 한다.

 

제일 상위 개념은 DB의 개념에 대해서 알아보자, DB는 여러개의 테이블(컬렉션)을 저장하는 단위이다.

Robomongo에서 mydb라는 이름으로 DB 를 생성해보자



Figure 3 robomongo에서 새로운  DB를 추가 하는 화면

 

다음으로 생성된 DB안에, 컬렉션을 생성한다. 컬렉션은 RDBMS의 단일 테이블과 같은 개념이다.

Robomongo에서 다음과 같이 ‘users’라는 이름의 컬렉션을 생성한다



Figure 4 robomongo에서 컬렉션(Collection) 생성

 

users 컬렉션에는 userid를 키로 해서, sex(성별), city(도시) 명을 입력할 예정인데, userid가 키이기 때문에, userid를 통한 검색이나 소팅등이 발생한다. 그래서 userid를 인덱스로 지정한다.

인덱스 지정 방법은 createIndex 명령을 이용한다. 다음과 같이 robomongo에서 createIndex 명령을 이용하여 인덱스를 생성한다.



Figure 5 users 컬렉션에서 userid를 인덱스로 지정

 

mongoDB는 디폴트로, 각 컬렉션마다 “_id”라는 필드를 가지고 있다. 이 필드는 컬렉션 안의 데이타에 대한 키 값인데, 12 바이트의 문자열로 이루어져 있고 ObjectId라는 포맷으로 시간-머신이름,프로세스ID,증가값형태로 이루어지는 것이 일반적이다.

_id 필드에 userid를 저장하지 않고 별도로 인덱스를 만들어가면서 까지 userid 필드를 별도로 사용하는 것은 mongoDBNoSQL의 특성상 여러개의 머신에 데이타를 나눠서 저장한다. 그래서 데이타가 여러 머신에 골고루 분산되는 것이 중요한데, 애플리케이션상의 특정 의미를 가지고 있는 필드를 사용하게 되면 데이타가 특정 머신에 쏠리는 현상이 발생할 수 있다.

예를 들어서, 주민번호를 _id로 사용했다면, 데이타가 골고루 분산될것 같지만, 해당 서비스가 10~20대에만 인기있는 서비스라면, 10~20대 데이타를 저장하는 머신에만 데이타가 몰리게 되고, 10세이하나, 20세 이상의 데이타를 저장하는 노드에는 데이타가 적게 저장된다.

이런 이유등으로 mongoDB를 지원하는 node.js 드라이버에서는 _id 값을 사용할때, 앞에서 언급한 ObjectId 포맷을 따르지 않으면 에러를 내도록 설계되어 있다. 우리가 앞으로 살펴볼 mongoosemonk의 경우에도 마찬가지이다.

 

이제 데이타를 집어넣기 위한 테이블(컬렉션) 생성이 완료되었다.

다음 컬렉션 에 대한 CRUD (Create, Read, Update, Delete) 를 알아보자

SQL 문장과 비교하여, mongoDB에서 CRUD 에 대해서 알아보면 다음과 같다.

CRUD

SQL

MongoDB

Create

insert into users ("name","city") values("terry","seoul")

db.users.insert({userid:"terry",city:"seoul"})

Read

select * from users where id="terry"

db.users.find({userid:"terry"})

Update

update users set city="busan" where _id="terry"

db.users.update( {userid:"terry"}, {$set :{ city:"Busan" } } )

Delete

delete from users where _id="terry"

db.users.remove({userid:"terry"})

Figure 6 SQL문장과 mongoDB 쿼리 문장 비교


mongoDB에서 쿼리는 위와 같이 db.{Collection }.{명령어} 형태로 정의된다.

roboMongo에서 insert 쿼리를 수행하여 데이타를 삽입해보자



Figure 7 mongoDB에서 users 컬렉션에 데이타 추가

 

다음으로 삽입한 데이타를 find 명령을 이용해 조회해보자



Figure 8 mongoDB에서 추가된 데이타에 대한 확인

 

mongoDB에 대한 구조나 자세한 사용 방법에 대해서는 여기서는 설명하지 않는다.

http://www.tutorialspoint.com/mongodb/ mongoDB에 대한 전체적인 개념과 주요 쿼리들이 간략하게 설명되어 있으니 이 문서를 참고하거나, 자세한 내용은 https://docs.mongodb.org/manual/ 를 참고하기 바란다.

https://university.mongodb.com/ 에 가면 mongodb.com에서 운영하는 온라인 강의를 들을 수 있다. (무료인 과정도 있으니 필요하면 참고하기 바란다.)

 

mongoDBnode.js에서 호출하는 방법은 여러가지가 있으나 대표적인 두가지를 소개한다.

첫번째 방식은 mongoDB 드라이버를 이용하여 직접 mongoDB 쿼리를 사용하는 방식이고, 두번째 방식은 ODM (Object Document Mapper)를 이용하는 방식이다. ODM 방식은 자바나 다른 프로그래밍 언어의 ORM (Object Relational Mapping)과 유사하게 직접 쿼리를 사용하는 것이 아니라 맵퍼를 이용하여 프로그램상의 객체를 데이타와 맵핑 시키는 방식이다. 뒷부분에서 직접 코드를 보면 이해가 빠를 것이다.

 

Monk를 이용한 연결

첫번째로 mongoDB 네이티브 쿼리를 수행하는 방법에 대해서 소개한다. monk라는 node.jsmongoDB 클라이언트를 이용할 것이다.

monk 모듈을 이용하기 위해서 아래와 같이 package.jsonmonk에 대한 의존성을 추가한다.


{

  "name": "mongoDBexpress",

  "version": "0.0.0",

  "private": true,

  "scripts": {

    "start": "node ./bin/www"

  },

  "dependencies": {

    "body-parser": "~1.13.2",

    "cookie-parser": "~1.3.5",

    "debug": "~2.2.0",

    "express": "~4.13.1",

    "jade": "~1.11.0",

    "morgan": "~1.6.1",

    "serve-favicon": "~2.3.0",

    "monk":"~1.0.1"

  }

}

 

Figure 9 monk 모듈에 대한 의존성이 추가된 package.json

 

app.js에서 express가 기동할때, monk를 이용해서 mongoDB에 연결하도록 한다.

var monk = require('monk');

var db = monk('mongodb://localhost:27017/mydb');

 

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

app.use(function(req,res,next){

    req.db = db;

    next();

});

app.use('/', mongo);

Figure 10 monk를 이용하여 app.js에서 mongoDB 연결하기

 

mongoDB에 연결하기 위한 연결 문자열은 'mongodb://localhost:27017/mydb' mongo://{mongoDB 주소}:{mongoDB 포트}/{연결하고자 하는 DB} 으로 이 예제에서는 mongoDB 연결을 간단하게 IP,포트,DB명만 사용했지만, 여러개의 인스턴스가 클러스터링 되어 있을 경우, 여러 mongoDB로 연결을 할 수 있는 설정이나, Connection Pool과 같은 설정, SSL과 같은 보안 설정등 부가적인 설정이 많으니, 반드시 운영환경에 맞는 설정으로 변경하기를 바란다. 설정 방법은 http://mongodb.github.io/node-mongodb-native/2.1/reference/connecting/connection-settings/ 문서를 참고하자.

 

이때 주의깊게 살펴봐야 하는 부분이 app.use를 이용해서 미들웨어를 추가하였는데, req.dbmongodb 연결을 넘기는 것을 볼 수 있다. 미들웨어로 추가가 되었기 때문에 매번 HTTP 요청이 올때 마다 req 객체에는 db라는 변수로 mongodb 연결을 저장해서 넘기게 되는데, 이는 HTTP 요청을 처리하는 것이 router에서 처리하는 것이 일반적이기 때문에, routerdb 연결을 넘기기 위함이다. 아래 데이타를 삽입하는 라우터 코드를 보자

 

router.post('/insert', function(req, res, next) {

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

     

      db = req.db;

      db.get('users').insert({'userid':userid,'sex':sex,'city':city},function(err,doc){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Inserted");

            

         });

});

Figure 11 /routes/mongo.js 에서 데이타를 삽입하는 코드


req 객체에서 폼 필드를 읽어서 userid,sex,city등을 읽어내고, 앞의 app.js 에서 추가한 미들웨어에서 넘겨준 db 객체를 받아서 db.get('users').insert({'userid':userid,'sex':sex,'city':city},function(err,doc) 수행하여 데이타를 insert 하였다.

 

다음은 userid필드가 HTTP 폼에서 넘어오는 userid 일치하는 레코드를 지우는 코드 예제이다. Insert 부분과 크게 다르지 않고 remove 함수를 이용하여 삭제 하였다.


router.post('/delete', function(req, res, next) {

      var userid = req.body.userid;

     

      db = req.db;

      db.get('users').remove({'userid':userid},function(err,doc){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Removed");

            

         });

});

Figure 12 /routes/mongo.js 에서 데이타를 삭제하는 코드

 

다음은 데이타를 수정하는 부분이다. Update 함수를 이용하여 데이타를 수정하는데,

db.get('users').update({userid:userid},{'userid':userid,'sex':sex,'city':city},function(err,doc){

와 같이 ‘userid’userid 인 필드의 데이타를 },{'userid':userid,'sex':sex,'city':city} 대치한다.

 

router.post('/update', function(req, res, next) {

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

      db = req.db;

      db.get('users').update({userid:userid},{'userid':userid,'sex':sex,'city':city},function(err,doc){

      //db.get('users').update({'userid':userid},{$set:{'sex':'BUSAN'}},function(err,doc){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Updated");

            

         });

});

Figure 13 /routes/mongo.js 에서 데이타를 수정하는 코드


전체 레코드를 대치하는게 아니라 특정 필드만 수정하고자 하면, $set: 쿼리를 이용하여, 수정하고자하는 필드만 아래와 같이 수정할 수 있다.

db.collection('users').updateOne({_id:userid},{$set:{'sex':'BUSAN'}},function(err,doc){

 

마지막으로 데이타를 조회하는 부분이다. /list URL은 전체 리스트를 리턴하는 코드이고, /get ?userid= 쿼리 스트링으로 정의되는 사용자 ID에 대한 레코드만을 조회해서 리턴한다.

router.get('/list', function(req, res, next) {

      db = req.db;

      db.get('users').find({},function(err,doc){

           if(err) console.log('err');

           res.send(doc);

      });

});

router.get('/get', function(req, res, next) {

      db = req.db;

      var userid = req.query.userid

      db.get('users').findOne({'userid':userid},function(err,doc){

           if(err) console.log('err');

           res.send(doc);

      });

});

Figure 14 /routes/mongo.js 에서 데이타를 조회하는 코드

 

이제 /routes/mongo.js 의 모든 코드 작업이 완료되었다. 이 코드를 호출하기 위한 HTML 폼을 작성하자.

 

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>Insert title here</title>

</head>

<body>

 

<h1> Native MongoDB Test Example</h1>

<form method='post' action='/insert' name='mongoform' >

      user <input type='text' size='10' name='userid'>

      <input type='submit' value='delete' onclick='this.form.action="/delete"' >

      <input type='button' value='get' onclick='location.href="/get?userid="+document.mongoform.userid.value' >

      <p>

      city <input type='text' size='10' name='city' >

      sex <input type='radio' name='sex' value='male'>male

      <input type='radio' name='sex' value='female'>female

      <p>

      <input type='submit' value='insert' onclick='this.form.action="/insert"' >

      <input type='submit' value='update' onclick='this.form.action="/update"' >

      <input type='button' value='list'  onclick='location.href="/list"' >

     

</form>

</body>

</html>

Figure 15 /public/monksample.html

 

node.js를 실행하고 http://localhost:3000/monksample.html 을 실행해보자



Figure 16 http://localhost:3000/monksample.html 실행 결과

 

아래 insert 버튼을 누르면, 채워진 필드로 새로운 레코드를 생성하고, update 버튼은 user 필드에 있는 사용자 이름으로된 데이타를 업데이트 한다. list 버튼은 컬렉션에서 전체 데이타를 조회해서 출력하고, delete 버튼은 user 필드에 있는 사용자 이름으로된 레코드를 삭제한다. get 버튼은 user 필드에 있는 사용자 이름으로 데이타를 조회하여 리턴한다.

다음은 list로 전체 데이타를 조회하는 화면이다.

 


Figure 17 /list를 수행하여 mongoDB에 저장된 전체 데이타를 조회하는 화면


이 코드의 전체 소스코드는 https://github.com/bwcho75/nodejs_tutorial/tree/master/mongoDBexpress 에 있으니 필요하면 참고하기 바란다


다음 글에서는  node.js의 mongoDB ODM 프레임웍인 mongoose 이용한 접근 방법에 대해서 알아보기로 한다.


저작자 표시 비영리
신고


IBM 블루믹스 소개

 

PaaS

IBM 블루믹스는 IBM에서 제공하는 PaaS(Platform As A Service) 클라우드 서비스이다. 아마존과 같은 서비스가 VM을 제공하는 IaaS(Infra as a service)라면, 블루믹스는 node.js, Java와 같은 런타임을 미리 깔아놓고, 거기에 소스코드를 넣어서 돌리는 구조이다. IaaS의 경우 Linux Windows Server와 같은 OS VM 기반으로 제공하기 때문에 직접 미들웨어를 설치해서 사용해야 하지만, PaaS의 경우 이미 설치된 미들웨어 위에 코드만 돌리면되기 때문에, 아무래도 관리가 편리하다. 

그러면 왜 PaaS인가?

얼마전까지만 해도, 개발 트렌드의 중심은 기업체에서 개발하는 B2C서비스였다. 페이스북이나 네이버와 같은 서비스들이 대표적인데, B2C 서비스들은 대용량의 사용자를 커버해야 하고, 세세한 튜닝이나 설정 변경이 필요하고 다소 복잡한 아키텍쳐 구조를 가지기 때문에, 직접 인프라를 세팅하고 미들웨어를 설치하는 것이 오히려 유리했다. 그래서 IaaS를 많이 사용했는데,

근래에 들어서 개발의 중심이 모바일 앱이 되고 스타트업이 중심이 되면서, 적은 인원으로 빠르게 개발하고 관리할 수 있는 플랫폼이 필요하게되었고, 그로 인해서, Google App Engine이나, Heroku와 같은 PaaS 서비스가 각광받게 되었다.

IBM의 블루 믹스는?

지원 플랫폼

블루믹스는 APPS라는 개념을 가지고 있는데 이는 하나의 서비스로 보면된다. 이 안에, 서비스를 기동하기 위한 node.js mongodb와 같은 미들웨어를 묶어서 배포 할 수 있다. 아래 그림은 실제로 Terry라는 App mongodb 서비스를 추가하는 화면이다.



<App 에 추가할 서비스를 선택하는 화면>

매우 편하다. 클릭 몇번만으로, 내가 원하는 플랫폼을 쉽게 설치할 수 있다.

아래 화면은 node.js mongodb,redis로 구성된 서비스 환경이다.



<node.js mongodb,redis로 구성된 App>

현재 지원되는 플랫폼은 Java, Node.JS, Ruby on rials, Ruby Sinatra등을 지원한다.

부가 서비스 들은, 앞에서 언급한 mongodb, redis이외에도, rabbitMQ, IBM MQ, memcached,Work flow engine, Cloudant (CouchDB 계열) 등의 미들웨어 서비스 이외에도 Single Sign On, IOT (사물인터넷)등의 서비스를 부가로 지원한다. (꽤 많음)


코드 저장 및 반영

런타임에 적용되는 코드들은, 블루믹스에서 제공되는 git 저장소를 사용하면 된다.



<블루믹스 git 저장소>

재미있는 것중의 하나는, 웹브라우져상에서 코드 개발 에디터 기능 자체도 제공한다. 아래는 node.js의 코드를 웹 개발환경에서 편집하는 화면이다.



<블루믹스내에서 코드 편집하는 화면>

그리고, Atlassian JIRA와 같은 이슈 트랙킹 시스템을 제공한다. 공동 프로젝트를 관리하기 위해서는 태스크를 관리할 수 있는 시스템이 필요한데, 블루믹스에서는 IBM Jazz를 기반으로한 태스크 관리 시스템을 제공하고 있다. 개인적으로 예전에 Jazz를 사용했을때 상당히 무겁고 복잡하다는 느낌을 받았는데.. 어떤지는 조금 더 써봐야 알 수 있겠다.



<Jazz를 이용한 Task 정의 화면>

블루믹스는 앞에서 본것과 같이 서비스를 제공하기 위한 플랫폼만을 제공하는 것이 아니라, 형상관리,태스크 관리 및 빌드/배포 까지 자동화한 ALM (Application Life cycle management) End2End 기능을 제공한다.


서비스 관리

아래 화면은 node.js의 인스턴스 수를 조정하는 화면인데, 정말 쉽다. 아래 인스턴스 개수 숫자를 올려주면, 그만큼의 인스턴스가 가동되고, 각 인스턴스별 메모리양을 설정할 수 있다.



<그림. Node.js의 인스턴스 수를 조정하는 화면>

좀 특이한 점이 아마존처럼 VM단위로 과금을 하는게 아니라, 나한테 정해진 메모리 용량에 따라서, 이 안에서 인스턴스를 마음대로 만들 수 있는 개념인데, 구체적인 과금에 개념에 대해서는 향후에 조금 더 테스트를 해보고 올리도록 하겠다.

 

지금까지 간략하게나마 IBM PaaS 클라우드 블루믹스에 대해서 알아보았다. 특징은 무엇보다 쉽다!! 이다. 블루믹스 클라우드는 가입하면 무료 평가기간 동안 사용할 수 있으며 다른 클라우드 처럼 신용카드 번호를 넣지 않아도 된다. (URL : https://ace.ng.bluemix.net)

서버 개발 환경이 필요한 사람이 있으면 꼭 한번 사용해보기를 추천한다.

알림 : 본글은 IBM 블루믹스로 부터, 스폰서를 받는 글이 아닙니다!!! 혹시나 오해하지 마시기를..

저작자 표시
신고

빠르게 훝어보는 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를 사용하는 것이 좋다. 실제 프로그램에서는 위의 용도에 맞게 두 프레임웍을 섞어 쓰는 것이 좋다

저작자 표시
신고

빠르게 훝어보는 node.js

#6- mongo-native 모듈을 이용한 MongoDB 연동

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


아래 글은 오래되서 monk를 이용한 방식으로 새로운 글을 작성하였습니다.

새 글은 node.js 4.x , express 4.x 에서 기동하도록 작성되었습니다.

링크 : 

http://bcho.tistory.com/1080

Persistence 연동

node.js DB NoSQL등의 연동을 지원하는데, 이 역시 철저하게 non-blocking io 방식으로 동작한다. db 연결 socket을 열어서 query를 던져놓고, query 결과가 오면 이벤트를 받아서 callback 함수로 처리하는 순서이다.

그러면 여기서는 몇가지 persistence 연동 방식에 대해서 알아보도록 한다.

MongoDB

먼저 mongodb nosql 데이터베이스중에 가장 많이 사용되는 제품중의 하나이다. Json document를 저장하는 스타일의 document db이며, index grouping과 같은 RDBMS와 유사한 기능까지 지원하기 때문에 사용이 매우 쉽다.

설치 테스트

Mongodb mongodb.org에서 다운로드 받을 수 있다. Mongodb는 무료 tutorial이 잘되어 있는 것이 많은데, https://university.mongodb.com/ 에 가면 언어별 (node.js용도 있다) 튜토리얼이 있으니 참고하기 바란다.

윈도우를 기준으로 다운 받아서 압축을 푼후,  db 디렉토리를 만들어 주어야 하는데, 필요한 곳에 디렉토리를 만든다. 여기서 C:\dev\mongodb-win32-x86_64-2.4.3 \data 에 만들었다.

C:\dev\mongodb-win32-x86_64-2.4.3 에서 다음과 같은 명령어를 이용하여 구동한다.

.\bin\mongod --dbpath C:\dev\mongodb-win32-x86_64-2.4.3\data

인스톨이 끝났으면 간단한 테스트를 해보자, ./bin/mongo.exe를 수행하면 Java Script 기반의 쉘이 수행된다. 이해를 돕기 위해서 하나의 테이블에 Insert , select, delete, update를 수행하는 명령을 SQL 문장과 비교해서 소개한다.

Insert

SQL          : insert into users ("name","city") values("terry","seoul")

Mongo DB     : db.users.insert({_id:"terry",city:"seoul"})

 

Select

SQL          : select * from users where id="terry"

Mongo DB     : db.users.find({_id:"terry"})

 

Update

SQL          : update users set city="busan" where _id="terry"

Mongo DB     : db.users.update( {_id:"terry"}, {$set :{ city:"Busan" } } )

 

Delete

SQL          : delete from users where _id="terry"

Mongo DB     : db.users.remove({_id:"terry"})

 

간단하게 나마, mongodb query에 대해서 설명하였다.

개념적을 약간 더 설명하자면, dbRDBMS db와 같은 개념으로 보면 되고, collection rdbms의 하나의 테이블로 보면 된다. collection에 들어가는 데이터 필드중에서 _id predefined된 필드로 해당 데이터에 대한 primary key로 생각하면 되고, 위와 같이 사용자가 직접 값을 넣어서 입력할 수도 있고 또는 값을 넣지 않으면 mongodb에 의해서 unique한 값으로 자동 지정된다.

조금 더 자세한 쿼리에 대한 설명은 http://docs.mongodb.org/manual/crud/ 를 참고하기 바란다.

GUI 툴로는 robomongo 라는 툴이 있다. http://robomongo.org/



mongodb-native 모듈을 이용하기

node.js에서 mongodb를 연결하는 방법은 여러가지가 있다. 먼저 가장 널리 사용되는 mongo-native 모듈에 대해서 알아보자 https://github.com/mongodb/node-mongodb-native

1) mongo native module 설치하기

mongo native module

% npm install mongo 로 설치가 가능하며, 설치중에 native 모듈을 컴파일 하기 때문에 반드시 컴파일러 (Windows의 경우 Visual C++)이 깔려 있어야 한다.

2) 간단한 쿼리 수행하기

설치가 끝났으면 간단하게 select를 해서 내용을 json으로 리턴하는 코드를 만들어보자. Select를 할것이기 때문에 미리 mongo에 값을 넣어보자

% mongo.exe 를 실행한 후에 다음 쿼리를 수행한다.

db.users.insert({_id:'terry',city:'seoul'});

db.users.insert({_id:'cath',city:'suwon'});

 

제대로 입력이 되었는지 select를 해본다.



아래는 mongodb 도구인 robomongo를 이용해서 데이터가 들어가 있는 것을 확인한 화면이다.



자아 그러면 이제 코드를 구현해보자

var express = require('express');

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

var http = require('http');

var path = require('path');

var app = express();

var MongoClient = require('mongodb').MongoClient

var Server = require('mongodb').Server;

먼저 위와 같이 MongoClient Server 클래스를 생성한다

// 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')));

 

var mongoclient = new MongoClient(new Server('localhost',27017,{'native_parser':true}));

var db = mongoclient.db('test');

다음으로 mongoclient를 생성한다. 일종의 Connection 설정이라고 보면 되는데, 설정만 여기서 하는 것이지 실제 연결은 open을 호출해야 된다. 다음으로 client연결을 이용해서 사용할 db를 선택한다. 여기서는 ‘test’ db를 사용하였다.

 

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

   db.collection('users').findOne({},function(err,doc){

       if(err) throw err;

       res.send(doc);

   });

});

Db 객체를 얻었으면, 쿼리를 수행할 수 있는데, collection(‘collection.즉 테이블명’)으로 collection을 선택한후, findOne()메서드를 이용하여 하나의 row select를 하였다. 비동기 호출이기 때문에 query 수행이 끝나면, function(err,doc) callback 함수를 호출하는데, 위에서는 에러가 났을때는 err throw하고 에러가 없을 경우에는 리턴받은 json document response return하도록 하였다. 여기 까지 끝났으면, 실제 mongodb를 연결해보자 연결은 mongoclient.open을 하면 되는데, 연결이 완료되면 open안에 정의된 callback method를 호출한다.

아래코드를 보면 이 callback 메서드 안에서 httpServer를 띄운 것을 볼 수 있는데, http server가 기동되서 mongodb를 사용하기 때문에 mongoclient를 먼저 띄우기 위해서 httpserver callback안에 넣었다.

 

mongoclient.open(function(err, mongoclient) {

    if(err) throw err;

    console.log('mongo client connected');

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

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

    });

 

});

 

코드 구현이 끝났으면 실행해보자. 다음과 같이 레코드가 JSON으로 리턴됨을 확인할 수 있다.



이 코드에서 보면 mongoclient를 하나만 생성하였다. 그렇다면 내부적으로 물리적인 connection이 하나만 생길까? Mongoclient는 내부적으로 connection pooling을 이용한다. 그래서 별도의 설정을 해주지 않아도 내부적으로 여러 개의 connection을 여는데, default 5개의 connection을 열도록 되어 있고, connection의 수는 open 옵션에서 조정할 수 있다.

var mongoclient = new MongoClient(new Server('localhost',27017,{'native_parser':true,'poolSize':8,'maxPoolSize':10}));

 

위의 코드는 max connection 10개로 하고, 초기에 poolSize 8개로 지정한 경우이다. 실제로 기동해보면 8개의 connection이 생성되었음을 확인할 수 있다.

3) Insert, update and delete

Select는 해봤는데, 그러면 insert,update,delete는 어떻게 할까? mongodb query와 매우 유사하다.먼저 Insert를 보자

db.collection('users').insert({city:'suji'},function(err,doc){

       console.log('inserted '+doc[0]._id+':'+doc[0].city);

   });

위와 같이 collection을 선택한다음에, insert메서드를 이용해서 json document를 넣으면 된다. 위에서는 callback function을 지정한 예인데, callback function은 생략할 수 도 있다. 위의 코드를 잘 보면 앞에 예제와는 다른게 _id값을 입력하지 않은 것을 볼 수 있다. 이 경우 _id값은 자동으로 mongodb가 생성하여 unique한 값을 넣게 된다.



그리고 insert된 값은 callback의 두번째 인자에서 array 형태로 넘겨 받게 된다.

또는

db.collection('users').insert([{city:'suji'},{city:'busan'}],function(err,doc){

          

와 같은 배열 형태를 사용하면, 하나의 insert문장으로 여러 개의 document를 동시에 insert할 수 있다.( batch insert가 가능하다)

삭제는 insert와 유사하게 remove 메서드를 이용하면 된다.

db.collection('users').remove({city:'busan'},function(err,doc){});

위의 예제는 city 필드가 busan인 것을 삭제한 것인데, _id 필드 이외에는 index로 지정이 되어 있지 않기 때문에 index를 지정하지 않은 필드로 삭제등을 했을 경우 table full scan이 발생할 수 있으니 주의하도록 해야 한다. (한마디로 느려진다는 이야기)

다음으로 수정은 collection.update 메서드를 이용하면 된다.

db.collection('users').update({_id:'terry'},{$set:{'sex':'male'}},function(err,doc){

       throw err;

   });

_id terry document‘sex’ 필드를 ‘male’로 변경하는 쿼리이다. 이미 ‘sex’필드가 있으면 그 내용을 ‘male’로 바꾸고, 없으면 새롭게 ‘sex’필드를 만든다.앞에 $set을 줬기 때문에 ‘sex’필드만 내용을 바꾸는데,

db.collection('users').update({_id:'terry'},{'sex':'male'},function(err,doc){

       throw err;

   });

$set을 빼버리게 되면 _id=’terry’ document의 내용을 {‘sex’:’male’}로 바꿔 버린다. (필드만 추가하는게 아니다.)

4) Search

앞에서는 findOne만 해서 하나의 record query 하는 예제 였는데, 여러 개의 record를 받고 싶으면 find 메서드를 사용해서 검색 조건을 주고 .toArray를 호출하면 인자로 넘어가는 callback함수의 docs 인자에 쿼리 결과를 배열로 리턴해준다.

db.collection('users').find({city:'suji'}).toArray(function(err,docs) {

       if (err) throw err;

       res.send(docs);

       for (i = 0; i < docs.length; i++) {

           console.log(docs[i].city);

       }

   });

여기서는 아주 기본적인 API만을 다뤘기 때문에 자세한 API들은 http://mongodb.github.io/node-mongodb-native/api-generated 를 참고하기 바란다

저작자 표시
신고

CouchDB Overview


어제 SSAG 에서 정명훈 이사가 강의한 CouchDB에 대한 내용 정리


CouchDB 일반 특징

CouchDB Apache 프로젝트로 MongoDB와 같은 Document DB의 형태를 띄며, NOSQL CAP이론중 AP 에 해당 한다. (장애에 매우 강하다.) 단 Consistency는 Eventual Consistency를 제공한다. (버전으로 하는 방식), Eventual Consistency 모델이기 때문에 Locking을 사용하지 않는다. (Optimistic Lock)


유사 프로젝트

유사 프로젝트로 CouchBase와 BigCouch, Cloudant등이 있다.

CouchBase는 memcached와 CouchDB를 합쳐놓은 제품으로, 앞단에 캐쉬가 있어서 성능이 매우 빠르다. BigCouch는 CouchDB가 single instance만 지원하는데 반해서, 클러스터링 기능을 가지고 있다. Cloudant는 이 BigCouch를 기반한 제품으로 DBaaS 형식의 서비스로, DB 서비스를 제공하고, 운영을 대행해준다. 향후 BigCouch와 CouchDB가 합쳐져서 클러스터링 기능을 제공할 예정이다.

그 외에도 mobile에 탑재 가능한 버전의 CouchDB들이 있다.


Secondary Index

재미있는 특징중의 하나가 Sencondary Index를 지원하는 데 이는 내부적인 Map & Reduce를 통해서 구현되는데, 2nd index search시마다 MR이 도는 것이 아니라, 2nd index를 만들어 놓은 다음에 이를 View와 같은 형태로 저장해놓고, 이 index 내에서 검색을 한다. 또한 새로운 데이타가 추가되거나 변경이 되더라도 전체 index를 rebuild하는 것이 아니라, incremental 방식으로 변경된 부분만 재 생성한다.

물론 이 index를 rebuild하는 과정에서 서버가 부하를 받아서 성능이 다소 떨어질 수 있으나, 초당 수천건 정도의 index 변경은 수용할 수 있는 수준이라고 한다.


Replication

CouchDB의 강력한 기능중의 하나가 복제 기능인데, 단방향, 양방향 복제, 1:N, M:N 그리고 필터나 룰 기반의 복제(서브셋만 복제하는 기능)도 가능하다. 1회성으로 복제하는 시나리오와 지속적으로 복제하는 시나리오도 가능하면, 네트워크에서 끊어졌다가 나중에 붙어도 복제 및 Sync만 하는 것도 가능하다.

무엇보다 CouchDB는 모바일 버전이 있기 때문에, 이를 이용하면 쉽게 단말과 서버간의 Sync 시나리오를 구현할 수 있다. 오픈 소스로 파일이나 이미지를 서버와 디바이스간에 복제하는 프로젝트가 있다.


Interface

인터페이스는 HTTP REST만 지원한다. 아쉬운 부분인데, 성능이 높은 Protocol Buffer등을 향후 지원했으면 한다.


보안

HTTPS와 OAuth 인증을 제공하며, HTTP Basic Auth도 지원한다. User Identity source (LDAP)등의 연동에 대해서는 자세하게 설명되지 않았는데, 앞단에 reverse proxy를 둬서 인증하는 방식으로 구현이 가능하리라 본다.


대략적인 아키텍쳐

기본적인 메카니즘이 append only 구조로, 일정 이상의 데이타가 쌓이면 내부적으로 자동 Compaction을 한다.

Erlang으로 구현되어 있으며, 그 위에 javascript를 제공하여, MR등은 javascript를 사용하여 구현한다.



저작자 표시
신고


import pika
import ast
import pymongo
import datetime
import logging
import time
import sys,traceback,socket,threading
from datetime import datetime
from time import sleep

# configuration
MONGODB_NAME = "terrydb"
HOSTNAME = ':'+socket.gethostname()
QUEUE_NAME = 'hello'
MONGODB_URL= 'mongodb://localhost'
RABBITMQ_URL='localhost'

LOG_FORMAT = ('[%(levelname)s] %(asctime)s %(name)s : %(message)s')
LOGGER = logging.getLogger(__name__)
              
class WorkerThread(threading.Thread):
    def __init__(self,threadID,name,counter):
        threading.Thread.__init__(