Blocking
Table Of Contents
- 블로킹
- 콜백 함수
- Arrow Function
- Welcome To Callback Hell
- Promise
- 비동기 제어 디자인 패턴 GET
- 비동기 제어 디자인 패턴 POST
- 비동기 제어 디자인 패턴 PUT
- 비동기 제어 디자인 패턴 DELETE
블로킹
- Node.js 프로세스에서 추가적인 JavaScript의 실행을 위해 JavaScript가 아닌 작업이 완료될 때까지 기다려야만 하는 상황
- 일반적으로 NodeJS의 I/O 논블로킹으로 앞선 작업이 완료될때까지 후순위 작업이 기다려주지 않습니다.
- I/O란 인풋,아웃풋으로 일반적으로 시스템 디스크 및 네트워크와 상호작용하는 것으로 이해하면 됩니다.
// 블로킹 코드
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // 파일을 읽을 때까지 여기서 블로킹 됩니다.
//위 코드는 에러 핸들링이 없음
//non 블로킹 코드
//논블로킹 코드는 기다려 주지 않음으로, 작업이 완료된 후 코드 실행을 원한다면 아래와 같이 콜백 함수를 만들어야 합니다.
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
논블로킹을 사용하는 이유는 웹앱의 퍼포먼스를 극대화 하기 위함입니다.
데이터를 불러올 때마다 지연이 발생하고, 다른 작업이 완료되지 못한다면 앱의 성능이 확연히 떨어질 것입니다.
콜백 함수
- 첫번째 함수가 실행된 결과에 따라 실행되는 함수이다.
- 위의 readFile 함수의 파일을 읽은 결과를 알고 싶을 경우, 애로우 펑션을 이용해 콜백함수를 구현하여 확인할 수 있었다.
function callFunction(callback){
console.log("callFunction called")
let one = 1
let two = 2
let result = one+two
if(result>2){
callback("biggner than 2")
} else {
callback("not biggner than 2")
}
}
callFunction(function(message){
console.log(message)
})
Arrow Function
- 화살표 함수는 익명함로 function이라는 표현을 대신하는 함수 입니다.
- 다만 this, super 등이 없고 생성자로 사용할 수 없습니다.
- 이 때문에 메소드로는 사용할 수 없고, 주로 콜백함수로 많이 사용합니다.
var wakeUp = function(){
console.log('wake up!')
}
setInterval(wakeUp, 1000);
//특정 기간동안 특정 기능을 실행 하는 함수
//wakeUp은 setInterval이라는 함수의 결과로 실행되는 'callback' 함수 입니다.
//위 함수는 아래와 같이 익명함수로 변경할 수 있습니다.
setInterval(function(){
console.log('wake up!')
}, 1000);
//그리고 이러한 익명함수는 아래와 같이 화살표 함수 표현으로 대체할 수 있습니다.
setInterval(() => {
console.log('wake up!')
}, 1000);
Welcome To Callback Hell
- 예전 Javascript 개발자는 콜백 지옥이라는 표현을 많이 썼습니다.
- 우리가 다음과 같은 함수를 순차적으로 작성한다고 해봅시다.
setTimeout(() => {
console.log('sleep')
}, 3000);
setTimeout(() => {
console.log('wake up!')
}, 1000);
console.log('eat');
저는 3초를 자고, 1초 후에 일어나서, 먹기를 바라지만 실제 실행되는 코드는 그렇지 않을 것입니다.
때문에 우리는 다음과 같이 코드를 짜게 됩니다.
setTimeout(() => {
console.log('sleep')
setTimeout(() => {
console.log('wake up!')
console.log('eat');
}, 1000);
}, 3000);
선생님👨🏫: 벌써 좀 복잡해 보이는게 느껴지시나요?
만약 이 콜백이 여러번 가게 된다면? 그것이 바로 콜백 지옥 입니다.
Promise
- 이런 논블로킹 코드를 제어하기 위해 우리는 Promise를 씁니다.
- Promise를 어떻게 쓰는 지 알아보기 위해 다음과 같은 간단한 코드를 한번 작성해 봅니다.
- 우리는 Router가 너무 복잡해져서, query 모델을 분리하기로 결정했습니다.
module.exports.getList = (connection, options) => {
connection.query('SELECT * FROM schools ', options.schoolName, function (error, results, fields) {
if (error) {
res.status(404).json({result:false})
throw error;
}
connection.commit(async function(err) {
if (err) {
res.status(404).json({result:false})
throw err;
}
console.log('results : ',results)
return results;
});
});
});
그리고 기존 코드에서는 아래와 같이 getList 모듈을 가져오기로 합니다.
var school = require('../models/school')
pool.getConnection(function(err, connection) {
const results = school.getList(connection, {schoolName:schoolName})
console.log('success')
res.status(200).json({result:results})
})
선생님👨🏫: 위 코드를 실행해보면, 쿼리 문의 results는 콘솔로 불러올 수 있지만 어쩐일인지, 클라이언트에 응답하는 result는 빈 값으로 나가게 됩니다.
NodeJS의 논블로킹에 따라, 디비에서 데이터를 가져오기 전에 이미 응답하기 때문입니다.
우리는 위 코드를 아래와 같이 바꿀 수 있습니다.
module.exports.getList = (connection, options) => {
return new Promise((resolve, reject) => {
connection.query('SELECT * FROM schools ', options.schoolName, function (error, results, fields) {
// connection.query(query, function (error, results, fields) {
if (error) {
res.status(404).json({result:false})
throw error;
}
connection.commit(async function(err) {
if (err) {
res.status(404).json({result:false})
throw err;
}
console.log('results : ',results)
resolve(results);
});
});
});
};
var school = require('../models/school')
pool.getConnection(async function(err, connection) {
const results = await school.getList(connection, {schoolName:schoolName})
console.log('success')
res.status(200).json({result:results})
})
선생님👨🏫: 그리고 다시 실행해 보면 이번에는 무사히 값들을 불러오는 걸 알 수 있습니다!
- resolve : 결정 (return)
- reject : 거부 (에러)
- async : 함수를 블로킹으로 제어하기 위해 붙여주는 명령어
- await : 기다려야 하는 함수 앞에 추가
선생님👨🏫: 여기서 하나 알아야할 것이 있습니다.
우리는 구조화하기 위해서 Promise를 사용한 것이 아닙니다.
우리는 구조화하는 과정에서 비동기 흐름을 제어할 필요를 느꼈고, 그렇기 때문에 Promise를 사용한 것입니다.
일종의 디자인 패턴을 만들기 위해서 꼭 Promise를 써야 하는 것은 아니라는 점을 말씀드리고 싶었습니다.
비동기 제어 디자인 패턴 GET
- 그리고 이렇게 Promise를 활용하면 코드를 더욱 멋지게 구조화할 수 있습니다.
- 먼저 components에 db.js 파일을 만들고, 아래와 같이 코드를 넣어 줍니다.
const config = require('../config')
const mysql = require('mysql')
var pool = mysql.createPool({
connectionLimit : config.database.connectionLimit,
host: config.database.host,
user: config.database.user,
password: config.database.password,
database: config.database.database
});
module.exports.query = (options) => {
return new Promise((resolve, reject) => {
pool.getConnection(function(err, connection) {
if (err) reject(err); // not connected!
connection.query(options.query, options.value, function (error, results, fields) {
if (error) {
reject(error)
}
connection.commit(async function(err) {
if (err) {
reject(err)
}
console.log('results : ',results)
resolve(results);
});
});
});
});
}
선생님👨🏫:
위 query 문 안에는 getConnection과 query 명령어를 돌리는 코드가 모두 들어가 있습니다.
즉 이제 반복적으로 해당 문을 다시 쓰지 않아도 됩니다.
그리고 models라는 폴더를 만들어 school.js를 생성하겠습니다.
이제 실제 query에 해당하는 명영어는 이 model 디렉토리에서 작성할 예정입니다.
const db = require('../components/db')
module.exports.getList = async (options) => {
console.log('options : ',options)
return await db.query({
query: 'SELECT * FROM schools WHERE name = ?',
value: options.schoolName
})
};
선생님👨🏫: 위와 같이 작성하면 이제 간단하게 query를 관리할 수 있습니다!
마지막으로 routes의 school.js 의 get 메소드는 다음과 같이 간단하게 바꿔줄 수 있습니다.
router.get('/', async function (req, res) {
const schoolName = req.query.schoolName
const results = await school.getList({schoolName:schoolName})
console.log('success')
res.status(200).json({result:results})
})
비동기 제어 디자인 패턴 POST
- 그리고 post도 상당히 다르게 변경할 수가 있습니다.
- POST, UPDATE, DELETE의 경우 우리는 rollback을 사용하기 때문에, beginTransaction으로 코드를 시작해야 합니다.
- 그래서 db.js에 아래 코드를 추가하겠습니다.
module.exports.beginTransaction = () => {
return new Promise((resolve, reject) => {
pool.getConnection(function(err, connection) {
if (err) reject(err); // not connected!
// Use the connection
connection.beginTransaction(function(err) {
if (err) { reject(err); }
resolve(connection)
});
});
})
}
선생님👨🏫: 또한 commit과 rollback도 구현 해 줍니다.
module.exports.commit = connection => {
return new Promise((resolve, reject) => {
connection.commit(err => {
if (err) reject(this.rollback(connection));
else {
connection.release();
resolve();
}
});
});
};
module.exports.rollback = connection => {
return new Promise((resolve, reject) => {
connection.rollback(err => {
if (err) reject(err);
else {
connection.release();
resolve();
}
});
});
};
routes/school.js의 post 메소드를 수정해 보겠습니다.
router.post('/', async function (req, res) {
try {
const json = req.body;
const connection = await db.beginTransaction();
var newSchool = { name: json.name, address: json.address, type: json.type };
const results = await school.insert(connection, newSchool);
await db.commit(connection);
if (results) {
res.status(200).json({ result: results.insertId });
}
} catch (err) {
console.log('err : ', err);
await db.rollback(connection);
next(err);
}
});
선생님👨🏫:
- try...catch에서 catch는 throw err가 일어났을 시(에러가 발생했을 시에), 실행하는 코드 입니다.
- 이때 우리는 rollback을 실행해 주며 next 함수를 실행합니다.
- next는 다음 미들웨어를 실행시키는 함수로, 에러가 발생했을 시 다음 미들웨어를 실행하도록 하여 넘어갑니다.
- 그리고 insert 코드는 다음과 같이 아름답게 변합니다.
module.exports.insert = async (connection, options) => {
console.log('options : ', options);
return await db.query({
connection: connection,
query: 'INSERT INTO schools SET ?',
value: options,
});
};
비동기 제어 디자인 패턴 PUT
선생님👨🏫: put은 이제 아주 손쉽게 고칠 수가 있습니다.
// routes/school.js
router.put('/', async function (req, res) {
try {
const json = req.body;
const connection = await db.beginTransaction();
var newSchool = { name: json.name, address: json.address, type: json.type };
const results = await school.update(connection, {
options: newSchool,
idx: json.idx,
});
await db.commit(connection);
if (results) {
res.status(200).json({ result: results });
}
} catch (err) {
console.log('err : ', err);
await db.rollback(connection);
next(err);
}
});
//models/shool.js
module.exports.update = async (connection, options) => {
console.log('options : ', options);
return await db.query({
connection: connection,
query: 'UPDATE schools SET ? WHERE idx = ?',
value: [options.options, options.idx],
});
};
비동기 제어 디자인 패턴 DELETE
선생님👨🏫: delete 역시 마찬가지 입니다.
// routes/school.js
router.delete('/', async function (req, res) {
const json = req.body;
try {
const json = req.body;
const connection = await db.beginTransaction();
const results = await school.delete(connection, json.idx);
await db.commit(connection);
if (results) {
res.status(200).json({ result: results });
}
} catch (err) {
console.log('err : ', err);
await db.rollback(connection);
next(err);
}
});
//models/shool.js
module.exports.delete = async (connection, options) => {
console.log('options : ', options);
return await db.query({
connection: connection,
query: 'DELETE FROM schools WHERE idx = ?',
value: [options],
});
};