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

빠르게 훝어 보는 node.js - #8 mySQL 연동

Terry Cho 2014. 4. 11. 00:03

빠르게 훝어보는 node.js

#8 - MySQL 연동

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


NoSQL이 근래에 유행이기는 하지만, 데이터간의 관계를 표현하고, 트렌젝션에 일관성을 보장하는 RDBMS는 아직까지는 서버쪽에서는 필수적이다. node.js는 대표적인 오픈소스 RDBMS MySQL은 지원한다. 몇가지 MySQL연동 모듈이 있기는 하지만, 여기서는 가장 많이 사용되는 node-mysql 모듈에 대해서 소개하고자 한다.

node-mysql은 오픈소스로 https://github.com/felixge/node-mysql 에 코드가 공개되어 있다.

mysql 모듈을 사용하려면 npm install mysql 명령을 이용하여, node-mysql 모듈을 설치하자.

mysql에 대한 설치와 사용법에 대해서는 워낙 문서들이 많으니 생략하기로 한다.

mysql 연결

간단한 예제 작성을 위해서 express로 프로젝트를 생성한후에,mysql을 어떻게 연결하는지 살펴보도록 하자.

var express = require('express');

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

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

var http = require('http');

var path = require('path');

먼저 아래와 같이 mysql 모듈을 로딩한다.

var mysql = require('mysql');

다음 로딩된 모듈로부터 Connection 객체를 생성한다. 이 때 실제적인 Connection 연결은 이루어지지 않는다.

var connection = mysql.createConnection({

    host    :'localhost',

    port : 3306,

    user : 'terry',

    password : 'asdf1234',

    database:'terry'

});

이렇게 명시적으로 connect 메서드를 이용해서 connection을 연결하거나 또는 첫번째 Query가 실행될 때, 위의 connection 객체의 정보를 이용해서 connection이 생성된다.

connection.connect(function(err) {

    if (err) {

        console.error('mysql connection error');

        console.error(err);

        throw err;

    }

});

 

다음으로, 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.favicon());

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

 

Query 수행

먼저 insert 기능을 구현해보자. 먼저 mysql에 접속하여, 다음 SQL문을 이용해서 userid,name,address 컬럼을 갖는 테이블을 생성하자

CREATE  TABLE `terry`.`users` (

  `userid` VARCHAR(45) NOT NULL ,

  `name` VARCHAR(45) NULL ,

  `address` VARCHAR(45) NULL ,

  PRIMARY KEY (`userid`) );

그리고 Table insert할 값을 받을 HTML 파일을 만들자



다음은 위의 화면을 표현하는 index.html

<html>

<head>

    <title></title>

</head>

<body>

<form name="user" method="post" action="/users">

    <b>Add Users</b><p>

    id <input type="text" name="userid"/>

    <br>

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

    <br>

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

    <br>

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

</form>

</body>

</html>

HTTP/POST로 값을 받아서 index.html에서 입력된 form field로 입력하는 코드를 구현하면 다음과 같다. 간단하게 connection.query에서 sql 문장을 수행하고, 결과를 callback으로 받으면 된다. 인자가 있을 경우에는 {:} pair로 만들어서 query의 두번째 인자로 넘기면 된다. 아래는 insert into users (userid,name,address) values(폼에서 읽어온 userid,폼에서 읽어온 name,폼에서 읽어온 name)으로 SQL을 생성해서 실행해주는 문장이다. 당연히 비동기 호출이기 때문에, response callback 함수에서 res.send(200,xx)으로 보냈다.

// insert

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

    var user = {'userid':req.body.userid,

                'name':req.body.name,

                'address':req.body.address};

    var query = connection.query('insert into users set ?',user,function(err,result){

        if (err) {

            console.error(err);

            throw err;

        }

        console.log(query);

        res.send(200,'success');

    });

});

다음으로 select를 구현해보면 아래와 같이 구현이 된다. 리턴값은 callback의 두번째 인자로 전달되는데, res.json을 이용하면 간단하게 json형태로 변환하여 리턴할 수 있다.

//select all

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

    var query = connection.query('select * from users',function(err,rows){

        console.log(rows);

        res.json(rows);

    });

    console.log(query);

});

위에서 구현한 select를 실행해보면 다음과 같은 결과를 얻을 수 있다.



만약에 where문을 사용하고자 한다면string을 그냥 생성하는 것보다 mysql.escape라는 메서드를 사용하는 것이 좋다. boolean이나 array와 같은 데이터형을 SQL문장으로 적절하게 변환해줄뿐만 아니라 mysql.escape SQL injection attack을 방어해준다.

var query = connection.query('select * from users where userid='+mysql.escape(req.params.userid),function(err,rows){

        console.log(rows);

자아 이제까지 간단하게 node-mysql 모듈을 이용해서 기본적인 SQL 문장을 수행하는 것에 대해서 알아보았다.

Transaction 처리

node-mysql 모듈을 트렌젝션 처리도 지원해주는데, 아래 예제는 users 테이블과 log 테이블에 각각의 데이터를 하나의 트렌젝션으로 묶어서 수행해주는 예제이다.

// insert with transaction

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

    var user = {'userid':req.body.userid,

        'name':req.body.name,

        'address':req.body.address};

 

    connection.beginTransaction(function(err) {

        if (err) {

            throw err;

        }

        connection.query('insert into users set ?', user, function (err, result) {

            if (err) {

                console.error(err);

                connection.rollback(function () {

                    console.error('rollback error');

                    throw err;

                });

            }// if err

            console.log('insert transaction log');

            var log = {'userid': req.body.userid};

            connection.query('insert into log set ?', log, function (err, result) {

                 if (err) {

                     console.error(err);

                     connection.rollback(function () {

                        console.error('rollback error');

                         throw err;

                      });

                  }// if err

                 connection.commit(function (err) {

                    if (err) {

                        console.error(err);

                        connection.rollback(function () {

                               console.error('rollback error');

                               throw err;

                            });

                    }// if err

                    res.send(200, 'success');

 

                 });// commit

                });// insert into log

        });// inset into users

    }); // begin trnsaction

});

먼저 connection.begintransaction을 호출한 후에, transaction이 시작되면 쿼리를 수행하기 위해서, begintrasaction안쪽 부분에서 sql 문장을 수행한다.

insert into users SQL문장을 먼저 수행한후

다음 insert into log SQL 문장을 수행하기 위해서 insert into users Query call back에서 insert into log SQL 문장을 수행하였다.

그 다음으로 transaction commit 처리를 insert into logs callback함수에서 connection.commit을 이용해서 수행하였다. 코드를 보면 알겠지만, transaction begin,insert 두개, commit을 순차 수행하기 위해서 callback을 중첩 사용하였다. 코드도 복잡하고 {} 도 많다. 이렇게 node.js async callback 구조로 인하여, 코드의 복잡도가 증가하는 것을 보통 callback hell(지옥)이라고 하는데, 이를 해결하기 위한 모듈로 Async 와 같은 모듈들이 있다. callback hell에 대해서는 차후에 별도의 글로 따로 소개하도록 하겠다.

Connetion Pooling

node-mysql도 기본적으로 connection pooling을 제공한다. Connection pooling을 사용하는 법은 매우 쉽다.앞의 예제의 경우에는 전체가 하나의 connection만 사용하기 때문에 동시에 많은 요청이 들어올 경우 제대로 처리가 되지 않는다.

Connection을 만드는 부분을 살펴보면 DB Connection을 만드는 방법과 크게 다르지 않다.여기에 몇가지 인자를 추가 지정할 수 있는데, connectionLimit pool에 담을 수 있는 최대의 connection 수이다. 여기서는 20개로 설정하였다. (디폴트는 10)

다음으로 사용한 옵션이waitForConnection이라는 옵션인데, true로 되어 있으면 Pool내에 가용한 Connection이 없을 경우에 Connection이 반납되기를 기다리고, false로 되어 있으면, Connection이 없을 경우 바로 에러를 리턴한다.

var pool = mysql.createPool({

    host    :'localhost',

    port : 3306,

    user : 'terry',

    password : 'asdf1234',

    database:'terry',

    connectionLimit:20,

    waitForConnections:false

});

 

실제로 테스트를 해보면 재미있는 점이, Pool이 생성되도, 실제로 connection이 만들어지지 않는다. Pool.getConnection시에, 유휴 connection이 없을 경우에만 connectionLimit 범위안에서 connection을 생성한다.

아쉬운 점이 자바 엔터프라이즈에서 사용하는 Connection Pool의 경우 min/max 값을 정해서 자동으로 connection pool의 사이즈를 조정한다거나, Connection health 상태를 주기적으로 검사해서 문제가 있는 것은 끊는다거나 등의 기능이 되는데, 거의 기능이 없다. (장애 대응이 쉽지 않을 듯)

Pool을 사용해서 Query를 해보자. 앞에서 구현한 select * from users pool로 구현해주면 query 전에 pool.getConnection으로 싸주고, getConnection callback 함수안에서 두 번째 인자로 넘어온 connection 객체를 이용해서 쿼리를 수행하면 된다.

//select all

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

    pool.getConnection(function(err,connection){

        var query = connection.query('select * from users', function (err, rows) {

            if(err){

                connection.release();

                throw err;

            }

            console.log(rows);

            res.json(rows);

            connection.release();

        });

        console.log(query);

    });

});

여기서 주의할점은 connection을 사용한 후에, 반드시 connection.release 를 이용해서 pool connection을 반납해야 한다.특히 에러가 났을때도 connection을 반납해야 하는 것을 잊지 말아야 한다.이렇게 connection을 반납하지 않아서 유휴 conneciton pool에 남아있지 않는 현상을 connection leak이라고 하는데, connection pool을 사용할 때 상당히 자주 발생하는 장애이기 때문에 반드시 꼼꼼하게 처리하기 바란다. (나중에 leak이 발생하면 찾기도 어렵다.)

PoolCluster

단일 DB를 연결하는 Connection Pool이외에도 여러 개의 데이터 베이스를 연결하는 Conneciton Pool의 개념인 poolCluster를 제공한다. poolCluster는 동시에 여러 개의 ConnectionPool을 포함하는 일종의 집합이다. 여러 개의 mysql node를 묶어서 클러스터 구성을 할 수 있는 환경이 없어서, https://github.com/felixge/node-mysql 에 나와있는 예제를 인용하였다.

// create

var poolCluster = mysql.createPoolCluster();

 

poolCluster.add(config); // anonymous group

poolCluster.add('MASTER', masterConfig);

poolCluster.add('SLAVE1', slave1Config);

poolCluster.add('SLAVE2', slave2Config);

 

// Target Group : ALL(anonymous, MASTER, SLAVE1-2), Selector : round-robin(default)

poolCluster.getConnection(function (err, connection) {});

 

// Target Group : MASTER, Selector : round-robin

poolCluster.getConnection('MASTER', function (err, connection) {});

 

// Target Group : SLAVE1-2, Selector : order

// If can't connect to SLAVE1, return SLAVE2. (remove SLAVE1 in the cluster)

poolCluster.on('remove', function (nodeId) {

  console.log('REMOVED NODE : ' + nodeId); // nodeId = SLAVE1

});

 

poolCluster.getConnection('SLAVE*', 'ORDER', function (err, connection) {});

 

// of namespace : of(pattern, selector)

poolCluster.of('*').getConnection(function (err, connection) {});

 

var pool = poolCluster.of('SLAVE*', 'RANDOM');

pool.getConnection(function (err, connection) {});

pool.getConnection(function (err, connection) {});

 

// destroy

poolCluster.end();

이 코드는 “master- 2개의 slave 복제 구조로 되어 있는 mysql 에 각 DB 3개의 connection pool을 연결하여 pool cluster로 묶은 예제이다. poolCluster에서 connection 을 가지고 오는 것은 poolCluster.getConnection(“풀이름”) 을 사용하면 되는데, 위의 SLAVE*와 같이 ‘*’를 사용할 수 있다이 경우에는 어떤 정책으로 Pool을 가지고 올지를 정해야 하는데, 3 가지 정책을 지원한다.

Ÿ   RR : Connection을 해당 이름의 풀들에서 번갈아 가면서 한번씩 리턴한다.

Ÿ   ORDER : Connection을 무조건 첫번째 풀에서부터 리턴하는데, 장애가 났을 경우에만 다음 풀의 Connection을 순차적으로 리턴한다.  이 경우에는 HA 구성등에 유리하게 사용할 수 있다.

Ÿ   RANDOM : 순서 없이 무작위로 아무 풀에서나 connection을 리턴한다.

이 외에도 살펴보면, Store Procedure 수행이나, Streaming, Pipe와 같은 기능들도 제공한다. 그러나 아직까지 엔터프라이즈 자바에 비해서는 분산 트렌젝션 관리 능력이나 배치 처리등은 부족한 것이 사실이고, 오픈되어 있는 모듈도 아직 성숙도가 기존의 JDBC/MySQL 모듈에 비해서 높지 않다. 예를 들어서, connection이 끊어지지 않게 주기적으로 Keep alive query를 보내는 기능등. 일반적인 B2C 애플리케이션에서는 충분히 사용가능 하지만, 고가용을 요구하거나 데이타 분실이나 은행과 같은 높은 수준의 트렌젝션 보장이 필요한 곳에는 적절하지 않다. (물론 그런곳에는 mysql을 사용하지도 않겠지만)