본문 바로가기
Development/Node.js

Express, mysql2 코드 모듈화하기 (with 트랜잭션)

by 메정 2022. 8. 18.

하게 된 계기

릴리즈를 진행한 프로젝트에서 서버 개발자를 담당하였는데,

db에 쿼리를 날릴 때 반복적인 부분(커넥션 풀 생, 반납, 트랙잭션)이 많아서 리팩토링을 맘 먹게 되었다.

프로젝트 구조는 다음과 같다.

먼저 nodejs + express에 mysql을 orm이 아닌 mapper 방식으로 사용하고 있다.

언어는 Javascript를 사용 중에 있다.

왜 orm을 안썻냐면,

  1. sql로 직접 쿼리문을 날리면서 공부를 하고 싶었다.
  2. 사용 언어에 따라 객체지향 프로그래밍을 하고 있지 않아서 의미가 없다고 생각했다.

📦src
┣ 📂config
┃ ┣ 📜db.js
┣ 📂controllers
┣ 📂models //여기서 db 접근
┣ 📂routes
┣ 📂utils
┣ 📜app.js
┃ 📜.eslintrc.js
┃ 📜.prettierrc.js
┃ 📜pacakge.json
┃ 📜pacakge-lock.json
┃ 📜webpack.config.js
┃ 📜ecosystem.config.js

코드를 계속 보다보니 좀 더 리팩토링할 부분이 많이 보여서 천천히 이슈를 나눠서 진행할 예정이다.

먼저 해당 포스팅에서 다룰 내용은 다음과 같다.

const mysql = require('mysql2/promise');
const logger = require('./winston');
require('dotenv').config({ path: '../.env' });

/* @see : connection은 DB에 접속 -> SQL문 날림 -> 결과 받고 -> 연결 종료의 flow를 갖음
 * connection을 닫지 않으면 리소스를 불필요하게 낭비
 * pool.getConnection() -> connection.query() -> connection.release()
 */

const TAG_SUCCESS = 'DB success create pool!!';
const TAG_PROTOCOL_CONNECTION_LOST = 'Database connection was closed.';
const TAG_ER_CON_COUNT_ERROR = 'Database has too many connections.';
const TAG_ECONNREFUSED = 'Database connection was refused.';

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  port: process.env.DB_PORT,
  database: process.env.DB_NAME,
  connectionLimit: 50,
});

logger.info(TAG_SUCCESS);

exports.connection = async function () {
  const connection = await pool.getConnection(async (conn) => conn);
  try {
    return connection;
  } catch (err) {
    switch (err.code) {
      case 'PROTOCOL_CONNECTION_LOST':
        logger.error(TAG_PROTOCOL_CONNECTION_LOST);
        break;
      case 'ER_CON_COUNT_ERROR':
        logger.error(TAG_ER_CON_COUNT_ERROR);
        break;
      case 'ECONNREFUSED':
        logger.error(TAG_ECONNREFUSED);
        break;
    }
  }
};
const pool = require('../config/db');

module.exports = {
  selectCart: async function (req) {
    const params = Number(req.decoded);

    const sqlSelect = `SELECT * FROM carts WHERE id = ?`;

    const connection = await pool.connection(async (conn) => conn);
    const [rows] = await connection.query(sqlSelect, params);
    connection.release();

        ...
  },
  insertCart: async function (req) {
    const userId = Number(req.decoded);
    const itemId = Number(req.body.item_id);

    const sqlInsert =
      'INSERT INTO cart (user_id, item_id, item_count) VALUES (?, ?, 1)';
    const params = [userId, itemId];

    const connection = await pool.connection(async (conn) => conn);
    await connection.beginTransaction();
    const [rows] = await connection
      .query(sqlInsert, params)
      .then(await connection.commit())
      .catch(await connection.rollback());
    connection.release();

        ...
  },
 updateCart: async function (req) {},
 deleteCart: async function (req) {},
};

이 코드에서 내가 리팩토링 하고 싶었던 부분은 다음과 같다.

  1. 쿼리문을 날리기 전 후로 connection을 생성하고, release 하는 부분을 모듈화해야지
  2. CUD와 같은 트랜잭션 처리가 필요한 부분을 따로 모듈화해야지

적용

모듈화를 위해 db.js 파일을 다음과 같이 수정하였다.

module.exports = {
  connection: async function () {
    const connection = await pool.getConnection(async (conn) => conn);
    try {
      return connection;
    } catch (err) {
      switch (err.code) {
        case 'PROTOCOL_CONNECTION_LOST':
          logger.error(TAG_PROTOCOL_CONNECTION_LOST);
          break;
        case 'ER_CON_COUNT_ERROR':
          logger.error(TAG_ER_CON_COUNT_ERROR);
          break;
        case 'ECONNREFUSED':
          logger.error(TAG_ECONNREFUSED);
          break;
      }
    }
  },
    // select 쿼리 같은 트랜잭션이 필요 없을 때 사용할 db 모듈
  query: async function (query, ...args) {
    let rows;
    const connection = await this.connection(async (conn) => conn);

    if (!args) {
      rows = await connection.query(query);
    } else { // where 절로 들어올 인자가 여러 개 일 수 있으므로
      let sqlQuery = '';
      await Promise.all(
        args.map(async (parameter) => {
          sqlQuery += mysql.format(query, parameter);
        }),
      );
      rows = await connection.query(sqlQuery);
    }
    connection.release();

    return rows;
  },
    // insert, update, delete와 쿼리 같은 트랜잭션이 필요할 때 사용할 db 모듈
  queryWithTransaction: async function (query, ...args) {
    let rows;
    const connection = await this.connection(async (conn) => conn);

    await connection.beginTransaction();
    if (!args) {
      rows = await connection
        .query(query)
        .then(await connection.commit())
        .catch(await connection.rollback());
    } else { // where 절로 들어올 인자가 여러 개 일 수 있으므로
      let sqlQuery = '';
      await Promise.all(
        args.map(async (parameter) => {
          sqlQuery += mysql.format(query, parameter);
        }),
      );
      rows = await connection
        .query(sqlQuery)
        .then(await connection.commit())
        .catch(await connection.rollback());
    }
    connection.release();
    return rows;
  },
};

그리고 함수를 호출할 때 다음과 같이 수정하였습니다.

const db = require('../config/db');

module.exports = {
  selectCart: async function (req) {
    const params = Number(req.decoded);

    const sqlSelect = `SELECT * FROM carts WHERE id = ?`;

    const [rows] = await db.query(sqlSelect, params);

        ...
  },
  insertCart: async function (req) {
    const userId = Number(req.decoded);
    const itemId = Number(req.body.item_id);

    const sqlInsert =
      'INSERT INTO cart (user_id, item_id, item_count) VALUES (?, ?, 1)';
    const params = [userId, itemId];

    const [rows] = await db.queryWithTransaction(sqlInsert, params);

        ...
  },
 updateCart: async function (req) {},
 deleteCart: async function (req) {},
};

호출부가 이렇게 한 줄로 깔끔하게 정리될 수 있게 되었습니다! 마음의 평안이 ….

where에 인자가 여러 개인 경우 혹은 insert 시 넘겼을 때 매핑이 안되는 문제 때문에 애를 많이 먹었는데요 …. mysql.format을 이용하여 쿼리문에 대응하는 ?에 일일히 하나하나 넣어서 새 쿼리를 만드는 방식으로 해결했습니다.

let sqlQuery = '';
await Promise.all(
    args.map(async (parameter) => { 
    sqlQuery += mysql.format(query, parameter);
}),

댓글