하게 된 계기
릴리즈를 진행한 프로젝트에서 서버 개발자를 담당하였는데,
db에 쿼리를 날릴 때 반복적인 부분(커넥션 풀 생, 반납, 트랙잭션)이 많아서 리팩토링을 맘 먹게 되었다.
프로젝트 구조는 다음과 같다.
먼저 nodejs + express에 mysql을 orm이 아닌 mapper 방식으로 사용하고 있다.
언어는 Javascript를 사용 중에 있다.
왜 orm을 안썻냐면,
- sql로 직접 쿼리문을 날리면서 공부를 하고 싶었다.
- 사용 언어에 따라 객체지향 프로그래밍을 하고 있지 않아서 의미가 없다고 생각했다.
📦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) {},
};
이 코드에서 내가 리팩토링 하고 싶었던 부분은 다음과 같다.
- 쿼리문을 날리기 전 후로 connection을 생성하고, release 하는 부분을 모듈화해야지
- 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);
}),
'Development > Node.js' 카테고리의 다른 글
CodeDeploy 이해하고 푸쉬 서버와 API 서버 분리하기 (0) | 2022.08.18 |
---|---|
슬랙에서 서버 에러 알림 받고 유연하게 에러 대응하기 (0) | 2022.08.18 |
[에러 해결] Error: No default engine was specified and no extension was provided (0) | 2021.11.23 |
bcrypt로 비밀번호 암호화 (0) | 2021.11.23 |
Nodejs Webpack으로 빌드하는 과정에서 발생한 에러(ERROR in ./node_modules/@mapbox/node-pre-gyp/lib/util/s3_setup.js) (0) | 2021.11.23 |
댓글