NHN Business Platform 글로벌플랫폼개발랩 에센 사그노브, 유성덕, 이재익

최근, 이벤트 기반의 Non-Blocking I/O 프로그래밍 모델이라는 점과 JavaScript로 서버 프로그램을 작성할 수 있다는 점에서 Node.js가 많은 주목을 받고 있습니다. Node.js는 소규모 비즈니스뿐만 아니라 LinkedIn, Yahoo!, Microsoft와 같은 큰 규모의 인터넷 기업에서도 사용하고 있습니다. Node.js의 또 다른 큰 장점은 NPM(Node Packaged Modules) 형태로 많은 모듈이 제작되고 있다는 점입니다. 현재 http://npmjs.org 사이트에 20,000개가 넘는 모듈이 등록돼 있고, 2012년 12월 한 달에만 13,000,000회 이상의 다운로드 수치를 보였습니다.

이 글에서는 2012년 10월에 출시한 node-cubrid를 소개합니다. node-cubrid는 Node.js로 CUBRID를 사용할 수 있도록 한 것입니다. NPM으로 제작됐기 때문에 쉽게 설치할 수 있습니다. node-cubrid는 CUBRID에 연결하고, 질의하기 위한 API를 제공합니다. 기본적인 데이터 조작 API뿐 아니라, 입력 값 포맷을 검증하거나 SQL문을 파라미터화하는 API 또한 개발 편의를 위해 제공하고 있습니다.

호환성과 의존성

Node.js와 ADO.NET을 제외한 PHP/PDO, Python, Perl, Ruby, OLEDB, ODBC용 CUBRID 드라이버는 CCI (CUBRID C Interface)에 의존성이 있다. CCI가 Linux와 Windows에서만 사용 가능하므로 이들 라이브러리 또한 Linux와 Windows에서만 사용가능하며 CUBRID 버전에도 종속성을 가지고 있다.

반면 node-cubrid는 CCI에 대한 의존성 없이 순수 JavaScript로만 작성돼 있다. 그래서 Node.js를 실행할 수 있는 모든 환경에서 node-cubrid를 사용할 수 있다는 장점이 있다. 여타의 다른 CUBRID 드라이버와 달리 CUBRID 버전에 맞춰 드라이버 버전을 선택할 필요가 없다.

설치와 사용

node-cubrid는 NPM 형태로 제공하기 때문에 다른 Node.js 모듈과 마찬가지로 <예제 1>처럼 npm 명령어로 node-cubrid를 쉽게 설치할 수 있다.

예제 1 node-cubrid 설치

npm install node-cubrid

<예제 1>처럼 node-cubrid를 설치하면 https://npmjs.org/에 등록된 최신 버전의 node-cubrid가 설치된다.

설치를 마지면 require() 함수를 이용해 node-cubrid 모듈을 불러올 수 있다.

예제 2 node-cubrid 모듈 추가

var CUBRID = require('node-cubrid');

node-cubrid 모듈은 다음과 같은 함수를 제공한다.

  • Helpers: 유틸리티 함수들
  • Result2Array: DB 결과 집합(result set)을 JavaScript 배열(Array)로 변환하는 함수들
  • createDefaultCUBRIDDemodbConnection(): 로컬 demodb에 대한 연결 객체를 반환하는 함수
  • createCUBRIDConnection(): CUBRID에 대한 연결 객체를 반환하는 함수

요청 흐름

node-cubrid로 SQL을 실행할 때 EVENT_QUERY_DATA_AVAILABLE 이벤트와 EVENT_ERROR 이벤트를 처리해야 한다. 또한 서버에서 응답이 왔을 때 호출될 콜백 함수를 등록해야 한다. 이는 일반적인 Node.js 프로그래밍 패턴과 같다.

665b5f44631beaa0b95ded2f6a1199a5.png

그림 1 node-cubrid 요청 흐름

요청에 대한 결과는 쿼리 결과 집합(Query result set)이나 에러 코드다. CUBRID는 요청을 보낸 클라이언트에 대한 ID를 반환하지 않도록 설계돼 있다. 그렇기 때문에 응답이 올 때 어느 요청에 대한 응답인지 알기 위해서는 요청을 하나씩 보내고 응답을 하나씩 받는 방법을 사용해야 한다. 이는 CUBRID뿐 아니라 많은 다른 DBMS에서도 마찬가지다. 여러 질의를 동시에 처리하려면 커넥션 풀(Connection Pool)을 사용해야 한다.

node-cubrid 사용법

데이터베이스 연결

CUBRID 서버에 연결하려면 호스트 이름, CUBRID Broker 포트, 사용자 ID, 암호, DB 이름을 파라미터로 전달해 createCUBRIDConnection() 함수를 호출한다.

예제 3 콜백 방식의 DB연결 예제 코드

var conn = CUBRID.createCUBRIDConnection('localhost', 33000, 'dba', 'password', 'demodb');
 
conn.connect(function (err) {
	if (err) {
		throw err.message;
	} else {
		console.log('connection is established');
		conn.close(function () {
			console.log('connection is closed');
		});
	}
});

<예제 3>에서 콜백 패턴을 이용해 CUBRID에 연결하는 방법을 알 수 있다. connect() 함수에 콜백 함수를 넘겨주면 연결이 완료된 후에 콜백 함수가 호출된다. <예제 4>는 같은 기능을 이벤트 기반으로 작성한 것이다.

예제 4 이벤트 방식의 DB연결 예제 코드

conn.connect();
 
conn.on(conn.EVENT_ERROR, function (err) {
	throw err.message;
});
 
conn.on(conn.EVENT_CONNECTED, function () {
	// connection is established
	conn.close();
});
 
conn.on(conn.EVENT_CONNECTION_CLOSED,
function () {
	// connection is closed
});

이벤트 기반 코딩 방식을 선호한다면, CUBRID.ORG(http://www.cubrid.org/wiki_apis/entry/cubrid-node-js-api-overview)의 관련 문서를 참조하기 바란다.

SQL 실행

연결 후에는 SQL을 실행할 수 있다. node-cubrid에서 SQL과 관련해 제공하는 API는 다음 표와 같다.

표 1 node-cubrid의 SQL 관련 API

함수

설명

query(sql, callback);

쿼리를 실행하고 데이터 레코드를 반환한다.

queryWithParams(sql, arrParamsValues, arrDelimiters, callback);

파라미터가 있는 쿼리(parameterized SQL queries)를 실행하고 데이터 레코드를 반환한다.

execute(sql, callback);

쿼리를 실행하고 데이터 레코드를 반환하지 않는다.

executeWithParams(sql, arrParamsValues, arrDelimiters, callback);

파라미터가 있는 쿼리를 실행하고 데이터 레코드를 반환하지 않는다.

batchExecuteNoQuery(sqls, callback)

배치 모드로 쿼리를 실행한다.

<표 1>에서 모든 API의 첫 번째 파라미터가 SQL임을 알 수 있다. 이름이 query로 시작하는 API는 데이터 레코드를 반환하고, 이름이 execute로 시작하는 API는 레코드를 반환하지 않는다. 따라서 SELECT 쿼리는 query() 함수를 사용하고, INSERT/UPDATE/DELETE 쿼리는 execute() 함수를 사용하면 된다.

파라미터가 있는 SQL 실행

queryWithParams() 함수나 executeWithParams() 함수를 이용하면 파라미터가 있는 쿼리(parameterized SQL queries)를 실행할 수 있다. 이 두 함수는 SQL 파라미터 “?”를 서버와 통신하기 전에 함수 파라미터로 대체한다.

예제 5 SELECT 구문 실행 예제 코드

var code = 15214,
sql = 'SELECT * FROM athlete WHERE code = ?';
 
conn.queryWithParams(sql, [code], [], function (err, result, queryHandle) {
	// check the error first then use the result
});

예제 6 INSERT 구문 실행 예제 코드

var host_year = 2008,
host_nation = 'China',
host_city = 'Beijing',
opening_date = '08-08-2008',
closing_date = '08-24-2008',
sql = 'INSERT INTO olympic (host_year, host_nation, host_city, opening_date, closing_date) VALUES (?, ?, ?, ?, ?)';
conn.executeWithParams(sql, [host_year, host_nation, host_city, opening_date, closing_date], ["", "'", "'", "'", "'"], function (err) {
	// check the error first
});

여러 개의 레코드를 한 번에 VALUES (...), (...), ..., 형식으로 처리하려면 <예제 7>과 같이 helper 함수를 사용해야 한다.

예제 7 helper 함수를 활용한 INSERT 구문 예제

var sql = 'INSERT INTO olympic (host_year, host_nation, host_city, opening_date, closing_date) VALUES ',
	partialSQL = '(?, ?, ?, ?, ?)',
	data = [{...}, {...}, {...}],
	values = [];
	 
	data.forEach(function (r) {
		var valuesSQL = CUBRID.Helpers._sqlFormat(
		partialSQL,
		[r.host_year, r.host_nation, r.host_city, r.opening_date, r.closing_date],
		["", "'", "'", "'", "'"]
		);
		 
		values.push(valuesSQL);
	});
	 
	sql += values.join(',');
	 
	conn.execute(sql, function (err) {
	// check the error first
});

대량의 데이터 가져오기

대량의 데이터를 조회할 때는 한 번에 필요한 데이터를 가져오는 것이 아니라 반복적으로 fetch() 함수를 호출해 데이터를 조금씩 덜어서 가져와야 한다.

예제 8 fetch() 함수를 호출해 대량의 데이터 얻기

var sql = 'SELECT * FROM participant';
conn.query(sql, function (err, result, queryHandle) {
	// assuming no error is returned
	// the following outputs 916
	console.log(CUBRID.Result2Array.TotalRowsCount(result));
	 
	function outputResults (err, result, queryHandle) {
		if (result) {
			// 309 records are in the first results se
			// 315 records are in the second results set
			// 292 records are in the third results set
			console.log(CUBRID.Result2Array
			.RowsArray(result).length);
			// try to fetch more data
			conn.fetch(queryHandle, outputResults);
		} else {
			// no more result, close this query handle
			conn.closeQuery(queryHandle,
			function (err) {
				conn.close(function () {
					console.log('connection closed');
				});
			});
		}
	}
	 
	outputResults(err, result, queryHandle);
});

커넥션 풀 사용하기

node-cubrid는 자체적인 커넥션 풀(Connection Pool) 기능을 제공하고 있지는 않다. 하지만 generic-pool 모듈(https://github.com/coopernurse/node-pool)을 사용해 커넥션 풀을 사용할 수 있다. generic-pool은 npm으로 설치할 수 있다.

예제 9 npm을 이용한 generic-pool 설치

npm install generic-pool

<예제 10>에서 커넥션 풀을 이용하는 방법을 알 수 있다. create() 함수에 CUBRID에 연결하는 코드와 destroy() 함수에 연결을 종료하는 코드를 삽입한다.

예제 10 connection pool 생성 예제 코드

var poolModule = require('generic-pool');
var pool = poolModule.Pool({
	name : 'CUBRID',
	// connection은 최대 10개까지 생성합니다.
	max : 10,
	// 생성된 connection은 30초 동안 유휴 상태(idle)면 destory됩니다.
	idleTimeoutMillis : 30000,
	log : true ,
	create : function(callback) {
		var conn = CUBRID.createCUBRIDConnection('localhost', 33000, 'dba', 'password', 'demodb');
		conn.connect(function (err) {
				callback(err, conn);
			});
		},
		destroy : function(con) {
			conn.close();
		}
});

<예제 11>은 generic-pool 모듈의 acquire() 함수를 이용해 DBMS에 대한 연결 객체를 얻은 후, 해당 객체에 질의를 보내는 예제 코드다. acquire() 함수의 콜백 함수에서 질의를 보내고 query() 함수의 콜백 함수에서 커넥션 풀에 연결을 반환하고 있다.

예제 11 generic-pool의 acquire() 함수 활용 예제 코드

pool.acquire(function(err, conn) {
	if (err) {
		// handle error - this is generally the err from your
		// factory.create function
	} else {
		conn.query("select * from foo", [], function() {
			// return object back to pool
			pool.release(conn);
		});
	}
});

async 모듈과 함께 node-cubrid 사용하기

처음 Node.js를 경험하는 많은 개발자들은 비동기 처리 방식과 수많은 콜백 함수로 인해 혼란을 겪는다. 이런 개발자들을 위해 많은 흐름 제어(flow control) 모듈이 개발됐다. 그 중에서도 async 모듈은 가장 대중적으로 사용되는 모듈이다. node-cubrid에서 제공하는 ActionQueue 모듈을 사용하면 async 모듈의 waterfall과 같은 기능을 사용할 수 있다.

예제 12 ActionQueue 모듈을 활용한 예제 코드

CUBRID.ActionQueue.enqueue(
	[
		function (cb) {
			conn.connect(cb);
		},
		 
		function (cb) {
			conn.getEngineVersion(cb);
		},
		 
		function (engineVersion, cb) {
			console.log('Engine version is: '
			+ engineVersion);
			conn.query('select * from code', cb);
		},
		 
		function (result, queryHandle, cb) {
			console.log('Query result rows count: '
			+ Result2Array.TotalRowsCount(result));
			console.log('Query results:');
			var arr = Result2Array.RowsArray(result);
			for (var k = 0; k < arr.length; k++) {
				console.log(arr[k].toString());
			}
			conn.closeQuery(queryHandle, cb);
			console.log('Query closed.');
		},
		 
		function (cb) {
			conn.close(cb);
			console.log('Connection closed.');
		}
	],
	function (err) {
		if (err == null) {
			console.log('Program closed.');
		} else {
			throw err.message;
		}
	}
);

<예제 13>은 <예제 12>와 같은 내용을 async 모듈을 사용해서 작성한 것이다.

예제 13 async 모듈의 waterfall을 활용한 예제 코드

async.waterfall(
	[
		function (cb) {
			conn.connect(cb);
		},
		 
		function (cb) {
			conn.getEngineVersion(cb);
		},
		 
		function (engineVersion, cb) {
			console.log('Engine version is: '
			+ engineVersion);
			conn.query('select * from code', cb);
		},
		 
		function (result, queryHandle, cb) {
			console.log('Query result rows count: '
			+ Result2Array.TotalRowsCount(result));
			console.log('Query results:');
			var arr = Result2Array.RowsArray(result);
			for (var k = 0; k < arr.length; k++) {
				console.log(arr[k].toString());
			}
			conn.closeQuery(queryHandle, cb);
			console.log('Query closed.');
		},
		 
		function (cb) {
			conn.close(cb);
			console.log('Connection closed.');
		}
	],
	 
	function (err) {
		if (err == null) {
			console.log('Program closed.');
		} else {
			throw err.message;
		}
	}
);

마치며

node-cubrid는 2013년 1월 현재 1.1 버전을 릴리스한 상태다. 다음 버전에서는 개발자 편의성을 위한 기능을 많이 추가하려 한다. 파라미터별로 쿼리에 전달하는 대신 프로퍼티를 가지고 있는 객체를 전달하면 가독성과 개발 편의성이 점 더 높아질 것으로 생각한다. CUBRID 설정을 조회하는 기능과 INSERT/UPDATE/DELTE 구문 실행 후 변경된 레코드 수를 반환하는 기능도 추가할 것이다. ORM(Object Relational Mapping) 개발자를 위해 테이블 스키마 정보를 가져오는 API 또한 제공하려 한다. 또한 다른 CUBRID 드라이버처럼 Connection URL을 사용할 수 있도록 하려 한다. 이 기능을 이용하면 CUBRID Broker 장애 극복(failover)을 위한 대체 호스트 목록을 주거나 쿼리 타임아웃을 설정할 수 있기 때문이다.

참고자료

 

3ae0d2b81ce7bd6c44fd77dba226d555.jpg
NBP 글로벌플래폼개발랩 에센 사그노브
a94b8c2dff66e1a5d562543c299bd6b3.JPG
NBP 글로벌플래폼개발랩 유성덕
오픈소스를 이용하여 대용량 로그 분석 시스템을 개발하였으며, 현재는 WorkFlow 엔진을 이용한 새로운 프로젝트를 진행 중입니다. 2012년에 사랑하는 딸이 생겨서 행복한 삶을 영위하고 있습니다.
31a9898c16d64bb9fd3a2a0043aeb1d3.JPG
NBP 글로벌플래폼개발랩 이재익
글로벌플랫폼개발랩에서 중국개발자들과 로그시스템을 개발하고 있다. 최근 elasticsearch, node.js, iOS 개발에 관심을 가지고 업무에 활용하고 있으며, 여가시간에는 무지함에서 벗어나기위해 독서를 하거나 여행을 하려고 노력 중이다.