본문 바로가기

웹 프로그래밍 백엔드/Node.js

Node.js에서 MySQL다루기 (싱글턴, 커넥션 관리, 커넥션 pool 생성)

노드를 통해 서버 api를 만드는 과정에서 데이터베이스를 다루는 일은 기본적이고 필수적이다. 

mysql모듈을 통해 MySQL을 다루는 과정을 다루어 보겠다.
데이터베이스를 다루는 과정에서 비동기 처리는 필수적이다.

비동기 처리에 관한 글은 여기를 보기 바란다-> https://t-anb.tistory.com/76

 

기본적인 형식은 connection객체(conn)를 만들고, 그 객체에 붙어있는 메서드(query)를 활용해 쿼리문을 실행하는 방식이다.

아래와 같다.

const mysql=require('mysql')
const connectionSetting={
    host:...,
    port:...,
    user:...,
    password:...,
    multipleStatements:...,
    database:...,
    connectionLimit:..., (pool을 만들 때 사용!)
}

const conn=mysql.createConnection(connectionSetting)
conn.query(...)

 

connection을 만들 때 createConnection안에 연결 세팅값을 주어야 하는데, 위와 같이 따로 빼서 관리할 수 있다. 거기서 multipleStatements에 true값을 주면 세미콜론으로 이어진 여러 개의 쿼리문을 한꺼번에 날릴 수 있게 된다.

 

위의 방식은 낱개의 connection객체를 만들 때 사용하는 방식이고, 보통은 pool을 만들어 사용하는 것으로 알고 있다. 커넥션 객체를 낱개로 만들면 만들 때마다 conn.connect(), conn.destroy() 를 통해 연결과 제거 과정을 필수적으로 해주어야 하는데, 여러 개의 요청을 처리하는 쿼리를 날릴 경우, 불필요한 반복 작업이 된다. 그리고 연결과 제거 과정 자체가 시스템 자원을 소모하는 작업인 것으로 알고 있다.

 

그래서 pool을 만들어 사용하는 방식이 일반적인데, 아래와 같다.

let settingObj = {
    host: dbSetting.host,
    port: dbSetting.port,
    user: dbSetting.user,
    password: dbSetting.password,
    multipleStatements: true,
    database: dbSetting.database,
    connectionLimit: dbSetting.connectionLimit,
}

const pool=mysql.createPool(settingObj)

위와 같이 연결 세팅 객체를 settingObj로 만들어서 관리하고, 이를 createPool메서드에 인자로 넣어 pool을 만들게 된다. 세팅 객체 내부에 있는 connectionLimit을 통해 pool안에 만들어 둘 연결 객체 수를 조절할 수 있다.

 

createConnection을 통해 연결객체를 만든다면, 쿼리를 수행할 때마다 매번 메서드를 불러야겠지만, pool에서 뽑아서 쓸 때는 단 한 개의 pool객체만 있어야 한다. 그래서 싱글턴으로 관리하는 것이 필연적이다. 아래의 코드를 보자.

// 파일명: dbPoolCreator.js

module.exports = (function () {
    let dbPool;
    const initiate = async () => {
        return await mysql.createPool(settingObj)
    }
    return {
        getPool: async function () {
            if (!dbPool) {
                dbPool = await initiate();
                return dbPool
            }
            else return dbPool;
        }
    }
})();

데이터베이스를 다루기 시작하면, 비동기 개념도 필수적으로 따라오게 된다. 왜냐하면 createPool, createConnection, conn.query등이 모두 비동기 함수이기 때문이다. async-await, promise와 같은 비동기 처리에 대한 내용은 다른 포스팅에서 다루도록 하겠다.

 

위의 코드는 function(){ ... } 이라는 무기명 함수를 만들고 이를 바로 즉각 실행하는, 즉시실행무기명함수를 export 해주는 역할을 하고 있다. dbPool과 initiate는 내부에서 활용하기 위해 만들어두고, 결국은 getPool을 포함하는 객체를 리턴해주는 것이다. 따라서 다른 파일에서는 이 모듈을 받고 getPool메서드를 실행하여 pool을 가져올 수 있게 되는 것이다. dbPool이 없다면 만들어서 넣고, 있으면 그대로 리턴하기에 항상 같은 pool을 보게 되는 것이다. 즉 싱글턴의 역할을 하고 있다.

 

이제 pool에서 커넥션을 가져와서 쿼리를 날리고 반납하는 과정을 알아보겠다. 위에서 dbPoolCreator라는 이름으로 만든 모듈을 아래에서 db라는 이름으로 받았다.

const mysql = require('mysql');
const db = require('./dbPoolCreator')

class Dao {

    sqlHandler = (sql, q, fn) => {
        if (q) sql = mysql.format(sql, q)
        // console.log(sql)
        return new Promise(async (resolve, reject) => {
            try {
            	// pool을 가져오는 과정
                const dbPool = await db.getPool()
                
                // pool에서 연결객체를 가져오는 과정
                dbPool.getConnection((err, conn) => {
                    if (err) {
                        if (conn) conn.release();
                        return reject(err)
                    }
                    // 내부 콜백에서 쿼리를 수행
                    conn.query(sql, (err, rows, fields) => {
                        conn.release();
                        if (err) return reject(err) or fn(err)
                        resolve(rows) or fn(rows)
                    })
                })
            } catch (err) {
                return reject(err) or fn(err)
            }
        })
    }
    
    findByEmail = email => {
        return this.sqlHandler(sqls.sql_findByEmail, email)
    }
}

module.exports = new Dao()

Dao클래스를 만들고, new Dao( )를 통해 객체를 생성하여 배출한다. 이렇게 해서도 일종의 싱글턴 역할을 할 수 있게 된다. 단 하나의 객체만 생성하였기 때문이다. 위에서 중요한 점은 바로 conn객체를 release해줘야 한다는 점이다. release를 통해 pool에 반납하게 되고, 이후에 다시 쓸 수 있다. 기본은 query수행 직후에 release 하는 것이지만, 위에서 보다시피 에러가 발생하였을 때도 반드시 release 해주어야 한다. 

 

그리고 sqlHandler메서드는 3번째 인자로 fn을 받는데, 이는 요청을 처리하는 라우터 모듈 등에서 쿼리 수행 후 처리를 위한 콜백함수를 지정했을 때 사용하려고 넣어두었다. 그래서 위에서 보다시피 resolve(rows), reject(err)등으로 프로미스 객체를 리턴할 수도 있지만, fn(rows), fn(err)등으로 sqlHandler를 콜한 함수에 이후 처리를 위탁할 수도 있는 것이다. 이 부분은 자바스크립트의 대단히 특이한 점이라고 생각한다. 콜백 함수의 사용 목적 중의 하나라고도 생각된다. 

콜백 함수는 처리의 순서를 정해주기 위함으로 만들 수도 있지만, 위처럼 모듈과 모듈 간의 연결에서 처리를 위탁하기 위해 사용할 수도 있다.

 

그리고 위에서 굉장히 중요한 부분이 있는데, 그것은 바로 this.sqlHandler를 호출하는 findByEmail메서드이다. 

현재 필자가 만든 Dao클래스에는 sqlHandler가 있고, 이 메서드가 쿼리 수행을 담당한다. 그리고  findByEmail 같은 메서드는 쿼리문과 인자만 결정하는 역할이고, 기본적으로는 sqlHandler를 사용하게 된다.

Dao를 클래스로 만들어서 관리하고 있는데 class내부는 use strict가 디폴트로 적용되기 때문에, 아래처럼 function으로 메서드를 만들게 되면 스코프 관리에서 문제가 발생한다. findByEmail메서드 내부에 있는 this는 이제 Dao가 아니게 되는 것이다.

class Dao{
	...
    
    findByEmail=function(email){
    		return this.sqlHandler(...)
    	}
}

그래서 this.sqlHandler를 사용하고 싶은 경우에는 반드시! 앞서 올린 코드에서처럼 findByEmail = 화살표 함수로 정의해야만 한다.

 

그리고 conn.query( )에는 sql문과 sql문에 있는 ?를 채우기 위한 인자가 들어가는데 이는 Java에서의 preparedStatement를 만드는 과정과 비슷하다고 생각하면 된다. 거기에서처럼 ? 순서에 맞게 인자를 넣어주어야 한다.

 

* 마지막으로 주의할 부분은 connection Pool에서 연결 객체를 가져온 후, 해당 연결 객체의 연결 관련 옵션을 변경한 경우이다.

연결 객체의 user, database 등 옵션을 변경하게 되면, 변경 직후에는 변경된 사항이 적용된 채로 사용할 수 있지만, release하여 pool로 반납되면 변경사항이 모두 없어진다.

 

위와 같이 node에서 mysql을 다루는 기본적인 사항에 대해 알아보았다.

그리고 데이터베이스를 다루는 함수들은 대부분 비동기이다. 비동기 관련된 글은 여기를 참조 바란다.

추후에 쿼리문 내의 ?와 ??의 차이점 등등에 대해 더 보완하겠다.