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


Archive»


 
 

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 이용한 접근 방법에 대해서 알아보기로 한다.


빠르게 훝어보는 node.js

#12 - Socket.IO (4/4)

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


채팅 프로그램에 (room/그룹) 기능을 추가하기

다음은 앞에서 만든 1:1 귓속말이 가능한 채팅에 채팅방기능을 추가한 버전이다.

var express = require('express');

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

var http = require('http');

var path = require('path');

 

var app = express();

app.use(express.bodyParser());

app.use(express.cookieParser('your secret here'));

app.use(express.session());

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

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

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

app.use(express.favicon());

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

app.use(express.json());

app.use(express.urlencoded());

app.use(express.methodOverride());

app.use(app.router);

 

var httpServer =http.createServer(app).listen(3000, function(req,res){

    console.log('Socket IO server has been started');

});

// upgrade http server to socket.io server

var io = require('socket.io').listen(httpServer);

 

var count = 0;

var rooms = [];

 

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

    console.log('room name is :'+req.params.room);

    res.render('index',{room:req.params.room});

});

 

 

 

io.sockets.on('connection',function(socket){

 

    socket.on('joinroom',function(data){

        socket.join(data.room);

 

        socket.set('room', data.room,function() {

            var room = data.room;

            var nickname = '손님-'+count;

            socket.set('nickname',nickname,function(){

                socket.emit('changename', {nickname: nickname});

 

                // Create Room

                if (rooms[room] == undefined) {

                    console.log('room create :' + room);

                    rooms[room] = new Object();

                    rooms[room].socket_ids = new Object();

                }

                // Store current user's nickname and socket.id to MAP

                rooms[room].socket_ids[nickname] = socket.id

 

                // broad cast join message

                data = {msg: nickname + ' 님이 입장하셨습니다.'};

                io.sockets.in(room).emit('broadcast_msg', data);

 

                // broadcast changed user list in the room

                io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                count++;

            });

        });

 

    });

 

    socket.on('changename',function(data){

        socket.get('room',function(err,room){

            socket.get('nickname',function(err,pre_nick) {

                var nickname = data.nickname;

                // if user changes name get previous nickname from nicknames MAP

                if (pre_nick != undefined) {

                    delete rooms[room].socket_ids[pre_nick];

                }

                rooms[room].socket_ids[nickname] = socket.id

                socket.set('nickname',nickname,function() {

                    data = {msg: pre_nick + ' 님이 ' + nickname + '으로 대화명을 변경하셨습니다.'};

                    io.sockets.in(room).emit('broadcast_msg', data);

 

                    // send changed user nickname lists to clients

                    io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                });

            });

 

        });

    });

 

 

    socket.on('disconnect',function(data){

        socket.get('room',function(err,room) {

            if(err) throw err;

            if(room != undefined

                && rooms[room] != undefined){

 

                socket.get('nickname',function(err,nickname) {

                    console.log('nickname ' + nickname + ' has been disconnected');

                    // 여기에 방을 나갔다는 메세지를 broad cast 하기

                    if (nickname != undefined) {

                        if (rooms[room].socket_ids != undefined

                            && rooms[room].socket_ids[nickname] != undefined)

                            delete rooms[room].socket_ids[nickname];

                    }// if

                    data = {msg: nickname + ' 님이 나가셨습니다.'};

 

                    io.sockets.in(room).emit('broadcast_msg', data);

                    io.sockets.in(room).emit('userlist', {users: Object.keys(rooms[room].socket_ids)});

                });

            }

        }); //get

    });

 

    socket.on('send_msg',function(data){

        socket.get('room',function(err,room) {

            socket.get('nickname',function(err,nickname) {

                console.log('in send msg room is ' + room);

                data.msg = nickname + ' : ' + data.msg;

                if (data.to == 'ALL') socket.broadcast.to(room).emit('broadcast_msg', data); // 자신을 제외하고 다른 클라이언트에게 보냄

                else {

                    // 귓속말

                    socket_id = rooms[room].socket_ids[data.to];

                    if (socket_id != undefined) {

 

                        data.msg = '귓속말 :' + data.msg;

                        io.sockets.socket(socket_id).emit('broadcast_msg', data);

                    }// if

                }

                socket.emit('broadcast_msg', data);

            });

        });

    })

});

코드를 살펴보자

처음에 입장은 http://localhos:3000/{방이름} 으로 하게 된다.

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

    console.log('room name is :'+req.params.room);

    res.render('index',{room:req.params.room});

});

그러면 URL에 있는 방이름을 받아서, index.ejs에 있는 UI로 채팅창을 띄워주고 방이름을 parameterindex.ejs에 넘겨준다.

 

    socket.on('joinroom',function(data){

        socket.join(data.room);

클라이언트가 서버에 접속되면 먼저 클라이언트가 join 이벤트를 보내는데, join 이벤트를 받으면, 이때 같이 room 이름으로 현재 소켓을 room 이름의 room join한다.

        socket.set('room', data.room,function() {

다음으로, 해당 소켓이 어느 룸에 있는지 set 명령을 이용하여 socket 저장해놓는다.

            var room = data.room;

            var nickname = '손님-'+count;

            socket.set('nickname',nickname,function(){

                socket.emit('changename', {nickname: nickname});

 

                // Create Room

                if (rooms[room] == undefined) {

                    console.log('room create :' + room);

                    rooms[room] = new Object();

                    rooms[room].socket_ids = new Object();

                }

윗부분이 room 데이터 객체를 생성하는 것인데, 앞의 예제와는 달리, 현재 연결된 클라이언트의 socket.id 이제는 room 단위로 관리를 해야 한다. 그래서 rooms라는 객체를 이용하여, 해당 room 대해서 rooms.room이라는 객체로 만들고, 그리고, room 현재 연결된 클라이언트 socket.id 저장하는 socket_ids 객체를 생성한다.

                // Store current user's nickname and socket.id to MAP

                rooms[room].socket_ids[nickname] = socket.id

 

그리고 나서, socket_ids 귓속말 채팅방 예제와 같이 nickname to socket.id 대한 맵핑 정보를 저장한다.

                // broad cast join message

                data = {msg: nickname + ' 님이 입장하셨습니다.'};

                io.sockets.in(room).emit('broadcast_msg', data);

// broadcast changed user list in the room

                io.sockets.in(room).emit('userlist', {users:

Object.keys(rooms[room].socket_ids)});

                count++;

            });

그리고 위와 같이 현재 room 들어 있는 클라이언트들에게만 , 새로운 사용자가 입장했음을 알리고, 사용자 리스트를 업데이트하는 이벤트를 보낸다.

disconnect 대한 부분도 크게 달라진 것이 없다. Socket_ids 객체가 rooms 아래로 들어갔고, 메시지를 보낼때, 귓속말 채팅방 예제가 io.sockets.emit 대신에, room 범위를 지정하는 in() 메서드를 써서 io.sockets.in(room).emit 같이 보내게 된다.

Sendmsg 이벤트 부분도, broadcast하는 부분에서 to를 이용하여 다음과 같이socket.broadcast.to(room).emit 특정 room에 있는 클라이언트에게만 메시지를 보내는 것으로만 변경되었다

아래는 클라이언트쪽의 코드이다. 앞의 예제에서 나온 귓속말이 가능한 대화방과 코드가 거의 동일하다.

<script type="text/javascript">

        var socket = io.connect('http://localhost');

        socket.emit('joinroom',{room:'<%=room%>'});

처음에 접속하였을 때, 서버 코드에서 방이름을 URL로부터 읽어서, 그 방이름으로 join 하는 이벤트를 보낸다.

/vies/index.ejs

<html>

<head>

 

    <title></title>

    <script src="/socket.io/socket.io.js"></script>

    <script src="//code.jquery.com/jquery-1.11.0.min.js"></script>

 

</head>

<body>

 

<b>Welcome ROOM : <%= room%></b><p>

    Name <input type="text" id="nickname" /> <input type="button" id="changename" value="Change name"/><br>

    To

    <select id="to">

        <option value="ALL">ALL</option>

    </select>

    Message  <input type="text" id="msgbox"/>

    <br>

    <span id="msgs"></span>

 

    <script type="text/javascript">

        var socket = io.connect('http://localhost');

        socket.emit('joinroom',{room:'<%=room%>'});

 

        $('#changename').click(function(){

            socket.emit('changename',{nickname:$('#nickname').val()});

        });

        $("#msgbox").keyup(function(event) {

            if (event.which == 13) {

                socket.emit('send_msg',{to:$('#to').val(),msg:$('#msgbox').val()});

                $('#msgbox').val('');

            }

        });

        socket.on('new',function(data){

            console.log(data.nickname);

            $('#nickname').val(data.nickname);

        });

 

        // 새로운 사용자가 들어오거나, 사용자가 이름을 바꿨을때 "To" 리스트를 변경함

        socket.on('userlist',function(data){

            var users = data.users;

            console.log(users);

            console.log(data.users.length);

            $('#to').empty().append('<option value="ALL">ALL</option>');

            for(var i=0;i<data.users.length;i++){

                $('#to').append('<option value="'+users[i]+'">'+users[i]+"</option>");

            }

        });

 

        socket.on('broadcast_msg',function(data){

            console.log(data.msg);

            $('#msgs').append(data.msg+'<BR>');

        });

    </script>

</body>

</html>

 

다음은 실제 실행 화면이다.



빠르게 훝어보는 node.js

#11 - Socket.IO (3/4)

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


앞서 작성한 대화방 http://bcho.tistory.com/896 에 이어서 이번에는 1:1 귓속말 대화가 가능한 기능을 추가해보자


귓속말이 가능한 대화방

이번에는 특정 사용자가 다른 사용자에게 귓속말을 보내는 기능을 가지는 대화방을 만들어보자. 이를 통해서 특정 클라이언트 소켓에 메세지를 어떻게 보내는지를 배울 수 있다.

이 대화방의 기능은 다음과 같다.

  • Ÿ   대화방에 입장하는 손님들은 자동으로 대화명을 부여 받는다.

  • Ÿ   사용자는 대화명을 바꿀 수 있다.

  • Ÿ   사용자는 대화 수신자의 대화명을 선택하여 특정 사용자에게 귓속말을 보낼 수 있다.

코드를 보면서 설명하도록 하자.

app.js

var express = require('express');

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

var http = require('http');

var path = require('path');

 

var app = express();

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

 

var httpServer =http.createServer(app).listen(3000, function(req,res){

    console.log('Socket IO server has been started');

});

// upgrade http server to socket.io server

var io = require('socket.io').listen(httpServer);

 

var socket_ids = [];

var count = 0;

 

function registerUser(socket,nickname){

    // socket_id nickname 테이블을 셋업

    socket.get('nickname',function(err,pre_nick){

        if(pre_nick != undefined ) delete socket_ids[pre_nick];

        socket_ids[nickname] = socket.id

        socket.set('nickname',nickname,function(){

            io.sockets.emit('userlist',{users:Object.keys(socket_ids)});

        });

 

    });

}

 

io.sockets.on('connection',function(socket){

    socket.emit('new',{nickname:'GUEST-'+count});

    registerUser(socket,'GUEST-'+count);

    count++;

 

    socket.on('changename',function(data){

        registerUser(socket,data.nickname);

    });

    socket.on('disconnect',function(data){

        socket.get('nickname',function(err,nickname){

            if(nickname != undefined){

                delete socket_ids[nickname];

                io.sockets.emit('userlist',{users:Object.keys(socket_ids)});

                                

            }// if

        });

     });

    socket.on('send_msg',function(data){

        socket.get('nickname',function(err,nickname){

 

            data.msg = nickname + ' : '+data.msg;

            if(data.to =='ALL') socket.broadcast.emit('broadcast_msg',data); // 자신을 제외하고 다른 클라이언트에게 보냄

            else{

                socket_id = socket_ids[data.to];

                if(socket_id != undefined){

                    io.sockets.socket(socket_id).emit('broadcast_msg',data);

                }// if

            }

            socket.emit('broadcast_msg',data);

        });

    });

});

 

먼저 Express에서 socket.io를 사용할 준비를 하고

특정 사용자 즉 클라이언트에게만 메세지를 보내려면

io.sockets.socket(socket_id).emit

메세드를 사용해야 한다. 해당 클라이언트의 socket_id를 알아야 하며, 우리는 대화명(이하 nickname)을 통해서 특정 사용자에게 메세지를 보낼 것이기 때문에 nickname 에서 socket_id로의 맵핑 테이블이 필요하다.

var socket_ids = []

에서 socket_ids nickname to socket.id에 대한 맵핑 정보를 저장한다.

io.sockets.on('connection',function(socket){

    socket.emit('new',{nickname:'GUEST-'+count});

    registerUser(socket,'GUEST-'+count);

    count++;

클라이언트가 접속되면 new 라는 이벤트를 통해서 nickname을 생성해서 보낸다. nickname은 사용자가 접속한 순서대로 GUEST-0,GUEST-1,… 식으로 순차적으로 이름을 배정한다.

다음으로, 새로운 사용자가 추가되었음을 알리고, 현재 사용자 리스트들을 보내야 하는데, 이를 registerUser 메서드에서 수행한다.

function registerUser(socket,nickname){

    // socket_id nickname 테이블을 셋업

    socket.get('nickname',function(err,pre_nick){

        if(pre_nick != undefined ) delete socket_ids[pre_nick];

        socket_ids[nickname] = socket.id

        socket.set('nickname',nickname,function(){

            io.sockets.emit('userlist',{users:Object.keys(socket_ids)});

        });

 

    });

}

먼저 register user에서는 socket.get을 이용하여 현재의 클라이언트 소켓의 nicknamepre_nick이라는 변수로 읽어온다. 대화명이 바뀔 경우 기존에 socket_ids에 기존의 대화명으로 저장되어 있는 socket.id를 삭제하기 위함이다. 기존의 데이타를 삭제 하였으면, socket_ids nickname Key 값으로 하여, socket.id를 저장한다. 다음. nickname을 해당 socket set명령을 이용해서 저장한후에, userlist라는 이벤트를 통해서, 현재 접속된 사용자의 nickname들을 보낸다. nickname들은 socket_ids property의 키들로 저장이 되었기 때문에, Object.keys(socket_ids)를 이용하여 nickname 리스트를 추출할 수 있다.

마찬가지로, 대화명이 변경되었을 때에도 registerUser 함수를 이용하여 전체 사용자의 리스트를 업데이트 하여 클라이언트에게 보내준다.

socket.on('changename',function(data){

        registerUser(socket,data.nickname);

    });

만약에 해당 클라이언트가 브라우져를 닫았을 경우에는 대화방을 떠난 것으로 간주하여, ‘disconnect’이벤트에 대해서,

socket.on('disconnect',function(data){

        socket.get('nickname',function(err,nickname){

            if(nickname != undefined){

                delete socket_ids[nickname];

            

현재 socket nickname을 읽어서 socket_ids에서 해당 nickname에 해당 하는 데이타를 삭제한 후에,

io.sockets.emit('userlist',{users:Object.keys(socket_ids)});

를 이용하여, 업데이트된 사용자 목록을 다시 클라이언트들이 업데이트 하도록 이벤트를 보낸다.

마지막으로 클라이언트로 부터 대화메세지를 다른 사용자에게 대화 메세지를 보내는 “send_msg” 이벤트가 들어왔을때

socket.on('send_msg',function(data){

        socket.get('nickname',function(err,nickname){

 

            data.msg = nickname + ' : '+data.msg;

에서 처리하는데, 현재 클라이언트의 nickname socket.get을 이용해서 읽어와서 보낼 메세지문자열을 대화명”+”대화내용으로 만들어서 저장한다.

만약에 전체 메세지인 경우 data.to ‘ALL’로 들어오는데, 이경우 broadcast를 하고

if(data.to =='ALL') socket.broadcast.emit('broadcast_msg',data);

특정 nickname data.to에 들어오는 경우 귓속말로 간주하여, nickname을 이용해서 socket_ids로 부터 해당 nickname을 사용하는 클라이언트의 socket.id를 가져온후

socket_id = socket_ids[data.to];

                if(socket_id != undefined){

다음으로, 해당 socket_id로 메세지를 보낸다.

io.sockets.socket(socket_id).emit('broadcast_msg',data);

다음은 위의 서버를 사용하기 위한 HTML 클라이언트 폼이다.

index.html

<html>

<head>

 

    <title></title>

    <script src="/socket.io/socket.io.js"></script>

    <script src="//code.jquery.com/jquery-1.11.0.min.js"></script>

 

</head>

<body>

<b>Send message</b><p>

    Name <input type="text" id="nickname" /> <input type="button" id="changename" value="Change name"/><br>

    To

    <select id="to">

        <option value="ALL">ALL</option>

    </select>

    Message  <input type="text" id="msgbox"/>

    <br>

    <span id="msgs"></span>

 

    <script type="text/javascript">

        var socket = io.connect('http://localhost');

        $('#changename').click(function(){

            socket.emit('changename',{nickname:$('#nickname').val()});

        });

        $("#msgbox").keyup(function(event) {

            if (event.which == 13) {

                socket.emit('send_msg',{to:$('#to').val(),msg:$('#msgbox').val()});

                $('#msgbox').val('');

            }

        });

        socket.on('new',function(data){

            console.log(data.nickname);

            $('#nickname').val(data.nickname);

        });

 

        // 새로운 사용자가 들어오거나, 사용자가 이름을 바꿨을때 "To" 리스트를 변경함

        socket.on('userlist',function(data){

            var users = data.users;

            console.log(users);

            console.log(data.users.length);

            $('#to').empty().append('<option value="ALL">ALL</option>');

            for(var i=0;i<data.users.length;i++){

                $('#to').append('<option value="'+users[i]+'">'+users[i]+"</option>");

            }

        });

 

        socket.on('broadcast_msg',function(data){

            console.log(data.msg);

            $('#msgs').append(data.msg+'<BR>');

        });

    </script>

</body>

</html>


앞에서 작성한 채팅 프로그램과 크게 다를바는 없으나, new 이벤트와 userlist 이벤트 핸들러가 추가되었다.

‘new’ 이벤트는 채팅방에 들어왔을때, 서버로 부터 대화명과 함께 보내지며, 클라이언트에서는 이 대화명을 받아서, “대화명 창부분에 세팅 한다.

socket.on('new',function(data){

            console.log(data.nickname);

            $('#nickname').val(data.nickname);

        });

‘userlist’ 이벤트는 현재 대화방에 있는 사용자들의 nickname을 모두 받은 후에, 대화 상대를 지정하는 HTML <select> box 부분에 대화명들을 넣어준다.

socket.on('userlist',function(data){

            var users = data.users;

            console.log(users);

            console.log(data.users.length);

            $('#to').empty().append('<option value="ALL">ALL</option>');

            for(var i=0;i<data.users.length;i++){

                $('#to').append('<option value="'+users[i]+'">'+users[i]+"</option>");

            }

        });

다음은 위의 예제를 실행한 화면이다. 아래와 같이 TO 부분에서 사용자를 지정하고 메세지를 보내면 특정 사용자에게만 메세지가 전달됨을 확인할 수 있다.



다음에는 대화방의 기능을 추가하여, socket.io의 room 개념에 대해서 알아보도록 한다.

 

빠르게 훝어보는 node.js

#9 - Socket.IO (1/4)

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


웹의 발전과 함께, 클라이언트의 요청에 대해서 응답만을 하는 단방향성이 아닌 양방향성의 사이트가 유행하게 되었는데, Socket.IO 자바스크립트 모듈로, 양방향 통신이 가능한 웹사이트를 구축하기 위해서 HTTP 클라이언트로 푸쉬 메시지를 보내줄 있는 모듈이다. 넓은 브라우져 지원성과 사용의 편의성 때문에 널리 사용되고 있고, node.js 인기 있어 지는 이유 중의 하나는 socket.io 때문이 아닐까 한다.

배경

Socket.io를 설명하기 전에, 웹에서의 푸쉬 개념에 대해서 이해할 필요가 있다. 웹은 기반적으로 클라이언트에서 서버로 가는 단방향성이지만, 채팅과 같은 실시간 양방향 애플리케이나 쪽지와 같이 서버에서 클라이언트로 알림을 보내줘야 하는 요구 사항이 생겼다. 그래서 여러가지 기법이 생겨났는데, 자바스크립트 기반의 AJAX가 유행하면서 몇가지 기법이 생겨났는데, 그 내용을 살펴보면 다음과 같다.



웹 푸쉬 방식 비교 : Polling vs Long Polling vs Streaming

출처 : https://blogs.oracle.com/theaquarium/entry/slideshow_example_using_comet_dojo


Polling

가장 기본적인 기법으로, 클라이언트가 서버에 주기적으로 폴링(request를 보내는 기법)이다. 주기적으로 클라이언트가 자기가 처리해야할 이벤트가 있는지 없는지를 체크하는 것이다.

서버가 폴링 요청이 들어올때 마다 이를 처리해야 하고, 다음 폴링이 이루어지기 전까지는 어떤 이벤트가 오는지를 모르기 때문에, 결정적으로 실시간성이 보장이안된다.

예를 들어 폴링 주기가 10분이라고 할때 폴링 이후에 바로 이벤트가 들어왔을때, 다음 폴링주기 (10)을 기다려야 된다. 그리고 폴링 주기가 짧을 수 록 서버가 받는 부하가 크다, 예를 클라이언트가 작업할 내용이 있는지 확인하려면, DB드에 작업 내용을 저장해놓고, 폴링 때마다 체크해야 하는데, 이때 매번 DB 쿼리를 해야 한다면, 폴링 때마다 서버가 DB를 쿼리해야하기 때문에, 받아야 하는 트렌젝션이 매우 많고 서버의 부담이 기하 급수적으로 늘어난다. 따라서, 짧은 폴링 주기는 서버에 많은 부하를 주기 때문에, 적절하지 않으며 클라이언트로 보내는 푸쉬 메시지의 실시간성이 필요하지 않은 경우에 적절하며, 서버의 부하가 상대적으로 적고(폴링 주기가 길 경우) 기존의 웹백엔드 인프라 (Tomcat과 같은 미들웨어)를 그대로 활용할 수 있는 장점을 가지고 있다.


Long Polling

Long Polling Polling과 비슷하나 즉시성을 갖는다. 방식은 클라이언트가 HTTP request를 보내고, 바로 request를 닫는 것이 아니라, 일정 시간 동안(오랫동안) 열어 놓고 있다가 서버에서 클라이언트로 보내는 메시지가 있으면 메시지를 HTTP response로 실어 보내고, 해당 Connection을 끊는다. 만약에 일정 시간동안 보낼 메시지가 없으면 HTTP 연결을 끊는다.

응답 메시지를 받건 안받건, 끊어진 연결은 다시 연결한다. 기본적으로 클라이언트가 연결을 해서 응답을 요청하는 Polling 형태이고, 응답이 오는지 기다리는 기간이 길기 때문에 이를 Long Polling 이라고 한다.

Long Polling의 경우 서버에 클라이언트들이 거의 항상 연결되어 있는 형태이기 때문에, 동시  서버의 동시에 연결할 수 있는 서버가 지원할 수 있는 동시 연결(Connection)수에 따라 결정된다.

예를 들어 Tomcat과 같은 WAS의 경우에는 HTTP 연결이 열려 있는 경우에는 1개의 Thread가 그 요청을 처리하기 위해서 사용되기 때문에, Tomcat Thread 100개인 경우, 1 Tomcat당 처리할 수 있는 Long Polling 가능한 클라이언트 수는 100개로 한정이 된다. (기존의 HTTP 요청을 처리하는 인프라로 핸들링하기 어렵다.)

서버로부터 푸쉬 메시지를 받으면 재 연결을 해야 하기 때문에, 클라이언트로 푸쉬하는 내용이 적을 경우에 유리하며 실시간 채팅과 같이 푸쉬해야 하는 메시지가 많은 경우에는 적절하지 않다. (채팅 메시지가 하나 왔다갔다 할 때 마다 재 연결을 해야 한다.)


Streaming

마지막으로 Streaming 기법인데, 이 기법은 일반적인 TCP Connection 처럼, 클라이언트가 서버로 연결을 맺은 후에, 그 연결을 통해서 서버가 이벤트를 보내는 방식이다. Long Polling이 이벤트를 받을 때마다 연결을 끊고 재 연결을 한가면, Streaming 방식은 한번 연결되면 계속해서 그 연결을 통해서 이벤트 메시지를 보내는 방식으로 재연결에 대한 부하가 없다.


WebSocket

이러한 푸쉬 로직을 AJAX 자바스크립트로 구현하다가 구현 방식이 브라우져들 마다 각기 상이하기 때문에 나온 표준이 WebSocket이라는 표준이다. http:// 대신 ws:// 로 시작하며 Streaming과 유사한 방식으로 푸쉬를 지원한다.

그러나 문제는 이 WebSocket 기술이 근래에 나왔기 때문에, 예전 브라우져는 지원하지 않는다는 것이다.



<그림: 웹소켓 지원 브라우져 현황>

출처 : http://caniuse.com

Socket.IO

Socket.IO 클라이언트로의 푸쉬를 지원하는 모듈인데, WebSocket 한계를 뛰어 넘어주는 모듈이다. 개발자는 Socket.IO 개발을 하고 클라이언트로 푸쉬 메시지를 보내기만 하면, WebSocket 지원하지 않는 브라우져의 경우, 브라우져 모델과 버전에 따라서 AJAX Long Polling, MultiPart Streaming, Iframe 이용한 푸쉬, JSONP Polling, Flash Socket 다양한 방법으로 내부적으로 푸쉬 메시지를 보내준다.

WebSocket 지원하지 않는 어느 브라우져라도 푸쉬 메시지를 일관된 모듈로 보낼 있다.

간단한 예제를 살펴보자


간단한 채팅 프로그램

간단한 채팅 프로그램을 하나 살펴보자. 아래 프로그램은 브라우져에 접속하면 메시지를 입력 받을 있는 Input Box 띄워주고, 여기에 메시지를 입력하면 현재 연결되어 있는 모든 웹브라우져에 메시지를 보내주는 프로그램이다.

먼저 서버쪽 코드를 보자

/**

 * Module dependencies.

 */

 

var express = require('express');

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

var http = require('http');

var path = require('path');

 

var app = express();

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

 

var httpServer =http.createServer(app).listen(8080, function(req,res){

  console.log('Socket IO server has been started');

});

// upgrade http server to socket.io server

var io = require('socket.io').listen(httpServer);

 

io.sockets.on('connection',function(socket){

   socket.emit('toclient',{msg:'Welcome !'});

   socket.on('fromclient',function(data){

       socket.broadcast.emit('toclient',data); // 자신을 제외하고 다른 클라이언트에게 보냄

       socket.emit('toclient',data); // 해당 클라이언트에게만 보냄. 다른 클라이언트에 보낼려면?

       console.log('Message from client :'+data.msg);

   })

});

 

<코드. App.js>

Express에서 Socket IO 사용한 예제인데, 기존에 Express에서 사용한것과 같은 방식으로 httpServer 생성한다. 다음에, httpServer socketIO 지원하는 서버로 다음과 같이 업그레이드를 한다.

var io = require('socket.io').listen(httpServer);

다음으로, 클라이언트가 socket.io 채널로 접속이 되었을때에 대한 이벤트를 정의한다.

io.sockets.on('connection',function(socket){

같이 클라이언트가 접속이 되면, callback 수행하는데, 이때, 연결된 클라이언트의 socket 객체를 같이 넘긴다. socket 객체를 받아서, 코드에서는 연결된 클라이언트에게 “Welcome !”이라는 메시지를 보냈다.

socket.emit('toclient',{msg:'Welcome !'});

일반적인 이벤트 처리 방식과 같게, 해당 클라이언트 소켓에 emit 메서드를 이용하여, 이벤트를 전송하면 된다. 여기서는 “toclient”라는 이벤트 명으로 msg라는 키를 갖고, value ‘Welcome !’ 이라는 값을 가지는 메시지를 전송하였다.

다음으로, 클라이언트로부터 오는 메시지를 처리하는 루틴인데, 채팅창에서 글을 쓰고 엔터를 누르면 서버로 “fromclient” 라는 이벤트를 보내도록 작성해놓았다. 그러면 서버쪽에서는 다음과 같이 socket.on(‘fromclient’ 라는 메서드를 이용하여 해당 이벤트에 따른 처리를 한다. 이때 들어오는 데이터는 채팅 문자열이 {msg:”문자열”} 형식으로 data라는 변수를 통해서 아래와 같이 들어오는데,

socket.on('fromclient',function(data){

채팅에서 이메세지를 다른 클라이언트들과 자신에게 다시 보낸다.

       socket.broadcast.emit('toclient',data);

       socket.emit('toclient',data);

socket.broadcast.emit 자신을 제외한 다른 모든 클라이언트에게 이벤트를 보내는 메서드이고, socket.emit 자신의 클라이언트()에게 이벤트를 보내는 메서드이다.

그러면 이제 클라이언트()쪽의 코드를 보자

<html>

<head>

 

    <title></title>

    <script src="/socket.io/socket.io.js"></script>

    <script src="//code.jquery.com/jquery-1.11.0.min.js"></script>

 

</head>

<body>

    <b>Send message</b><p>

    Message  <input type="text" id="msgbox"/>

    <br>

    <span id="msgs"></span>

 

    <script type="text/javascript">

        var socket = io.connect('http://localhost');

        $("#msgbox").keyup(function(event) {

            if (event.which == 13) {

                socket.emit('fromclient',{msg:$('#msgbox').val()});

                $('#msgbox').val('');

            }

        });

        socket.on('toclient',function(data){

            console.log(data.msg);

            $('#msgs').append(data.msg+'<BR>');

        });

    </script>

</body>

</html>

<코드. index.html>

먼저 socket.io 사용하기 위해서 script src 아래와 같이 정의하고

<script src="/socket.io/socket.io.js"></script>

다음으로, 자바 스크립트가 실행되면, socket.io 서버로 연결을 한다.

var socket = io.connect('http://localhost');

그리고, input box에서 엔터를 누르면, input box 메시지를 읽어서, ‘fromclient’라는 이벤트를 서버에 전송한다.

socket.emit('fromclient',{msg:$('#msgbox').val()});

그리고 반대로, 서버로부터, ‘toclient’라는 이벤트가 들어오면, 들어온 문자열을 msgs라는 id 갖는 <span> 영역에 append 한다.

socket.on('toclient',function(data){

            console.log(data.msg);

            $('#msgs').append(data.msg+'<BR>');

클라이언트와 서버쪽 코드가 완성되었으면, 이를 배포하고 node.js 실행해서 테스트를 해보자.



<그림. socket.io 이용한 채팅 프로그램 화면>

다음에는 Socket.IO API들에 대한 소개와 1:1 귓속말, 그리고 그룹의 개념을 가지는 채팅방 예제에 대해서 설명하도록 한다.


빠르게 훝어보는 node.js

#5 - Express 2/2

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

File upload download 처리

Express는 파일 업로드 기능을 제공한다. Express 의 경우, 파일을 tmp directory에 업로드한후, 업로드가 끝나면 이벤트를 주는 형태이다. 그래서, 파일 업로드가 끝나면 파일 저장 디렉토리로 옮겨 줘야 한다. 그러면 간단하게 코드를 살펴보자.

express에서 업로드되는 file stream multipart 형태로 업로드가 된다. multi part request stream을 인식하려면, express세팅에 bodyParser 미들웨어를 사용함을 명시해줘야 한다.

var app = express();

app.use(express.bodyParser());

http://expressjs.com/3x/api.html#req.files

다른 미들웨어도 이 bodyParser() 미들웨어를 사용하기 때문에, 다른 미들웨어 선언전에 앞쪽에 선언을 해줘야 한다.

다음으로 file 업로드를 해줄 HTML 파일을 정의하자

<form action="/upload" method="post" enctype="multipart/form-data">

    <input type="file" name="myfile" />

    <input type="submit" name="upload" />

</form>

HTTP Post 형태로 multipart 형태로 데이터를 보내며, 파일은 “myfile”이라는 폼 이름으로 전송된다. 이를 받는 코드는 아래와 같다.

var fs = require('fs');

 

exports.upload = function(req, res){

    fs.readFile(req.files.myfile.path,function(error,data){

        var destination = __dirname + '\\..\\uploaded\\'+ req.files.myfile.name;

        fs.writeFile(destination,data,function(error){

            if(error){

                console.log(error);

                throw error;

            }else{

                res.redirect('back');

            }

        });

    });

 

};

파일은 req.files.{폼이름}.path에 업로드 된다. 위의 예제는 파일 컴포넌트의 폼명을 “myfile”로 했기 때문에 파일의 경로는 req.files.myfile.path가 되낟. 다음 fs.readFile을 이용해서 업로드된 파일이 tmp 디렉토리에 모두 업로드가 되면 destination 디렉토리로 복사해주는 예제이다.

tmp 디렉토리의 경우 bodyParser 미들웨어 적용시 적용할 수 있다.

조금 더 효율적인 코드를 구성해보면, 파일을 tmp 디렉토리에 써지지는 것을 바로 읽어서 write destination에 쓸 수 있도록 Stream을 이용할 수 있다. tmp에서 읽어서 destination 디렉토리에 쓰는 것은 위의 예제와 똑같지만, 나중에 설명하겠지만 Stream을 사용하면, 파일을 읽을 때, 쓸 수 있는 만큼만 (버퍼크기만큼만) 읽은 후 쓰기 때문에, 훨씬 효율적인 IO를 할 수 있다.

exports.uploadstream = function(req, res){

    var destination = __dirname + '\\..\\uploaded\\'+ req.files.myfile.name;

    var ws =  fs.createWriteStream(destination);

    fs.createReadStream(req.files.myfile.path).pipe(ws);

    res.redirect('back');

};

아쉬운 점은 Express의 특성상 바로 destination 디렉토리에 write하는 것은 안되고, tmp 디렉토리를 거쳐서 write해야 한다.


JSON REST API

Express JSON 기반의 REST API 구현도 지원하는데, Spring/Java를 알고 있는 개발자라면, 아주 짜증이 날(?) 정도로 express를 이용한 REST API 구현은 매우 간단하다. 설명은 생략하고 먼저 코드부터 보자

app.use(express.json());

app.post('/rest',function(request,response){

    request.accepts('application/json');

 

    // input message handling

    json = request.body;

    console.log('name is :'+json.name);

    console.log('address is :'+json.address);

 

    // output message

    response.json({result:'success'});

 

});

위의 코드는

{

   "name":"Terry",

   "address":"seoul"

}

와 같은 JSON 메시지를 받은 후에, 내용을 파싱하고, { ‘result’:’success’} 라는 리턴을 보내는 코드이다. 먼저 exress.json 미들웨어를 적용하고, request.accept application/json 타입으로 해서 JSON request를 받음을 명시한다.

다음으로는 request.body.{JSON 필드명} 을 사용하면 된다. 위의 예의 경우 JSON 필드명이 name address이기 때문에 그 값에 대한 경로는 body.name body.address가 된다.

Response를 보낼 때에는 위의 예제와 같이 response.json({key:value,…}) 형태로 지정하면 된다. 만약에 HTTP response code를 보내고 싶으면 response,json(HTTP_CODE,{key:value…}) 형태로 지정한다.

예를 들어 response.json(500,{error:’error message’});  형태로 지정할 수 있다.

또한 response.jsonp 메서드를 이용해서 JSONP 를 지원하는데, JSONP는 간단하게 말하면 Cross Site Scripting을 지원할 수 있는 방법이다. 자세한 설명은

http://beebole.com/blog/general/sandbox-your-cross-domain-jsonp-to-improve-mashup-security/

를 참고하기 바란다.


Connect Module pipe line

지금까지 Express의 기능에 대해서 간략하게 살펴보았는데, 에러 처리 방식에 앞서서 Express의 근간이 되는 Connect framework에 대해서 짚고 넘어가고자 한다.

Connect Framework Javascript 를 기반으로 한 웹/서버 개발용 프레임웍이다. Javascript 기반의 서버를 만들기 위해서, 개발되었으며, Ruby Rack Framework을 기반으로 하였다.

Connect에서는 Middleware라는 개념을 사용하는데, reusable한 컴포넌트를 middleware라고 한다. Request, response 파이프라인상에 middle ware를 넣어서 기능을 추가 및 처리 하는 개념인데, Java Servlet Filter Servlet Chaining과 같은 개념과 유사하다고 보면 된다. 아래 그림과 같이 request가 들어와서 서버에서 처리되고 reponse로 나가는 형태라고 할 때,



아래 그림과 같이 처리 과정에 middleware를 추가하여 기능을 처리하도록 할 수 있다.



우리가 지금까지 express를 사용하면서 app.use라고 했던것들이 middleware 모듈을 추가하는 기능이었다.

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

app.use(express.json());

app.use(express.urlencoded());

app.use(express.methodOverride());

app.use(express.cookieParser('your secret here'));

app.use(express.session());

app.use(app.router);

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

 

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

   console.log('custom log :'+req.path) ;

   next();

});

 

app.post('/upload', upload.fileupload);

위의 코드를 분석해 보면



와 같은 순서로 middleware가 적용된 것이다.

static 파일의 경우 위에서부터 순차적으로 logger 모듈부터 적용이 되다가 express.static 모듈에서 적용후에, static file response한 다음에 바로 리턴이 된다.

static 파일이 아닌 경우는 모두 아래 함수를 수행하게 되는데

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

   console.log('custom log :'+req.path) ;

   next();

});

Middleware로 넘어오는 parameter HTTP request,response 뿐만 아니라 next라는 함수 포인터를 넘겨주는데, middleware를 수행한 다음에 다음 middleware를 실행하기 위한 포인터이다. 위의 예에서는 콘솔에서 로그를 출력한 후에, next()를 호출하여 다음 미들웨어를 호출하도록 하였다.

HTTP/POST /upload request의 경우에는 app.post('/upload', upload.fileupload); 미들웨어에 의해서 처리된다.

이렇게 middleware들은 순차에 의한 chaining 개념을 가지고 있기 때문에, middleware use를 이용해서 불러드릴 경우 순서가 매우 중요함을 알 수 있다.


Error Handling

다음으로 Express에서 에러처리에 대해서 알아보자 Express에서 별도의 에러처리를 하지 않으면, 404의 경우 한줄로 없는 페이지라는 메시지가 나오거나 500 에러의 경우 아래와 같이 에러스택이 바로 표시되어 버린다. (보안상의 이유라도 이런식으로 내부 스택이 나오는 것은 좋지 않다.)



그러면 어떻게 에러 처리를 하는지 알아보도록 하자. 먼저 코드를 보면

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

 

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

   // this will make a error

    var err = new Error('custom error');

    err.type = 'my error';

    next(err);

 

});

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

   console.log(err.type);

   console.log(err.stack);

 

   res.format({

       html: function(){

            res.send(500,'internal server error');

        },

       json:function(){

           res.send(500,{code:err.type,description:'error detail'});

       }

    });

 

});

app.use(function(req,res){

    res.send(404,"I cannot find the page");

} );

 

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

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

});

 

먼저 404 에러의 경우 앞에 Connect에서 살펴본 middlewarechaining 개념을 이용하면 된다. 간단하게 다른 middleware에 의해서 처리되지 않은 URL 404로 처리하면 된다. 그래서 middleware를 불러드리는 맨 마지막에 404 에러 처리 로직을 구현하였다.

app.use(function(req,res){

    res.send(404,"I cannot find the page");

} );

 

다음은 500이나 503 같은 에러 처리 방식을 알아보자, 인위적으로 에러를 만들기 위해서 HTTP GET/error 시에 인위적으로 에러를 발생시키는 코드를 구현하였다.

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

   // this will make a error

    var err = new Error('custom error');

    err.type = 'my error';

    next(err);

 

});

 

여기서 주의 할점은 node.js의 일반적인 에러 처리 방식 처럼 throw를 통해서 에러를 던지는 것이 아니라 next()를 통해서 에러 메시지를 다음 middleware로 넘기는 형태를 사용한다.next 호출시 인자에 error가 있을 경우 미리 정의된 error handler를 부르게 된다.error handler는 다른 middleware와는 총 4개 인자를 받으며, 다르게 첫번째 인자가 err로 정의 된다.

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

가 에러 핸들러를 구현한것이며, res.format을 이용하여, 브라우져가 선호하는 포맷 (content/accept에 정의된) 포맷으로 html이나 json으로 메시지를 보내주도록 구현하였다. 예제라서 간단하게 구현했지만, err.type에 에러가 발생할 때 타입을 정해놓으면 error handler에서 이 err.type에 따른 다양한 에러 핸들링 로직을 구현할 수 있고 (예를 들어 Nagios 기반의 모니터링 시스템에 이벤트를 날리거나, IT Admin에게 SMS 메시지를 보내는 것등), 500 error의 경우에는 template을 미리 만들어놓고 잘 디자인된 에러페이지를 출력할 수 있다.

이 밖에도 HTTP Basic Auth를 이용한 인증, 압축 모듈, CSRF (Cross Site Request Forgery) 등을 방어 하는 모듈등 다양한 기능을 지원하는 API 및 모듈이 있다. 자세한 내용은 http://expressjs.com/api.html 을 참고하기 바란다.

빠르게 훝어보는 node.js

#3 - Express 1/2

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

Express

node.js는 여러 종류의 웹 개발 프레임웍을 제공한다.얼마전에 Paypal이 내부 시스템을 대규모로 node.js로 전환하면서 오픈소스화한 KarkenJS Meteo 등 여러가지 프레임웍이 있는데그중에서 가장 많이 사용되는 프레임웍 중하나인 Express에 대해서 설명하고자 한다.

Express는 웹 페이지 개발 및 REST API 개발에 최적화된 프레임웍으로 매우 사용하기가 쉽다.

프로젝트 생성

먼저 express module npm을 이용해서 설치한 다음 express 프로젝트를 생성해보자.

% express --session --ejs --css stylus myapp

명령을 이용하면 아래와 같이 express 프로젝트가 생성되고, 기본 디렉토리 및 파일이 생성된다.



Ÿ   --session HTTP session을 사용하겠다는 것이다.

Ÿ   --ejs는 템플릿 렌더링 엔진으로 ejb를 사용하겠다는 것이다. (자세한 내용은 나중에 template 엔진에서 설명한다.)

Ÿ   그리고 --css CSS 엔진을 stylus로 사용한다는 것이다.

위와 같이 프로젝트가 생성되었으면, 의존성이 있는 모듈을 설치하기 위해서

%cd myapp

%npm install

을 실행해보자. 앞에서 정의한 옵션에 따라서, ejb,stylus 등의 모듈과 기타 필요한 모듈들이 서리되는 것을 확인할 수 있을 것이다.

디렉토리 구조



그러면 생성된 디렉토리 구조를 살펴보도록 하자

Ÿ   ./node_module : express앱에 필요한 module을 저장한다.

Ÿ   ./public : html, image, javascript,css와 같은 정적 파일들을 저장한다.

Ÿ   ./routes : URL 별로 수행되는 로직을 저장한다.

Ÿ   ./views : HTML view 템플릿을 저장한다. (여기서는 ejs로 지정하였기 때문에, ejs 템플릿들이 여기 저장된다.)

Ÿ   app.js : 이 웹프로젝트의 메인 소스 코드

Ÿ   package.json : module package.json 파일로, 의존성 및 npm script 명령어를 정의한다.

예제 코드

다음은 자동으로 생성된 app.js 코드의 전문이다. 주요 부분에 대한 내용을 살펴보도록 하자.

var express = require('express');

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

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

var http = require('http');

var path = require('path');

 

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.favicon());

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

app.use(express.json());

app.use(express.urlencoded());

app.use(express.methodOverride());

app.use(app.router);

app.use(require('stylus').middleware(path.join(__dirname, 'public')));

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

 

// development only

if ('development' == app.get('env')) {

  app.use(express.errorHandler());

}

 

app.get('/', routes.index);

app.get('/users', user.list);

 

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

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

});

먼저 require 부분에서 필요한 module 들을 로드한다.


환경 설정

다음으로, var app = express(); express 객체를 설정한 후에, express 애플리케이션에 대한 환경을 설정한다. 몇몇 주요 내용만 살펴보면,

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

를 이용해서, node.js의 포트를 설정한다. 여기서는 default 3000 포트로 사용하고, 환경변수에 PORT라는 이름으로 포트명을 지정했을 경우에는 그 포트명을 사용하도록 한다.(Linux의 경우 export PORT=80 이런식으로 환경 변수를 지정한다.)

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

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

다음으로, 템플릿 엔진의 종류와 템플릿 파일의 위치를 정의한다 템플릿 파일의 위치를 path.join(__dirname, 'views') 로 정의 하였는데, __dirname은 현재 수행중인 파일의 위치 즉, app.js가 위치한 위치를 의미하며, path.join을 이용하면, ${현재디렉토리}/views 라는 경로로 지정한 것이다.

Ÿ   app.use(require('stylus').middleware(path.join(__dirname, 'public')));

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

css 엔진의 종류를 stylus로 지정하고, 엔진이 렌더링할 static 파일들의 위치를 지정한다. 그리고, express static 파일의 위치를 지정한다. “./public” 디렉토리로 지정

Ÿ   app.use(express.json());

는 들어오는 http request body json 일때도 파싱할 수 있도록 지원한다.

이 밖에도, urlencoded request multipart request(파일 업로드)를 지원하려면 아래 부분을 추가하면 된다

Ÿ   app.use(express.urlencoded());

Ÿ   app.use(express.multipart());

 

이밖에도 다양한 지원 설정들이 있는데, 자세한 사항은 http://expressjs.com/3x/api.html#middleware를 참고하기 바란다.


router 처리

다음으로 특정 URL로 들어오는 http request에 대한 handler (node에서는 router라고 한다.)를 지정한다.

Ÿ   app.get('/', routes.index); : HTTP GET / 에 대해서 /router/routes.js 에 있는 index 함수를 실행하도록 한다.

Ÿ   app.get('/users', user.list); : HTTP GET /users 요청에 대해서 /router/user.js에 있는 list() 함수를 이용하도록 한다.

user.js routes.js 코드 초기 부분에서 require 이용하여 import 처리가 되어 있다.

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

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

 

http server의 기동

마지막으로 위의 설정값을 기반으로 http server를 기동 시킨다.

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

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

});


Router

Router는 특정 URL로 들어오는 HTTP Request를 처리하는 패턴이다. 앞에서 간단하게 살펴본것과 같이 app.{HTTP_METHOD}(“URL”,{Callback_function}); 으로 정의한다.

express에서는 보통 각 URL Method에 맵핑되는 function /routes/ 디렉토리 아래에 모듈로 만들어 놓고 require를 사용해서 불러서 사용하게 된다.

아래 그림을 보면, app.js에서 /routes/index.js (디렉토리 경로만 지정해놓으면 디폴트로 index.js를 부른다.) /route/user.js 모듈을 부른후에

HTTP/GET “/” route 모듈의 index 메서드를 통해서 처리하게 하고,
HTTP/GET “/users”
route 모듈의 list 메서드를 통해서 처리하도록 한다.



 

HTML Parameter Passing

웹서버를 기동하였으면, 그러면 어떻게 HTML에서 parameter를 넘기는지 보자, HTML에서는 HTTP URL의 일부 또는 Query String 또는 HTML body form value로 값을 넘길 수 있다.

각각의 방법을 알아보면

URL Param

Ÿ   app.get(“URI/:{parameter name}”,callbackfunction…);

식으로 정의하면 URL 내의 경로를 변수로 사용할 수 있다. 예를 들어서

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

          console.log(req.params.name);

      });

의 코드는 /tweeter/{경로} 로 들어오는 HTTP GET 요청에 대해서 {경로} 부분을 변수 처리 한다. /tweeter/terry라고 하면, req.params.name으로 해서 “terry”라는 값을 받을 수 있다.

Query Param

다음으로는 HTTP request로 들어오는 Query String의 값을 추출하는 방법인데, request.query.{query string 키 이름} 으로 추출할 수 있다.

예를 들어 HTTP GET /search?q=조대협 으로 요청이 들어왔을때

Ÿ   var q = request.query.q  로 하면 조대협이라는 value를 추출할 수 있다.

Form Param

마지막으로 HTML <form>을 통해서 들어오는 값을 추출하는 방법이다.

다음과 같은 HTML이 있다고 가정할때,

<input name=”username” …/>

form 값을 읽어 올려면

Ÿ   var q = request.body.username  으로 하면 HTML form에서 name“username”으로 정해진 element의 값을 읽어올 수 있다.

Rendering & Template

HTTP response로 응답을 보내는 방법을 rendering이라고 하는데, 간단한 문자열을 경우, response.send(“문자열”); 을 이용해서 보낼 수 도 있다. 또는 response code를 싫어서 보낼때는 response.send(404,”페이지를 찾을 수 없습니다.”); 와 같은 식으로 첫번째 인자에 HTT response code를 실어서 보내는 것도 가능하다.

그러나 복잡한 HTML을 경우 문자열 처리가 어렵기 때문에, Template이라는 것을 사용할 수 있다.

아래는 Express가 지원하는 템플릿중의 하나인 ejs 템플릿으로, JSP,ASP 와 유사한 형태를 갖는다.

먼저 ejs 모듈을 npm을 이용해서 설치 한 후에,



Express 프로젝트 생성시에 다음과 같이 “—ejs”옵션을 줘서, EJB를 템플릿 엔진으로 사용하도록 지정한다.

% express --session --ejs --css stylus myapp

ejs에 대한 module 의존성을 정의하기 위해서 package.json을 정의한다.

{

  "name": "application-name",

  "version": "0.0.1",

  "private": true,

  "scripts": {

    "start": "node app.js"

  },

  "dependencies": {

    "express": "3.4.8",

    "ejs": "*"

  }

다음으로 생성된 app.js 파일에서의 설정 부분을 보면, 템플릿 엔진이 ejs로 지정되어 있는 것을 확인할 수 있다.그리고 ejs 템플릿 파일을 저장할 위치를 __dirname/views 로 지정한 것을 확인할 수 있다.

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

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

 

이제 ejs를 사용할 준비가 되었다. 템플릿을 직접 만들어 보자. 앞에서 지정한것과 같이 “/views”디렉토리에 생성하면 된다. /views/index.ejs 파일을 만들어 보자

<!DOCTYPE html>

<html>

<head>

    <title><%= title %></title>

</head>

<body>

<h1><%= title %></h1>

<p>Welcome to <%= name %></p>

</body>

</html>

일반적인 HTML과 거의 유사하다. Parameter를 사용하고자 할때는 ASP JSP처럼 <%=변수%> 로 사용하면 된다. 마찬가지로, for,while,if등 간단한 스크립트 로직도 작성할 수 있다.

ejs에 대한 스크립트는 http://embeddedjs.com/ 를 참고하면 된다.

템플릿을 만들었으면, router를 정의해서, 이 템플릿을 부르도록 해보자

다음과 같은 코드로 /routes/index.js 파일을 생성한다.

exports.index = function(req, res){

          res.render('index', { title: 'Express',name:'Terry' });

        };

       

이 파일은 이 request에 대해서 rendering을 할 때, index라는 템플릿을 부르고 (앞서 엔진과 view 디렉토리를 지정했기 때문에, __dirname/index.ejs 파일을 부르게 된다.), 이때 인자로 title=”Express”, name=”Terry” 두 변수를 각각의 값으로 넘기게 된다.

이렇게 넘겨진 값은 앞서 정의한 template 파일 내의 <%=변수%> 부분에 의해서 HTML 로 렌더링이 되서 출력되게 된다.



Express ejs이외에도 jadeHogan.js와 같은 다른 template 엔진도 지원한다.

https://github.com/Deathspike/template-benchmark 를 보면, 각 템플릿 엔진의 성능 비교가 나와 있는데, Hogan,ejs,jade 순으로 빠른 것을 볼 수 있다. 100,000 템플릿을 렌더링 했을 시에 Hogan 4257 ms, ejs 5283 ms, jades 13068ms 가 소요됨을 볼 수 있다.

Jade html을 사용하지 않고, 고유의 태그 언어를 이용하여, 템플릿을 정의하는데, 이는 실행시에 자동으로 HTML로 변경이 되게 된다. 아래 그림 참조 (jade 스크립트가 렌더링 후 우측의 HTML처럼 변경이 된다.). Jade 스크립트를 사용하면, HTML 보다 더 구조화 되고 깔끔한 템플릿을 만들 수 있다는 장점이 있지만. HTML Publisher(디자이너)가 직접 Jade를 만들어서 페이지를 만들어 주기가 어려우니 (디자이너가 HTML이외의 jade 스크립트를 다시 배워야 함). 분업이 쉽지 않다.



Jade의 장점은 HTML Layout을 지원한다는 것인데, Layout은 하나의 페이지를 Header,Footer,Left Menu 와 같이 별도의 구역으로 나누어서,개별 템플릿으로 렌더링 할 수 있는 기능이다. EJS의 경우에도 https://github.com/RandomEtc/ejs-locals 확장 모듈을 이용하여 Layout과 유사한 기능을 제공할 수 있다.
ejs에서 layout을 지원하는 모듈들이 있기는 하지만 근래에 들어서 잘 maintenance가 안되고 있으니, 다운로드 횟수나 최종 업데이트 시간등을 확인하고 사용하는 것을 권장한다.

필자의 경우 ejs를 선호하는데, 디자이너로부터 받은 HTML을 작업해서 그대로 템플릿으로 변환하기가 쉽다.

HTTP Header 정보의 처리

HTTP에서 Request/Response Header에 대한 처리 방법은 다음과 같다. 먼저 request에서 Header를 읽는 방법은 간단하다.

var ua = req.headers['user-agent'];

 

와 같이 request 객체에서 headers array에 들어 있는 value를 위와 같이 읽으면 된다.

반대로 response message header 정보를 쓰는 방법은 

response.writeHead({HTTP Response Code},{“key”:”value”});

 

식으로 Http response code와 함께, header 정보를 key,value pair array로 넘겨주면 response message에 같이 실어서 리턴한다. 다음은 사용 예이다.

response.writeHead(200,{‘Content-Type”:’text/html’,’Server’:’terry’ });

 

HTTP Cookie 처리

HTTP에서 사용되는 Cookie에 대한 사용법은 아래 예를 통해서 살펴보자. 아래 예는 Cookie를 쓰고 읽는 부분이다.

App.js 파일 일부이다. 먼저 express cookie를 사용함을 알려줘야 한다.

app.use(express.cookieParser());

 

app.get('/writecookie', routes.writecookie);

app.get('/readcookie', routes.readcookie);

 

다음, request객체에서 cookie 메서드를 이용해서 cookie 값을 쓴다. 이때 인자는 cookie 이름, cookie 값 그리고, 배열로 Cookie에 대한 옵션 (Expire time )을 기술한다.

/routes/index.js 파일

exports.writecookie = function(req, res){

    res.cookie('name','terry',{ expires: new Date(Date.now() + 900000), httpOnly: true });

    res.end();

};

 

exports.readcookie = function(req, res){

    var name = req.cookies.name;

    console.log("name cookie is :"+name);

    res.end();

}

 

아래는 위의 코드를 호출하여, cookie를 읽고 쓴 결과를 console로 출력한 내용이다.



Signed Cookie의 사용

위와 같은 방식을 사용하면, Cookie가 네트워크를 통해서 전송 및 브라우져에 저장될 때 암호화 되지 않은 형태로 전송 및 저장 되기 때문에, 악의 적인 공격에 Cookie값이 노출될 우려가 있다.

아래는 실제로 Cookie read하는 HTTP/GET 프로토콜을 Fiddler를 이용해서 Capture한 케이스이다. 아래 내용을 보면 name이라는 쿠키 값이 암호화 되지 않고 올라오는 것을 확인할 수 있다.


 

이를 방지 하기 위해서 Express에서는 Secure Cookie를 지원한다.

app.use에서 express.cookieParser를 정의할때, 암호화 키를 넣을 수 있다.

app.js에서 아래와 같이 cookieParser안에 쿠키에 대한 암호화 키를 지정한후에

app.use(express.cookieParser('mykey'));

 

쿠키를 쓸때는 쿠키 옵션에 “signed:true”옵션을 주고, 쓰면 암호화 된 형태로 쿠키를 쓸 수 있으며

exports.writecookie = function(req, res){

    res.cookie('name','terry',{ expires: new Date(Date.now() + 900000), httpOnly: true ,signed:true});

    res.end();

};

 

쿠키를 읽을때는 request.Cookies가 아니라 request.signedCookie에서 값을 읽으면 암호화된 쿠키를 읽을 수 있다.

exports.readcookie = function(req, res){

    var name = req.signedCookies.name;

    console.log("name cookie is :"+name);

    res.end();

}


(http://expressjs.com/api.html#req.signedCookies
내용 참조).

쿠키를 쓸 때 Packet을 잡아보면, 아래와 같이 Set-Cookie에서 name 쿠키가 암호화된 형태로 전송되는 것을 확인할 수 있으며



읽을 때도 아래와 같이 암호화된 형태로 쿠키가 전송되는 것을 확인할 수 있다.



HTTP Session 처리

HTTP Session 을 사용하는 방법도 Cookie와 크게 다르지 않다. 다만,HTTP Session의 경우 Signed Cookie를 사용하기 때문에, 아래와 같이 express app.use cookieParser 정의시 secret 키를 반드시 정해주고, HTTP Session을 사용함을 express.session use처리 함으로써 명시적으로 알려줘야 한다.

app.use(express.cookieParser('your secret here'));

app.use(express.session());

 

다음으로 session을 쓸때는 request.session.{key name}={value} 식으로 저장하고

exports.writesession = function(req, res){

  req.session.name='terry';

  console.log('write session = name:'+req.session.name);

  res.end();

};

 

값을 읽을 때는 마찬가지로 request.session.name으로 값을 빼낼 수 있다.

exports.readsession = function(req, res){

    console.log('write session = name:'+req.session.name);

  res.end();

};

 

아래는 브라우져를 연 후에 session read/write/read 순서로 테스트한 결과이다. Session write전에는 session에 값이 없다가 write 후에는 정상적으로 값을 읽을 수 있음을 확인할 수 있다.



클러스터에서의 Session 처리

HTTP Session 사용시에 주의할 점은 여러 개의 node.js 인스턴스를 시용할 시, 특히 클러스터링을 사용할 경우에는 인스턴스간에 Session 정보가 공유가 되지 않는다. 이 경우 앞단에 L4 Reverse Proxy같은 로드 밸런스를 둘 경우, 사용자가 항상 같은 서버로 붙지 않기 때문에 세션 정보가 유실 될 수 있다. (처음에는 1번 서버로 연결되었다가 두번째 request L4 Round Robin 정책에 의해서 2번 서버로 연결되는 케이스)

이런 문제를 해결하기 위해서 각 node.js 인스턴스간의 Session 정보를 공유 스토리지에 저장해놓고 서로 공유할 수 있는데, Redis가 많이 사용된다.

방식은 connect-redis 모듈을 이용하여 redis node와 연결하고, app.use에서 session 설정시에 아래 설정과 같이 RedisStore Session Store로 지정하여, Redis를 통해서 Session 정보를 공유할 수 있다.

var express = require('express');

var RedisStore = require('connect-redis')(express);

var ports = require('./classes/ports.js');

var config = require('./config/config.js');

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

 

var app = express();

app.use(express.cookieParser());

app.use(express.session({

  store: new RedisStore({

    port: config.redisPort,

    host: config.redisHost,

    db: config.redisDatabase,

    pass: config.redisPassword

  }),

  secret: 'Your secret here',

  proxy: true,

  cookie: { secure: true }

}));

 

  자세한 설정은 clustering 부분에서 다시 설명한다.


#1 – node.js의 소개와 내부 구조 http://bcho.tistory.com/881

#2 - 설치와 개발환경 구축 http://bcho.tistory.com/884

#3 - Event,Module,NPM  http://bcho.tistory.com/885

#4 - 웹 개발 프레임웍 Express 1/2 - http://bcho.tistory.com/887


빠르게 훝어보는 node.js

#3 - Event,Module,NPM

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

비동기 이벤트 프로그래밍

기존의 프로그래밍 언어들은 일반적으로 함수를 부르는 형태의 프로그래밍 구조를 가지고 있다. 이를 procedural programming model이라고 하는데코드가 순차적으로 실행되면서 함수를 호출하는 식의 구조를 가지고 있기 때문에 코드를 보면 코드의 수행 순서를 예측할 수 있다.

 

node.js event driven programming 이라는 개념을 가지고 있는데, 이 개념은 특정 이벤트가 발생되면 미리 이벤트에 맵핑된 함수가 실행되는 형태이다. 즉 해당 함수가 언제 호출 되는지를 예측할 수 가 없다.

var callback = function(data){

        console.log("call back has been called "+data);

}

 

$.get('/endpoint',callback);

 

위의 코드에 있는 callback 이라는 함수는 HTTP GET /endpoint request가 발생할 때만 수행된다.

  cf. procedural programming의 경우 함수는 코드상에서 명시적으로 호출을 해줘야 발생하지만, event driven programming은 이벤트에 의해서 함수가 호출 된다.

이러한 이벤트 방식의 유사한 사례는 윈도우즈나, 자바 SWING과 같은 GUI 계통의 프로그래밍에서도 찾아볼 수 있다. Mouse Click이나 Button Click 같은 이벤트에, callback 함수를 Binding 시켜놓는 형태에서 볼 수 있다.

 

다음으로 node.js 특징은 비동기 프로그래밍 방식이 라는 것인데, 앞서 설명한 바와 같이 node.js는 비동기식 IO를 이용한다. IO 요청을 보내놓고, 코드를 blocking 상태에서 기다리는 것이 아니라 다음 코드로 진행한 다음, IO 가 끝났다는 이벤트가 오면, 미리 지정해놓은 함수를 실행하는 형태이다. 이렇게 함수를 호출한후, 작업이 끝난 후에, 호출되도록 정의한 함수를 “callback”함수라고 한다.

 

아래 코드를 보자

var fs = require('fs');

 

var contents = fs.readFile('hello.txt','utf-8',function(err,contents){

        console.log('read 1:'+contents);

});

이 코드는 hello.txt 라는 파일을 읽는 코드인데, 맨 뒤에 function(err.contents)라는 함수를 정의했다. 이 함수는 파일을 다 읽었을때 호출되는 callback 함수이다. fs.readFile을 호출하면, node는 파일이 다 읽을때 까지 이 코드에서 block되어 있는 것이 아니라 다음코드로 진행을 한다음, 파일을 다 읽으면 이벤트를 발생시켜서 여기에 연결된 function(err,contents)를 수행하게 되는 것이다.

 

node.js를 공부하다보면, 가장 큰 진입장벽중의 하나가, javascript node.js의 라이브러리를 새롭게 배우는 것보다, 기존의 procedural programming model에서 이러한 event driven programming의 개념을 익히는 것이 더 어렵다.

 

Event Emitter

그러면 이러한 이벤트를 어떻게 정의하고 처리할까? node.js에서 이벤트를 발생시키고 처리하는 방식은 EventEmitter 객체를 상속 받아서 구현한다.

 

라디오객체를 만들어서, on,off,change channel 이라는 이벤트가 발생했을때 각각 radoTurnOnCallBack,radioChangeChannelCallback, radioTurnOffCallback 함수가 각각 호출 되도록 해보자

 

먼저 Radio 객체를 만들어 보자

Radio = function(){

    events.EventEmitter.call(this); // call super class constructor

};

다음으로 Radio 객체를 EventEmitter로 부터 상속 받도록 하자.

클래스의 상속은 util 모듈의 inherits 메서드를 사용하면 된다.

util.inherits(Radio,events.EventEmitter);

 

다음으로, 호출될 callback 함수를 정의한 다음

// this is listener

var radioTurnOnListener = function(){

        util.debug('Radio turned on!!')

    }

var radioChangeChannelListener = function(channel){

        util.debug('Channel has been changed to '+ channel);

    }

var radioTurnOffListener = function(){

        util.debug('Radio turned off!!')

    }

radio 객체를 만들어서, 이 객체에 각각의 이벤트를 바인딩해보자

이벤트에 대한 바인딩은 emitter객체.on(‘이벤트명’,callback함수); 식으로 정의하면 된다.

radio.on('turnon',radioTurnOnListener);

radio.on('changechannel', radioChangeChannelListener);

radio.on('turnoff', radioTurnOffListener);

또는 event 바인딩시에, 함수명 대신 직접 함수를 다음과 같이도 정의할 수 있다.

radio.on('turnon', function(){

        util.debug('Radio turned on!!')

    });

 

이제 event가 바인딩 된 radio 객체가 생성되었다. 이제 이 객체에 이벤트를 날려보자

radio.emit('turnon');

radio.emit('changechannel');

radio.emit('turnoff');

다음은 실행 결과 이다.



전체 소스코드

var events = require('events');

var util = require('util');

 

// This is object that generate(emit) events

var Radio = function(){

    events.EventEmitter.call(this); // call super class constructor

 

};

util.inherits(Radio,events.EventEmitter);

 

// this is listener

var radioTurnOnListener = function(){

        util.debug('Radio turned on!!')

    }

var radioChangeChannelListener = function(channel){

        util.debug('Channel has been changed to '+ channel);

    }

var radioTurnOffListener = function(){

        util.debug('Radio turned off!!')

    }

 

var radio = new Radio();

 

radio.on('turnon',radioTurnOnListener);

radio.on('changechannel', radioChangeChannelListener);

radio.on('turnoff', radioTurnOffListener);

 

radio.emit('turnon');

radio.emit('changechannel');

radio.emit('turnoff');

 

Event Emitter methods

그러면 EventEmitter method들을 살펴보자..

Ÿ   emitter.addListener(eventname,listener function)

Ÿ   emitter.on(eventname,listener function)

이 메서드들은 eventname에 해당하는 이벤트에 대해서 ‘listener function’ 이름의 함수가 매번 호출 되도록 한다. 이벤트에 함수를 binding 할때는 하나의 이벤트에 여러개의 listener를 바인딩 할 수 있으며, 최대 바인딩 개수는 디폴트 값은 10개이다.

Ÿ  emitter.once(eventname,listener function)

이 메서드들은 eventname에 해당하는 이벤트에 대해서 ‘listener function’ 이름의 함수가 처음 한번만 호출 되도록 한다.

Ÿ  emitter.removeListener(eventname,listener function)

이 메서드는 “eventname”에 바인딩 되어 있는 “listener function” 이름의 함수와의 binding을 제거한다.

Ÿ  emitter.removeAllListener([eventnames])

인자는 배열형으로, 배열내에 들어가 있는 “eventnames”에 각각 바인딩 된 모든 함수에 대한 바인딩을 제거한다.

Ÿ  emitter.setMaxListeners(n)

해당 eventEmitter에 바인딩될 수 있는 이벤트의 수를 조정한다.

Ÿ  emitter.listeners(event)

“event”이름의 이벤트에 바인딩된 모든 callback 함수 이름을 리턴한다.

Ÿ  emitter.emit(eventname,[args])

“eventname”의 이벤트를 생성하고, 이벤트를 생성할 당시 [args]에 정의된 값 들을 이벤트와 함께 전달한다.

Module

모듈은 개념은, 다른 파일에서 모듈을 불러다 쓸 수 있는 일종의 라이브러리 개념이다.

java import되는 다른 클래스나 C에서 #include 되는 라이브러리의 개념을 생각하면 된다.

모듈은 파일 단위로 구현되는데, export를 이용하여, 외부에 노출된다. 마치 java class public method와 같은 개념으로 생각하면 된다. 해당 파일에 있는 함수라도 exports를 하지 않으면, 외부에서 호출할 수 없다. (일종의 java class private과 같은 개념)

Hello.js 파일에 hello라는 함수가 있고, 이를 다른 파일에서 불러쓰고 싶다면, Hello.js 파일에서 다음과 같이 정의한후

var hello = function(){...}

exports = hello;

hello 함수를 사용하고자 하는 파일 (예를 들어 app.js에서) require 를 이용해서 모듈을 불러오고, 호출해서 사용한다.

var hello = require('./Hello');

hello();

require에는 사용하고자 하는 모듈의 파일명을 “.js” 확장자를 제외 하고 서술한다.

모듈에서 exports 될 수 있는 것은 함수와 자바스크립트 객체가 된다. 위의 예는 함수 형태를 이용하여 모듈을 사용하는 경우인데, 만약에 객체형으로 export 하고 싶다면 export하는 파일에서는

exports.hello = function(){...}

export하고 불러 사용하는 쪽에서는

var h = require('./Hello');

h.hello();

형태로 호출한다.

Module의 경로

앞의 예에서는 “./Hello” Hello.js에서 .js를 제거하고 서술하였으며, 앞에 “./”를 이용하여 경로를 서술하였다. 파일의 경로를 아래와 같이 서술할 수 있는데,

var hello = require('Hello');

 

이 경우 node.js는 현재 실행 디렉토리를 먼저 찾고 없으면, 애플리케이션 디렉토리의 하위디렉토리인 /node_modules/ 라는 디렉토리를 찾는다.

이 디렉토리는 node의 모듈을 저장하는 디렉토리이다. 만약에 이 디렉토리에서 찾지 못하면, 하위 디렉토리의 /node_modules/ 디렉토리를 찾게 된다.

예를 들어서 애플리케이션 디렉토리가 /home/terry/myapp 인 경우,

/home/terry/myapp/node_modules를 먼저 찾고 없으면 다음과 같은 순서로 찾게 된다.

Ÿ  /home/terry/node_modules

Ÿ  /home/node_modules

Ÿ  /node_modules

 

 

Module의 종류

Native Module javascript 모듈

node.js는 엔진은 C++로 짜야져 있고, 그 위에서 동작하는 애플리케이션은 javascript로 구현된다. 그래서 모듈도 두 가지 타입을 가지고 있다. C++/C로 된 모듈을 Native 모듈이라고 하고, Javascript로 된 모듈을 javascript 모듈이라고 한다. Javascript 모듈의 경우, 설치시에 파일이 복사되는 수준에서 설치가 되지만, native module의 경우에는 컴파일을 하면서 설치를 한다. (마치 Linux make install 처럼). 그래서 반드시 C/C++ 컴파일러가 설치되어 있어야 한다. Linux에서는 GCC, Windows에서는 Visual Studio Expess(무료)등을 설치하면 된다.

Global Module Local Module

다음으로 Global Module Local Module이라는 개념을 가지고 있는데, Global Module은 시스템내에 설치된 모든 node.js 프로그램들이 참조할 수 있는 전역 모듈이다.

윈도우즈의 경우 디폴트로 ${user_home}/AppData/Roaming/npm/node_module 디렉토리에 설치된다.

또는 환경 변수 NODE_PATH에 그 경로를 다음과 같이 지정할 수 있다.

NODE_PATH=C:\Users\terry\AppData\Roaming\npm\node_module

Local Module의 경우 application 디렉토리의 /node_module 디렉토리에 설치되며, 해당 애플리케이션만 그 모듈을 참조할 수 있다.

기본 모듈과 확장 모듈

node의 모듈에도 node 설치시에 기본적으로 설치되는 모듈과, 추가로 설치해야 하는 확장 모듈이 있다. 기본 모듈은 http 프로토콜 핸들링이나, file system, event, cluster,TLS/SSL와 같은 암호화 등의 모듈이 있고, 확장 설치로는 웹 개발 프레임웍인 express등이 있다. 기본/확장 모듈에 대해서는 https://github.com/joyent/node/wiki/modules를 참고하기 바란다.


NPM

npm node package manager의 약자로, 앞서 설며한 모듈들에 대한 설치 및 의존성을 관리해 주는 도구이다. 마치 Linux rpm이나 Python pip 처럼 설치를 하면, repository에서 해당 모듈을 읽어다가 설치를 해주며, java maven처럼 package.json 이라는 파일에 (pom.xml과 비슷한 역할을 함) module간의 dependency (의존성)에 따라서 의존성이 있는 모듈을 같이 설치한다.

주요 명령어

여러가지 기능들이 있지만, 주요한 명령을 설명한다.

Ÿ      npm list {module} {-g} : 이 명령은 현재 디렉토리 아래에 설치되어 있는 확장 모듈을 리스트 해준다. {module}을 정해주면, 해당 모듈에 대한 리스트를 출력해주고, {-g} 옵션을 추가하면 global에 설치된 모듈 리스트들을 출력해준다.

Ÿ      npm install {-g} : npm 레파지토리 (maven 레파지토리 처럼 외부에 설치되어 있음)로부터, 모듈을 읽어서 로컬에 설치한다. –g 옵션을 적용할 경우 전역 모듈로 설치한다.

Ÿ      npm update {module} {-g} : 설치된 모듈을 최신 버전으로 업데이트 한다.

Ÿ      npm remove {module} {-g} : 설치된 모듈을 삭제한다.

Ÿ      npm info {module} : 해당 모듈의 의존성, 모듈명등 상세 정보를 출력한다.

Ÿ      npm init : 이 명령어를 수행하면 interactive prompt 모드를 통해서 package.json 파일을 만들기 위한 사용자로부터 받아서, package.json 파일을 생성해준다.

package.json 파일

package.json maven pom.xml과 같은 역할을 한다. 모듈에 대한 정보 (버전,제작자,모듈명 등) 기술을 하면서, 모듈에 대한 의존성, repository 경로등을 정의한다.

의존성 관리

아래 코드는 샘플 package.json 파일로, app이라는 모듈 0.0.1 버전에 대해서 서술하였으며, 이 모듈을 사용하기 위해서는 express 모듈 3.x버전과 redis 모듈 (버전에 상관없이 최신)을 필요로 한다. 또한 모듈은 git://xxx URL로부터 읽어서 설치하도록 되어 있다.

{

  "name": "app",

  "version": "0.0.1",

  "dependencies": {

    "express": "3.x",

    "redis": "*"

}

“repository”: {“type”:”git”,”url”,”git://xxxx”}

}

npm을 이용한 스크립트와 테스트 수행

npm make maven 처럼 custom command를 지정하여 명령어를 수행하도록 할 수 있다. 예를 들어 node server start하거나, test를 수행하거나 또는 빌드(?)패키징을 하도록 설정이 가능하다.

“scripts”라는 엘리먼트를 사용하면 되는데, 아래 예제는 npm start를 하면 app.js 애플리케이션으로 node.js를 실행하고, npm test를 하면, mocha 테스트 프레임웍을 수행하여,테스트를 수행하도록 하는 스크립트이다.

:중략

"redis": "*"

}

“scripts”: {“start”:”node app.js”,

             “test”:”mocha”}

}

참고

Npm 메뉴얼을 보면, “config’ 엘리먼트를 정의해놓고, 여기에 환경 변수를 설정할 수 있다.  db 접속 정보나, http listen 포트와 같이 환경에 따라서 변경이 되는 부분은 코드 상에 직접 넣지 않고, package.json 안에 설정해서, 이 파일만 변경을 하면 되도록 한다. 이를 통해서 개발,테스트 환경에 대해서package.json만 다르게 운영하거나, 운영 환경으로 배포시 간략하게 package.json만 변경하도록 한다.

예를 들어서 package.json

{ “config”:{“dbport”:”3306’,”dbuser”:”terry”}, ..} 라고 정해 놓으면

코드내에서 http.createServer(...).listen(process.env.npm_package_config_dbport); 라고 하면, package.json에서 지정한 환경 변수를 가져다 쓸 수 있다고 한다.

https://www.npmjs.org/doc/misc/npm-config.html 문서 참고

근데, 직접해보니 안된다.

 

그래서 사용하는 방법이 별도의 config 파일을 만들고 예를 들어 config.json으로 만들고

파일내에

{ “dbport”:”3306”,

“dbuser”:”terry”}

라고 해놓고

var config = require(“./config.json”); 으로 부르면 바로 json 객체로 나온다.

다음으로 값을 참고하려면

console.log(config.dbport);

console.log(config.dbuser);

식으로 사용하면 된다.

 

버전 Semantics

Module의 버전Semantics를 살펴보자. 보통 3자리로 구성되는데 1.2.3일 경우

Ÿ   1: major version

Ÿ   2: minor version

Ÿ   3:patch level

이다.package.json에서 dependency에 대한 버전을 정의할 수 있는데,

"dependencies" : {

   "mymodule" : "1.8.1

}

1.8.1 버전에 대한 의존성을

"dependencies" : {

   "mymodule" : "~1"

}

이 의미는 : >= 1.0.0 <2.0.0

"dependencies" : {

   "mymodule" : "~1.8”

}

이 의미는 : >=1.8 <2.0.0 까지

을 정하는 것인데, 설명은 했지만, node.js의 경우 한참 개발되고 있는 신생 에코 시스템이기 때문에 모듈 버전간의 변화가 심할 수 있기 때문에 되도록이면 range 방식은 사용하지 않는 것이 바람직하다.

회사 같은 곳에서 HTTP proxy를 사용하는 경우 해결 방법

npm install 인스톨시 회사 내부 네트워크에서 사용할 경우, 회사에서 proxy를 사용하면, npm install proxy 를 타지 않아서 설치가 npm 설치가 제대로 동작하지 않는 경우가 있다. 이런 문제를 해결 하려면,npm 환경 변수 세팅에 http proxy 서버를 지정해주면 되는데, 지정 방법은 다음과 같다.

Ÿ   npm config set proxy http://proxy.company.com:8080

Ÿ   npm config set https-proxy http://proxy.company.com:8080


다음 연재에서는 node.js의 웹 프레임웍인 Express에 대해서 소개하겠다.


#1 – node.js의 소개와 내부 구조 http://bcho.tistory.com/881

#2 - 설치와 개발환경 구축 http://bcho.tistory.com/884

#3 - Event,Module,NPM  http://bcho.tistory.com/885

#4 - 웹 개발 프레임웍 Express 1/2 - http://bcho.tistory.com/887




Node.js vs Vert.x 비교


간단하게 정리해본 Node.JS Vert.x의 장단점 비교,

두 서버 모두 C10K 문제를 해결 하기 위한 Single Thread 기반의 고성능 비동기 서버이다.

C10K 문제는 대용량 (10,000개이상의 동시 커넥션을)처리 하기 위한 문제로 전통적인 Tomcat등의 WAS에서는 이 문제를 해결할 수 없다 들어온 request는 무조건 큐잉이 되었다가 뒷단의 멀티 쓰레드에 의해서 작업이 처리되는데 이 멀티 쓰레드의 수 만큼만 동시 사용자를 처리할 수 있는 개념이다.

반대로 이 두 서버들은 일단 Connection이 연결되면, 연결된 socket들을 물고 있다가, Single Thread Event Loop가 고속으로 돌면서, socket에 들어온 메세지들을 처리하고 빠지는 구조이기 때문에 하나의 Thread임에도 불구하고, 동시에 여러 사용자를 처리할 수 있는 장점을 가지고 있다.

Vert.x Node.js에 영감을 받고 만들어진 서버이기 때문에, 그 특성이 매우 비슷하지만, 장단점이 극명하게 들어난다.

 

Node.js

Vert.x

내부 엔진 기반

Google Chrome V8 자바스크립트 엔진

libuv 기반의 비동기 처리 IO

Netty 기반의 NW IO

Hazel Case 기반 클러스터링

구현언어

C

Java

사용가능 언어

Javascript(애플리케이션), C (네이티브 모듈)

Python,JavaScript,Java,Groovy,Scala

외부 모듈

40,000개 이상

100개이하

클러스터링

한 하드웨어에 여러개 node.js를 띄울 수 있음. Node.js 인스턴스간 상태 Share 불가

한 하드웨어에 여러개의 vertx를 띄울 수 있음. node간의 상태 공유 메세징 가능
(HazelCast
기반)

에코시스템

매우 풍부. 레퍼런스,서적,교육,컨설팅 기관

공식 서적 2 (2권다 100페이지 이하))
컨설팅,교육 업체 없음

성능

열세
(
node 인스턴스당 CPU 코어 1개 이상 사용이 불가함)

우세
(JVM
기반으로 하나의 vert.x인스턴스에 여러개의 verticle 인스턴스를 띄워서 CPU 사용률을 극대화 할 수 있음)

한 마디로 정리하면, Vert.xHazelCast기반의 IMDG (In memory data grid)를 가지고 있어서, 클러스터링 기능이 좋으며, JVM기반으로, 여러 개의 Verticle을 동시에 띄어서 CPU 사용률을 극대화 함에 따라 더 높은 성능을 낼 수 있으며, 여러 프로그래밍 언어를 지원한다. 기술적으로는 Vert.x가 우세적인 면이 있으나, 아직 에코 시스템이 제대로 형성되지 않아서 기술 지원이나 자료를 구하기가 어려워, 기술 습득이나 운영 유지보수에서는 Node.JS가 우세이다.

 



 

비동기 네트워크 서버 프레임웍 Vert.x

조대협

 

* 서문

Vert.x는 NodeJS와 같은 비동기 소켓서버 프레임웍이다.

Vert.x에 대한 이해를 돕기 위해서, Tomcat과 같은 WebApplication Server(이하 WAS)에 대해서 먼저 간단하게 짚고 넘어가자.

 


<그림. Tomcat의 쓰레드 구조>

Tomcat의 경우에는 HTTP request가 들어오면, request가 앞의 request Queue에 쌓이게 된다.

쌓이게된 request들은 Thread Pool에 있는 Thread에게 하나씩 할당되어, request를 처리하고, 작업이 끝나면, request가 들어온 connection으로 response를 보낸후, 작업을 끝낸다. 작업이 끝난 Thread는 다시 Thread Pool로 들어간다.

이런 구조에서, Tomcat이 순간적으로 동시에 처리할 수 있는 Connection의 수는 Thread Pool의 Thread 수만큼이 되는데, 일반적으로 Tomcat은 50~500개의 Thread정도가 적절하다.

즉, Tomcat 서버 하나는 동시에 최대 500여개의 Connection을 처리할 수 있다고 보면 된다.

 

요즘 들어, 서비스의 규모와 용량이 커짐에 따라서 동시에 여러개의 Connection을 처리해야 하는 기능이 필요하게 되었는데, 이러한 WAS로는 수십만,수백만개의 Connection을 동시에 처리하는 것이 불가능하다.

또한, HTTP 뿐만 아니라, TCP와 같은 다른 request를 처리해야 하는 여러가지 Protocol 지원 문제도 있고

단순히 request/response 기반의 HTTP 요청 뿐만 아니라, HTTP long polling/Streaming과 같은 Push성 서비스를 구현하려면, 동시에 유지되어야 하는 Connection수가 많아야 한다. (모든 클라이언트가 Conenction을 물고 있기 때문에)

이러한 요구 사항을 반영하기 위해서 나온 서버들이 nodejs와 같은 비동기 소켓 서버이다.

 

Vert.x는 nodejs의 자바 버전정도로 보면 되는데, 아주 재미있는 것이 구조에서 부터 기능까지 node.js에 비해서 발전된 모습이 매우 많다.

Vert.x는 nodejs와 마찬가지로 single thread model이다.

WAS와 같이 Thread pool을 이용하는 것이 아니라, 하나의 Thread로 모든 작업을 처리한다. Single Thread 모델을 사용할 경우 Thread의 Context Switching 오버헤드를 줄일 수 있어서 성능에 도움이 되고, 또한 Multi threading에서 고민해야 하는 locking 처리나 공유 데이타 처리에 대해서 전혀 걱정할 필요가 없다.

요즘 트렌드인 것 같은데, 요즘 유행하는 고속 서버들 nginx 웹서버나, redis와 같은 IMDB역시 single thread 모델을 사용한다.

Vert.x는 Single Thread로 도는데, 이를 Event Loop(EL)이라고 한다. Vert.x에 일단 클라이언트가 연결되면, EventLoop가 각 연결된 개별 socket들에 대해 event를 검사한후에, event가 있으면 그 function을 수행해준다.



<그림.Vert.x의 Single Thread 구조>

예를 들어서, 클라이언트 A,B,C와 소켓 SA,SB,SC 로 연결이 되어 있다면. Single Thread에서 Event Loop가 돌면서, 첫번째는 SA에 대한 loop를 돌고, 두번째는 SB,세번째는 SC에 대한 루프를 돈다. 루프를 돌다가 소켓에서 메세지가 들어오거나 연결이 끊기거나 기타 이벤트가 있으면, 그 이벤트를 처리한다.



< 그림. Event Loop의 개념>

모든 연결된 소켓에 대해서 Event loop가 도는데, 보통 하나의 Event Loop를 처리하는 Thread에서 10,000~20,000개 정도의 Connection을 처리하는데는 큰 무리가 없다. (백기선氏 감사)

물론, 이벤트 발생 비율이 많고, 이벤트를 처리하는 로직에 소요되는 시간이 높다면 당연히 성능은 떨어진다. 전체 클라이언트로 메세지를 Broadcast 하면, 당연히 성능이 내려가겠지만, 일반적인 P2P 형식의 이벤트 처리에는 큰 무리가 없다.

 

* Vert.x 특징 들여다 보기

그러면 Vert.x의 몇가지 기술적인 특성을 살펴보도록 하자.

 

1. 내부 컴포넌트 구성

Vert.x를 만든 사람들이 대단하다고 느끼는 점은, 다 처음부터 개발한 것이 아니라 대부분의 모듈을 기존의 오픈소스들을 기반으로 해서 개발하였다는 것이다.

고속 네트워크 처리를 위해서는 국내 개발자 이희승씨가 만들어서 더 유명한 Apache Netty를 서버 엔진으로 사용하고 있고, Vert.x 노드간의 통신을 지원하기 위해서, 데이타그리드 솔루션인 HazelCast를 사용하고 있다.

 클러스터링과, 네트워크 고속 처리 부분을 신뢰가 가는 오픈 소스를 기반으로 했으니, 안정성이 좋은 것은 두