몽고디비는 자바스크립트 객체를 그대로 저장할 수 있어서 자바스크립트 언어를 사용하는 노드에서 데이터를 저장하기 좋은 데이터베이스이다.
06-1. 몽고디비 시작하기
몽고디비는 관계형 데이터베이스와 달라 SQL을 사용하지 않는다.
몽고디비란?
비관계형 데이터베이스로 NoSQL, Not Only SQL이라고 한다. NoSQL 데이터베이스는 성능을 최우선으로 생각하기 때문에 실시간으로 처리해야 하는 경우나 대용량 트래픽을 감당할 수 있는 메시징 시스템 등에 활용된다.
몽고디비는 테이블 개념이 없이 여러거 데이터가 모인 하나의 단위를 **Collection(컬렉션)**이라 부른다. 컬렉션은 테이블과 달리 데이터를 정해 놓은 column의 형태대로 넣어야 한다는 제약은 없다.
즉, 데이터베이스는 컬렉션의 집합이라고 할 수 있다. 그리고 각각의 컬렉션은 여러 개의 문서 객체를 가질 수 있다. 이 문서 객체들은 관계형 데이터베이스에서 한 줄의 데이터인 레코드와 비슷하다. 하지만 다른 문서 객체와 똑같은 속성을 가질 필요가 없어서 필요에 따라 완전히 다른 속성을 넣어 둘 수 있다.
몽고디비에 데이터를 추가하거나 조회하기
셸(Shell)
셸이란 명령을 받아서 실행하는 프로그램이다.
$ mongo
데이터 베이스는 db라는 이름으로 접근할 수 있으며 그 안에 컬렉션을 만들고 문서를 저장할 수 있다. 컬렉션을 별도로 만들지 않고 해당 컬렉션에 필요한 작업을 실행하도록 지정만해도 자동으로 만들어진다.
insert() : 데이터 추가
$ db.users.insert({name:'박우진',age:19})
find() : 데이터 조회
$ db.users.find().pretty()
remove() : 데이터 삭제
06-2. 익스프레스에서 몽고디비 사용하기
regexp 표현식 : 정규표현식의 약자로 특정한 규칙을 가진 문자열의 집합이다.
//몽고디비 모듈 사용var MongoClient =require('mongodb').MongoClient;//데이터베이스 객체를 위한 변수 선언var database;//데이터 베이스 연결functionconnectDB(){//DB 연결정보var databaseUrl ='mongodb://localhost:27017/local';//DB연결MongoClient.connect(databaseUrl,function(err,db){if(err) throw err;console.log('데이터베이스에 연결되었습니다. : ',databaseUrl);//database변수 할당 database = db; });}...http.createServer(app).listen(3000,function(){console.log('3000포트에서 시작'+app.get('port'));connectDB();});
id속성값에 unique가 true이면 자동으로 인덱스가 만들어진다. 위치 기반 서비스를 위해 저장되는 경위도 좌표에 공간 인덱싱이 사용된다.{type: [Number],index:'2d',sparse: true}
스키마 객체 메소드
메소드
설명
static(name,fn)
모델 객체에서 사용할 수 있는 함수 등록
함수의 이름과 함수 객체를 마라미터로 전달
method(name,fn)
모델 인스턴스 객체에서 사용할 수 있는 함수 등록
함수의 이름과 함수 객체를 마라미터로 전달
사용자 리스트 조회 기능 추가
//데이터 베이스 연결functionconnectDB(){//DB 연결정보var databaseUrl ='mongodb://localhost:27017/local';console.log('데이터베이스 연결을 시도합니다.');mongoose.Promise =global.Promise;mongoose.connect(databaseUrl); database =mongoose.connection;database.on('error',console.error.bind(console,'mongoose connection error'));database.on('open',function(){console.log('데이터베이스에 연결되었습니다.'+databaseUrl);//스키마정의 UserSchema =mongoose.Schema({ id: {type: String,required:true, unique:true}, password: {type: String,required:true}, name: {type: String, index:'hashed'}, age: { type: Number,'default':-1}, created_at: {type:Date, index: {unique:false},'default':Date.now}, updated_at: {type:Date, index: {unique:false},'default':Date.now} });// 스키마에 static메소드 추가UserSchema.static('findById',function(id,callback){returnthis.find({id: id},callback); });UserSchema.static('findAll',function(callback){returnthis.find({ },callback); });console.log('schema정의');//모델 정의 UserModel =mongoose.model("users2",UserSchema);console.log('모델정의'); });//연결끊어지면 5초후 재연결database.on('disconnected',function(){console.log('연결이 끊어짐. 재연결');setInterval(connectDB,5000); });}//사용자를 인증하는 함수varauthUser=function(database,id,password,callback){console.log('authUser호출');//id,password검색UserModel.findById(id,function(err,results){if(err){callback(err,null);return; }console.log('id : %s로 사용자 검색 결과',id);console.dir(results);if(results.length>0){console.log('아이디와 일치하는 사용자 찾음');//비밀번호 확인if(results[0]._doc.password == password){console.log('비밀번호 일치');callback(null,results); }else{console.log('비밀번호 일치하지 않음');callback(null,null); } }else{console.log('아이디와 일치하는 사용자를 찾지못함');callback(null,null); } });}...//사용자 리스트 함수router.route('/process/listuser').post(function(req,res){console.log('/process/listuser 호출');//데이터베이스 객체가 최기화된 경우, 모델객체의 findAll메소드 호출if(database){//1.모든 사용자 검색UserModel.findAll(function(err,results){//오류가 발생햇을 때 클라이언트로 오류 전송if(err){console.log('사용자 리스트 조회 중 오류 발생'+err.stack);res.writeHead('200',{'Content-Type':'text/html;charset=utf8'});res.write('<h1>사용자 리스트 조회 중 오류 발생</h1>');res.write('<p>'+err.stack+'</p>');res.end(); return; }if(results){ // 결과 객체 있으면 리스트 전송console.dir(results);res.writeHead('200',{'Content-Type':'text/html;charset=utf8'});res.write('<h1>사용자 리스트</h1>');res.write('<div><ul>');for(var i=0;i<results.length;i++){var curId = results[i]._doc.id;var curName = results[i]._doc.name;res.write(' <li>#'+i+' : '+curId+','+curName+'</li>'); }res.write('</ul></div>');res.end(); }else{ // 결과 객체가 없으면 실패 응답 전송res.writeHead('200',{'Content-Type':'text/html;charset=utf8'});res.write('<h1>사용자 리스트 조회 실패</h1>');res.end(); } }); }else{ // 데이터베이스 객체가 초기화되지 않았을때 실패응답 전송res.writeHead('200',{'Content-Type':'text/html;charset=utf8'});res.write('<h1>DB연결 실패</h1>');res.end(); }});...
_doc속성은 각 문서 객체의 정보를 담고 있다.
06-5 비밀번호암호화하여 저장하기
virtual 함수 사용하기
비밀번호는 단방향으로 암호화하여 원본 비밀번호 문자열을 알 수 없도록 만든다.
단방향 암호화는 암호화된 데이터는 다시 원본으로 복구 할 수 없다. 원본 글자를 복구하는 과정을 복호화라고 하는데 복호화할 수 있는 방법이 없으므로 한쪽 방향으로만 암호화가 가능하다는 의미이다.
virtual()은 문서 객체에 실제로 저장되는 속성이 아니라 가상의 속성을 지정할 수 있다. 문서 객체를 저장할 때 set()메소드로 지정한 함수가 필요한 작업을 수행하며, 문서 객체를 조회할 때 get()메소드로 지정한 함수가 실행된다.
스키마 객체의 virtual()함수 사용법 알아보기
//====모듈 불러들이기 ====//var mongodb =require('mongodb');var mongoose =require('mongoose');//db연결var database;var UserSchema;var UserModel;//데이터베이스에 연결하고 응답 객체의 속성으로 db객체 추가functionconnectDB(){//db 연결 정보var databaseUrl ='mongodb://localhost:27017/local';//db 연결mongoose.connect(databaseUrl); database =mongoose.connection;database.on('error',consol.error.bind(console,'mongoose connection error.'));database.on('open',function(){console.log('db연결됨'+databaseUrl);//user 스키마 및 모델 객체 생성createUserSchema();//testdoTest(); });database.on('disconnected',connectDB);}// user스키마 및 모델 객체 생성functioncreateUserSchema(){//스키마 정의//password를 hashed_password로 변경,default속성 추가, salt속성 추가 UserSchema =mongoose.Schema({ id: {type: String,required:true, unique:true}, name: {type: String, index:'hashed','default':''}, age: { type: Number,'default':-1}, created_at: {type:Date, index: {unique:false},'default':Date.now}, updated_at: {type:Date, index: {unique:false},'default':Date.now} });//info를 virtual 메소드로 정의 UserSchema.virtual('info').set(function(info){var splitted =info.split(' ');this.id = splitted[0];this.name = splitted[1];console.log('virtual info 설정 : %s %s',this.id,this.name); }).get(function(){ returnthis.id +' '+this.name});console.log('UserSchema정의');//UserModel 모델 정의 UserModel =mongoose.model("users4",UserSchema);console.log('UserModel 정의함');functiondoTest(){//UserModel 인스턴스 생성//id, name 속성은 할당하지 않고 info속성만 할당var user =newUserModel({"info":'test01 박우진'});//save()로 저장user.save(function(err){if(err){throw err;}console.log("사용자 데이터 추가함");findAll(); });console.log('info속성에 값 할당함');console.log('id: %s, name : %s',user.id,user.name); }functionfindAll(){UserModel.find({},function(err,results){if(err){throw err;}if(results){console.log('조회된 user문서 객체 #0 -> id : %s, name :%s',results[0]._doc.id,results[0]._doc.name); } }); }}
비밀번호 암호화하여 저장하는 코드 적용하기
노드는 암호화를 위해 crypto모듈을 제공한다.
...//===== 데이터베이스 연결 =====//// 데이터베이스 객체를 위한 변수 선언var database;// 데이터베이스 스키마 객체를 위한 변수 선언var UserSchema;// 데이터베이스 모델 객체를 위한 변수 선언var UserModel;//데이터베이스에 연결functionconnectDB() {// 데이터베이스 연결 정보var databaseUrl ='mongodb://localhost:27017/local';// 데이터베이스 연결console.log('데이터베이스 연결을 시도합니다.');mongoose.Promise =global.Promise; // mongoose의 Promise 객체는 global의 Promise 객체 사용하도록 함mongoose.connect(databaseUrl); database =mongoose.connection;database.on('error',console.error.bind(console,'mongoose connection error.')); database.on('open',function () {console.log('데이터베이스에 연결되었습니다. : '+ databaseUrl);// user 스키마 및 모델 객체 생성createUserSchema(); });// 연결 끊어졌을 때 5초 후 재연결database.on('disconnected',function() {console.log('연결이 끊어졌습니다. 5초 후 재연결합니다.');setInterval(connectDB,5000); });}// user 스키마 및 모델 객체 생성functioncreateUserSchema() {// 스키마 정의// password를 hashed_password로 변경, 각 칼럼에 default 속성 모두 추가, salt 속성 추가 UserSchema =mongoose.Schema({ id: {type: String, required:true, unique:true,'default':''}, hashed_password: {type: String, required:true,'default':''}, salt: {type:String, required:true}, name: {type: String, index:'hashed','default':''}, age: {type: Number,'default':-1}, created_at: {type: Date, index: {unique:false},'default':Date.now}, updated_at: {type: Date, index: {unique:false},'default':Date.now} });// password를 virtual 메소드로 정의 : MongoDB에 저장되지 않는 가상 속성임. // 특정 속성을 지정하고 set, get 메소드를 정의함 UserSchema.virtual('password').set(function(password) {this._password = password;this.salt =this.makeSalt();this.hashed_password =this.encryptPassword(password);console.log('virtual password의 set 호출됨 : '+this.hashed_password); }).get(function() {console.log('virtual password의 get 호출됨.');returnthis._password; });// 스키마에 모델 인스턴스에서 사용할 수 있는 메소드 추가// 비밀번호 암호화 메소드UserSchema.method('encryptPassword',function(plainText, inSalt) {if (inSalt) {returncrypto.createHmac('sha1', inSalt).update(plainText).digest('hex'); } else {returncrypto.createHmac('sha1',this.salt).update(plainText).digest('hex'); } });// salt 값 만들기 메소드UserSchema.method('makeSalt',function() {returnMath.round((newDate().valueOf() *Math.random())) +''; });// 인증 메소드 - 입력된 비밀번호와 비교 (true/false 리턴)UserSchema.method('authenticate',function(plainText, inSalt, hashed_password) {if (inSalt) { console.log('authenticate 호출됨 : %s -> %s : %s', plainText, this.encryptPassword(plainText, inSalt), hashed_password);
returnthis.encryptPassword(plainText, inSalt) === hashed_password; } else {console.log('authenticate 호출됨 : %s -> %s : %s', plainText,this.encryptPassword(plainText),this.hashed_password);returnthis.encryptPassword(plainText) ===this.hashed_password; } });// 값이 유효한지 확인하는 함수 정의varvalidatePresenceOf=function(value) {return value &&value.length; };// 저장 시의 트리거 함수 정의 (password 필드가 유효하지 않으면 에러 발생)UserSchema.pre('save',function(next) {if (!this.isNew) returnnext();if (!validatePresenceOf(this.password)) {next(newError('유효하지 않은 password 필드입니다.')); } else {next(); } })// 필수 속성에 대한 유효성 확인 (길이값 체크)UserSchema.path('id').validate(function (id) {returnid.length; },'id 칼럼의 값이 없습니다.');UserSchema.path('name').validate(function (name) {returnname.length; },'name 칼럼의 값이 없습니다.');UserSchema.path('hashed_password').validate(function (hashed_password) {returnhashed_password.length; },'hashed_password 칼럼의 값이 없습니다.');// 스키마에 static으로 findById 메소드 추가UserSchema.static('findById',function(id, callback) {returnthis.find({id:id}, callback); });// 스키마에 static으로 findAll 메소드 추가UserSchema.static('findAll',function(callback) {returnthis.find({}, callback); });console.log('UserSchema 정의함.');// User 모델 정의 UserModel =mongoose.model("users3", UserSchema);console.log('users3 정의함.');}...// 사용자를 인증하는 함수 : 아이디로 먼저 찾고 비밀번호를 그 다음에 비교하도록 함varauthUser=function(database, id, password, callback) {console.log('authUser 호출됨 : '+ id +', '+ password);// 1. 아이디를 이용해 검색UserModel.findById(id,function(err, results) {if (err) {callback(err,null);return; }console.log('아이디 [%s]로 사용자 검색결과', id);console.dir(results);if (results.length>0) {console.log('아이디와 일치하는 사용자 찾음.');// 2. 패스워드 확인 : 모델 인스턴스를 객체를 만들고 authenticate() 메소드 호출var user =newUserModel({id:id});var authenticated =user.authenticate(password, results[0]._doc.salt, results[0]._doc.hashed_password);if (authenticated) {console.log('비밀번호 일치함');callback(null, results); } else {console.log('비밀번호 일치하지 않음');callback(null,null); } } else {console.log("아이디와 일치하는 사용자를 찾지 못함.");callback(null,null); } });}...
06-6 MySQL 데이터 베이스 설치하기
$ npm install mysql --save
관계형 데이터베이스에 연결할 때는 보통 Connection Pool을 사용한다. 데이터베이스 연결 객체가 너무 많이 만들어지는 것을 막고 한번 만든 연결을 다시 사용할 수 있게 한다.