nineBatis (9batis)

Java에서 SQL문을 처리 할 때 많이 사용하는 Mybatis처럼,

Node.js에서 SQL을 쉽게 사용하기 위해 간단하게 제작한 라이브러리이다.


여기에서는 이전에 정리한 Node.js 기반의 단순 게시판

9batis를 적용하여

9batis의 사용법을 예제와 문서로 정리하였다.


게시판 예제는 Node.js (express)와 MySQL을 기반으로

단순 CRUD 기능을 구현한 예제이고,

board1.js, board2.js의 2가지 예제로 구성되어 있다.


이 예제에 9batis을 적용하여 3 가지 예제를 추가하였다.

9batis를 이용하여

SQL 문장만 반환 받아서 사용하는 단순 예제(board3.js),

SQL을 실행하고 결과를 반환 받아서 사용하는 기본 예제(board4.js),

중첩 SQL문 실행 등 다소 복잡한 기능을 정리한 예제(board5.js)로 구성하였다.


routes 폴더에 있는 각 컨트롤(board3.js, board4.js, board5.js)들은

각각의 HTML 파일(ejs)을

views 폴더 하위에

컨트롤과 동일한 이름의 폴더(board3, board4, board5)로 가지고 있다.

각 폴더에는 list.ejs, form.ejs, read.ejs 파일이 있다 [그림 왼쪽 참조].


이 샘플 코드는 github에서 받을 수 있다.

github 사용법은 게시판 예제에 정리되어 있으니 참고하면 되고

정리된 내용 중에서 다운 받을 github 주소를

boardJS에서 nineBatisSample로 바꾸어서 따라하면 된다.

https://github.com/gujc71/nineBatisSample


이제부터 nineBatis (9batis)의 사용법에 대해서 정리한다.


Node.js에서는 SQL문을 문자열로 처리하여

컨트롤과 같이 작성한다.

nineBatis (9batis)에서는 SQL문을 별도의 XML 파일로 작성하여 관리한다.

다음 코드(board3.xml)와 같이

select, insert 등의 태그를 사용하여 SQL 문을 작성하면 된다.

다음 코드는 게시판 예제(board1.js)에서 SQL 문만 따로 추출하여

9batis 문법에 맞추어 XML로 정리한 것이다.

board3.xml

XML 파일은 한개 이상을 사용할 수 있고

별도의 폴더에 (예제는 routes/sql) 모아서 사용해야 한다.


태그에 사용되는 id (selectBoardList, selectBoardOne 등)는

전체 XML 파일내에서 고유한 값이어야 한다.

중복된 id를 지정하면 Node.js 시작시 오류 메시지를 출력하고

Node.js 실행을 중지한다.


XML 파일에서 제공되는 기본 기능(명령어)은 4 가지이다.

숫자 변수, 문자 변수, include, if문이 지원된다.

이것은 Mybatis와 비슷하게 구현했는데

사용한 라이브러리의 제약(xml2js)등의 문제로 아직은 사용법이 다소 부족하다.


위 코드에서 사용한 것처럼

숫자 변수는 ${변수} [라인 4],

문자 변수는 #{변수} 로 사용한다 [라인 21, 27].

MyBatis에서는 #을 PrepareStatement로

$를 Statement로 정의하여 구분하지만,

여기서는 쉽게 정의하기 위해 문자와 숫자 변수로 정의하였다.


include, if문은 XML로 작성해야 하는데

xml2js 파싱시 처리가 되지 않아

{include ref='sql 태그 id'}로 사용한다 [라인 16, 22, 32].

include문은 위 SQL 문처럼 (includeWhere)

중복된 코드(sql 태그)를 별도로 지정하고 [라인 4],

이 것을 사용할 SQL에서 가져오고자 할 때 사용한다 [라인 16, 22, 32].

이때 ID가 일치해야 한다.


if 문은 다음 코드처럼

@{if test="조건식"}실행할 내용{/if}으로 사용한다.

정규식을 이용한 파서에 문제가 있어서 IF문에만 @를 사용했는데,

일관성을 위해 include도 @{include}로 처리할지 고려 중이다.


if 문은 board5.xml 파일에 다음과 같이 작성되어 있다.

board5.xml

조건식은

숫자 변수를 사용할 경우에는 $변수 > 10과 같이 사용하고

문자 변수를 사용할 경우에는 $변수 === "test"와 같이 사용한다.

Mybatis와 마찬가지로 else 문이 없다.

따라서 예제와 같이 두개의 if문을 사용해야 한다.


데이터를 정해진 개수(cnt) 만큼 가져올 때와 [라인 6]

정해진 개수가 없거나($cnt === undefined), 0 이면 [라인 11]

모든 데이터를 가져오도록 조건을 사용하지 않았다.


명령어

설명

예제

 #{변수명}

 문자열 변수 (PrepareStatement)

 #{cnt}

 ${변수명}

 숫자형 변수 (Statement)

 ${cnt}

 {include}

 공통 SQL 문 가져오기

 {include ref='sql 태그 id'}

 @{if test="조건식"}

 비교연산자

 @{if test="cnt>1"}실행할 내용{/if}


이렇게 작성한 SQL문을

nineBatis(9batis)를 이용하여 프로그램(js)에서 호출해서 사용한다.


9batis는 다음과 같이 선언하여 사용한다.

var batis9 = require("./nineBatis");
batis9.loadQuery(__dirname +'/sql', true);

9batis 파일(nineBatis.js)은

아직 npm에 등록하지 않아서 nineBatis.js 파일을 복사해서 사용해야 한다.

샘플 프로젝트의 routes 폴더에 다른 코드들과 같이 있다.

(등록은 어느 정도 쓸만하다 싶을 때 할 예정)


require로 nineBatis.js 파일을 가져오기 한 후,

9batis의 loadQuery() 함수로 xml 파일들을 읽어들인다.

파라미터로 xml파일들이 있는 폴더(sql)를 지정하고,

디버그 여부를 지정하면 된다.

디버그를 true로 지정하면 console에 현재 실행하는 SQL문이 출력된다.


Node.js를 Eclipse에서 실행한 경우에는 하단 console 탭에 SQL ID와 SQL문이 출력된다.

Node.js를 cmd 창에서 실행한 경우에는 cmd 창에 SQL ID와 SQL문이 출력된다.

SQL ID와 SQL문이 출력이 중요한 것이

ID로 문제가 되는 SQL을 쉽게 찾을 수 있고

출력된 SQL 문을 복사해서

전용 Client (WorkBench, HeidiSQL등)에서 붙여넣고 실행하면

쉽게 SQL 오류를 찾을 수 있기 때문이다.


loadQuery()는 한 프로젝트에서 한번만 실행해야 하고

커넥션 풀링처럼

9batis을 app.js에서 실행한 후

변수 batis9 을 글로벌로 사용하는 것이 좋다.


먼저, 9batis를 소극적으로 사용한 예제이다.

board3.js

예제로, 모든 데이터를 DB에서 가져와 출력하는

게시판 글 리스트 부분을 정리하면,

기존에는 다음과 같이 SQL문이 프로그램 내에서 문자열로 사용되었다.

router.get('/list', function(req,res,next){
    pool.getConnection(function (err, connection) {
        var sql = "SELECT BRDNO, BRDTITLE, BRDWRITER, DATE_FORMAT(BRDDATE,'%Y-%m-%d') BRDDATE" +
                   " FROM TBL_BOARD";
        connection.query(sql, function (err, rows) {
            if (err) console.error("err : " + err);

            res.render('board1/list', {rows: rows});
            connection.release();
        });
    });
});

board1.js

9batis에서는

이 SQL문을 XML (board3.xml)로 저장하고,

ID (selectBoardList)를 부여하여 작성한 뒤에

컨트롤에서는 이 ID로 호출하면 (batis9.getQuery())

해당 SQL문을 반환 받아서 사용하게 된다 [라인 15].

이것이 9batis의 기본 사용 방법이자 기본 개념이다.


조금 더 복잡한 기능으로

SQL문에 필요한 파라미터를 지정 방법에 대하여 정리한다.

기존에는 글읽기와 글저장 부분의 SQL문이

다음과 같이 문자열로 사용되었다.

router.get('/read', function(req,res,next){
    pool.getConnection(function (err, connection) {
        var sql = "SELECT BRDNO, BRDTITLE, BRDMEMO, BRDWRITER, DATE_FORMAT(BRDDATE,'%Y-%m-%d') BRDDATE"+
                   " FROM TBL_BOARD" +
                  " WHERE BRDNO=" + req.query.brdno;
        connection.query(sql, function (err, rows) {
            ~~ 생략 ~~
        });
    });
});

router.post('/save', function(req,res,next){
    var data = [req.body.brdtitle, req.body.brdmemo, req.body.brdwriter, req.body.brdno];

    pool.getConnection(function (err, connection) {
        var sql = "";
        if (req.body.brdno) {
            sql = "UPDATE TBL_BOARD" +
                       " SET BRDTITLE=?, BRDMEMO=?, BRDWRITER=?" +
                  " WHERE BRDNO=?";
        } else {
            sql = "INSERT INTO TBL_BOARD(BRDTITLE, BRDMEMO, BRDWRITER, BRDDATE) VALUES(?,?,?, NOW())";
        }
        connection.query(sql, data, function (err, rows) {
            ~~ 생략 ~~
        });
    });
});

board1.js

SQL문에 필요한 파라미터는

글 읽기(/read)를 예로 할 경우 글 번호(brdno)를 의미한다.

지정된 글 번호에 따라 DB에서 글 내용을 조회하여 화면에 출력한다.

이 글 번호는 글 리스트에서 사용자의 선택에 따라 바뀌는 것으로

위 코드처럼 지정된 글 번호(req.query.brdno)에

맞는 데이터(WHERE)를 가지고 오도록

문자열 조작을 통하여 SQL문을 작성 한다.

" WHERE BRDNO=" + req.query.brdno


글 저장(/save)은 사용자가 입력한 값을 저장하기 위한 것으로

파라미터를 모두 ? 로 지정하고

배열 (data 변수)로 값을 넘기는 방식으로 작성한다.


9batis에서는

getQuery()를 호출할 때, JSON으로 파라미터를 지정한다 [라인 27, 41, 43].

SQL문 작성시 ${brdno}로 지정한 경우,

JSON의 키 이름을 brdno 로 하여 사용한다 [라인 27].

9batis에서 해당 변수에 JSON 값을 치환하여 SQL문을 반환한다.

(콘솔에 출력된 SQL문으로 확인할 수 있다.)

$는 SQL 문 변환시 숫자라는 의미로 값만 출력되고,

#은 문자라는 의미로 홑따옴표(')로 싸여서 출력된다.

    <sql id="includeWhere">
        WHERE BRDNO=${brdno}
    </sql>
 
    <update id="updateBoard">
        UPDATE TBL_BOARD
           SET BRDTITLE=#{brdtitle}, BRDMEMO=#{brdmemo}, BRDWRITER=#{brdwriter}
          {include refid="includeWhere"}
    </update>
    
    <insert id="insertBoard">
        INSERT INTO TBL_BOARD(BRDTITLE, BRDMEMO, BRDWRITER, BRDDATE)
        VALUES (#{brdtitle}, #{brdmemo}, #{brdwriter}, NOW())
    </insert>

board3.xml

이렇게 구현하면서 부수적인 효과는

기존에는 Node.js에서 제공하는 JSON 데이터(req.body)를

배열 (data 변수)로 변환해서 사용했지만

  var data = [req.body.brdtitle, req.body.brdmemo, req.body.brdwriter, req.body.brdno];

9batis에서는 JSON 데이터(req.body)를 그대로 사용한다 [라인 41, 43].

  sql = batis9.getQuery('insertBoard', req.body);


이번에는 9batis를 제대로 사용하는 방법(board4.js)을 정리한다.

board4.js

일단, 일부만 올린 board3.js 파일의 내용 보다 확 줄어든 코드 양을 볼 수 있다.

이것은 DB에 접속해서 SQL을 실행하고

실행 결과를 반환 받는 부분을 execQuery() 함수로 처리했기 때문이다.


다음과 같이 글 리스트(/list)에 대하여

기존에 작성한 코드와

9batis로 작성한 코드를 직접 비교하면 쉽게 알 수 있다.

router.get('/list', function(req,res,next){
    pool.getConnection(function (err, connection) {
        var sql = "SELECT BRDNO, BRDTITLE, BRDWRITER, DATE_FORMAT(BRDDATE,'%Y-%m-%d') BRDDATE" +
                   " FROM TBL_BOARD";
        connection.query(sql, function (err, rows) {
            if (err) console.error("err : " + err);

            res.render('board1/list', {rows: rows});
            connection.release();
        });
    });
});

board1.js

execQuery() 함수를 호출하고

반환 받은 실행 결과인 rows을 render()에 파라미터로 지정하면 끝이다.

파라미터가 null인 것은 SQL문 생성을 위해 필요한 값이 없다는 것으로,

SQL문 생성을 위해 필요한 값이 있는 경우

앞서 정리한 예제와 동일하게 JSON 으로 지정하면 된다 [라인 19, 29, 37 43 참조].

router.get('/list', function(req,res,next){
    batis9.execQuery(pool, 'selectBoardList', null, function (rows) {
         res.render('board4/list', {rows: rows});
    });
});

board4.js

execQuery()의 파라미터는

DB접속 클래스, 실행할 SQL 아이디, SQL 실행에 필요한 파라미터(JSON), SQL 실행 완료 후 실행할 콜백 함수

의 4 가지 이다.

이 중 DB접속 클래스(pool)는

loadQuery() 호출시 미리 지정해서 사용하는 것이 편리하지만

이해를 위해 현재는 이렇게 구현했다.

메소드

설명

예제

 loadQuery

 - SQL문이 있는 XML 파일을 읽어서 보관.

 - 파라미터

   filepath: XML 파일이 있는 폴더
   mode: 디버깅 여부.

 batis9.loadQuery(__dirname +'/sql', true)

 getQuery

 - 지정된 ID의 SQL문을 찾아서 반환.

 - 파라미터

   id: XML 파일에서 지정된 SQL문의 id
   params: SQL문에 지정된 변수의 값 (Json)

 batis9.getQuery('selectBoardList');

 batis9.getQuery('selectBoardOne', {brdno: 1});

 execQuery

 - 지정된 ID의 SQL문을 찾아서 실행

 - 파라미터

   pool: DBMS에 대한 연결
   id: XML 파일에서 지정된 SQL문의 id
   params: SQL문에 지정된 변수의 값 (Json)
   callback: SQL문 실행후 호출할 함수

batis9.execQuery(pool, 'selectBoardList', null, function (rows) {
    res.render('board4/list', {rows: rows});
});


마지막으로 2개 이상의 SQL문을 실행하는 것과

하나의 SQL문으로 두 가지 기능(IF문)을 하는

다소 복잡해 보이는 예제(board5.js)를 정리한다.

board5.js

먼저, 9 라인 과 10 라인을 보면

두 개의 SQL문이(selectBoardOne, selectBoardList) 사용된 것을 볼 수 있다.


글 읽기(/read)에서

지정된 글 내용을 DB에서 가지고 온다(selectBoardOne).

이 기능에 이전 글에 대한 정보(selectBoardList5)를 제공하는 기능을 추가하였다.

현재 글을 모두 읽은 후

글 리스트로 돌아가서 다른 글을 선택하는 것이 아니라

글을 읽던 페이지에서 바로 이전 글을 읽을 수 있도록 하는 것이다.

(다음 글 기능은 각자 구현해 보길)


두 개의 SQL문을 사용하는 것은

기존의 Node.js에서 사용하는 것이랑 동일하게

하나의 SQL문이 실행 완료되면 [라인 9],

지정된 콜백 함수에서 나머지 SQL문을 실행하도록 하면 된다 [라인 10].

이 기능은 구현한 것이 아니고 Node.js의 기능을 그대로 쓰는 것이다.


다음으로 하나의 SQL문(board5.xml)으로 두 가지 기능을 하도록 구현한다.


글 읽기에서 이전 글에 대한 정보를 가지고 오는 것은

글 리스트와 거의 유사한 SQL문을 사용한다.

이전 글은 현재 글 보다 글 번호가 적은 글 중 하나를 가지고 오는 것이다.

글 리스트(/list)는 그냥 모든 글을 가지고 온다.


이 둘의 공통 점은 글에 대한 정보(글 제목, 작성일자 등)를 가지고 오는 것이고

차이점은

조건에 의한 하나의 행(LIMIT)을 조회하느냐,

또는 여러 개의 리스트를 가지고 오느냐 이다.


board5.xml

기존에 사용하던 selectBoardList를 두고

예제를 위해 별도로 selectBoardList5로 새로 생성하였고,

출력할 데이터 개수(cnt)를

지정하면(if) 이전 글을 찾는 SQL문을 [라인 6~10],

지정하지(else) 않으면

조건을 사용하지 않아 모든 데이터가 조회된다 [라인 11~13].


이 SQL문을

글 리스트(/list)에서는 별 지정 없이(null) 호출하고 [board5.js 라인 3]

batis9.execQuery(pool, 'selectBoardList5', null,


글 읽기(/read)에서는 {cnt: 1}과 같이 파라미터를 지정한다 [board5.js 라인 10].
    batis9.execQuery(pool, 'selectBoardList5', {brdno: req.query.brdno, cnt: 1},



이상으로 nineBatis (9batis)의 사용법을 정리하였다.

9batis는 SQL문을 XML 파일에 저장하고,

프로그램에서 간단하게 호출하여 (execQuery)

사용할 수 있도록 도와 주는 Node.js 라이브러리로

여기에서 다운로드 받을 수 있다.




+ Recent posts