Vana Blog

Node.js Design Pattern - 일반 JavaScript의 사용

April 20, 2019

콜백 규칙

비동기 코드를 작성할 때 명심해야 할 규칙

  • 콜백을 정의할 때 함부로 클로저를 사용하지 않는 것.
  • 모듈화 및 재사용을 고려해야 한다.

코드를 개선하는데 도움이 되는 기본 원칙

  • 가능한 빨리 종료한다. return, continue, break 사용으로 중첩문을 대신하여 현재 문을 빠져 나가도록 한다. 코드를 얕게 유지.
  • 콜백을 위해 명명된 함수를 생성하여 클로저 바깥에 배치하며 중간 결과를 인자로 전달한다.
  • 코드를 모듈화한다. 작고 재사용 가능한 함수들로 분할한다.

콜백 규칙 적용

else문 제거

if(err) {
  callback(err);
} else {
  // 오류가 없을 때 실행할 코드
}
 
if(err) {
  return callback(err);
}
// 오류가 없을 때 실행할 코드

콜백이 호출된 후에 함수를 종료하는 것을 잊는 것에 주의!

이전 spider()함수를 if else로 중첩되어 있던 코드를 별도의 함수로 분리하여 최적화 시킬 수 있다.

function saveFile(filename, contents, callback) {
mkdirp(path.dirname(filename), err => {
if(err) {
return callback(err);
}
fs.writeFile(filename, contents, callback);
});
}
function download(url, filename, callback) {
console.log(`Downloading ${url}`);
request(url, (err, response, body) => {
if(err) {
return callback(err);
}
saveFile(filename, body, err => {
if(err) {
return callback(err);
}
console.log(`Downloaded and saved: ${url}`);
callback(null, body);
});
});
}
function spider(url, callback) {
const filename = utilities.urlToFilename(url);
fs.exists(filename, exists => {
if(exists) {
return callback(null, filename, false);
}
download(url, filename, err => {
if(err) {
return callback(err);
}
callback(null, filename, true);
})
});
}
이와 같이 하면 saveFile, download 함수를 export 하여 다른 모듈에서 재사용하도록 할 수 있다. 클로저와 익명 함수를 남용하지 않도록 한다.

순차 실행

순차적으로 실행한다는 것은 한 번에 하나씩 실행한다는 것을 의미한다. 실행 순서가 중요하다. 이 흐름에는 다음과 같은 변형이 있다.

  1. 결과를 전달하거나 전파하지 않고 일련의 알려진 작업을 순서대로 실행.
  2. 작업의 출력을 다음 작업의 입력으로 사용.(체인, 파이프라인, 폭포수)
  3. 순차적으로 각 요소에 대해 비동기 작업을 실행하면서 일련의 작업들을 반복.

알려진 일련의 작업에 대한 순차 실행

function asyncOperation(callback) {
process.nextTick(callback);
}
function task1(callback) {
asyncOperation(() => {
task2(callback);
});
}
function task2(callback) {
asyncOperation(() => {
task3(callback);
});
}
function task3(callback) {
asyncOperation(() => {
callback(); //finally executes the callback
});
}
task1(() => {
//executed when task1, task2 and task3 are completed
console.log('tasks 1, 2 and 3 executed');
});
위의 패턴은 일반적인 비동기 작업 완료 시, 각 작업이 다음 작업을 호출하는 방법이다. 작업의 모듈화에 중점을 두어 비동기 코드를 처리하는데 항상 클로저를 사용할 필요가 없다는 것을 보여준다. 실행될 작업의 수와 양을 미리 알고 있을 경우 완벽하게 동작하지만 그렇지 못할 경우 동적으로 구현해야함.

순차 반복

모든 링크를 추출한 다음 각각의 웹 스파이더를 재귀로 호출하여 순서대로 시작하도록 함. 웹 페이지의 링크를 하나씩 다운로드하도록 구현.

웹 스파이더 버전2

function spiderLinks(currentUrl, body, nesting, callback) {
if(nesting === 0) {
return process.nextTick(callback);
}
let links = utilities.getPageLinks(currentUrl, body); //[1]
function iterate(index) {
if(index === links.length) {
return callback();
}
spider(links[index], nesting - 1, function(err) {
if(err) {
return callback(err);
}
iterate(index + 1);
});
}
iterate(0);
}
function spider(url, nesting, callback) {
const filename = utilities.urlToFilename(url);
fs.readFile(filename, 'utf8', function(err, body) {
if(err) {
if(err.code !== 'ENOENT') {
return callback(err);
}
return download(url, filename, function(err, body) {
if(err) {
return callback(err);
}
spiderLinks(url, body, nesting, callback);
});
}
spiderLinks(url, body, nesting, callback);
});
}
view raw webSpider_v2.js hosted with ❤ by GitHub
spiderLinks() 함수와 같은 코드 패턴은 비동기 순차적으로 반복해야 하는 경우에 사용할 수 있다.
function iterate(index) {
if(index === tasks.length) {
return finish();
}
const task = tasks[index];
task(function() {
iterate(index + 1);
});
}
function finish() {
// 반복 작업이 완료된 후 처리
}
iterate(0);
view raw iterate.js hosted with ❤ by GitHub
위 패턴은 배열의 값들을 맵핑하거나 반복문에서 연산의 결과를 다음 반복에 전달하여 reduce 알고리즘을 구현할 수 있다.

패턴(순차 반복자) iterator는 컬렉션에서 다음에 사용 가능한 task를 호출하고 현재 task가 완료될 때 다음 단계를 호출하도록 한다.

병렬 실행

비동기 작업들의 실행 순서가 중요하지 않고 일련의 작업들의 모든 실행이 끝났을 때 알림을 받고자 한다면 병렬로 처리하는 방법이 좋다. Node.js는 멀티 스레드 처럼 작업들을 동시에 실행시킬 수는 없지만 논 블로킹 방식으로 하나의 스레드만 가지고도 병렬 실행처럼 하는 동시성을 가진다.

웹 스파이더 버전3

function spiderLinks(currentUrl, body, nesting, callback) {
if(nesting === 0) {
return process.nextTick(callback);
}
const links = utilities.getPageLinks(currentUrl, body); //[1]
if(links.length === 0) {
return process.nextTick(callback);
}
let completed = 0, hasErrors = false;
function done(err) {
if(err) {
hasErrors = true;
return callback(err);
}
if(++completed === links.length && !hasErrors) {
return callback();
}
}
links.forEach(function(link) {
spider(link, nesting - 1, done);
});
}
let spidering = new Map();
function spider(url, nesting, callback) {
if(spidering.has(url)) {
return process.nextTick(callback);
}
spidering.set(url, true);
const filename = utilities.urlToFilename(url);
fs.readFile(filename, 'utf8', function(err, body) {
if(err) {
if(err.code !== 'ENOENT') {
return callback(err);
}
return download(url, filename, function(err, body) {
if(err) {
return callback(err);
}
spiderLinks(url, body, nesting, callback);
});
}
spiderLinks(url, body, nesting, callback);
});
}
view raw webSpider_v3.js hosted with ❤ by GitHub
패턴(무제한 병렬 실행) 한 번에 모든 항목을 생성하여 일련의 비동기 작업들을 병렬로 실행한 다음, 콜백이 호출된 횟수를 계산하여 모든 작업이 완료되기를 기다린다.

동시 작업에서의 경쟁 조건 조정

Node.js에서는 모든 것이 단일 스레드에서 실행되기 때문에 일반적으로 동기화 매커니즘이 필요하지 않다. 그러나 문제는 비동기 작업 호출과 그 결과 통지 사이에 생기는 지연이다. 예로 파일을 읽어 들이는데 완료 되기 전에는 이를 확인하지 않으면 같은 파일을 읽어 들여 중복 작업을 하거나 데이터 손상으로 이어질 수 있으며 일시적인 특성으로 디버그 하기가 어렵다. 경쟁 상황은 병렬로 실행할 때 유형별 상황을 명확히 확인하는 것이 중요하다.

제한된 병렬 실행

많은 양의 데이터를 처리할 때 동시에 처리하는 수를 제어하지 않고 병렬 작업을 생성하면 과도한 부하가 발생할 수 있다. 따라서 동시에 실행할 수 있는 작업의 수를 제어하는 것이 좋다. 그러면 서버의 부하에 대한 예측성을 가질 수 있으며 어플리케이션의 리소스가 부족하지 않도록 할 수 있다.

전역적으로 동시실행 제한하기

Node.js 0.11이전 버전은 호스트 당 동시 HTTP 연결 수를 5로 제한함. 0.11부터는 동시 연결 수에 대한 기본 제한 없음.

큐를 사용한 해결

module.exports = class TaskQueue {
constructor (concurrency) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
pushTask (task) {
this.queue.push(task);
this.next();
}
next() {
while (this.running < this.concurrency && this.queue.length) {
const task = this.queue.shift();
task (() => {
this.running--;
this.next();
});
this.running++;
}
}
};
view raw taskQueue.js hosted with ❤ by GitHub

웹 스파이더 버전4

const TaskQueue = require('./taskQueue');
let downloadQueue = new TaskQueue(2);
function spiderLinks(currentUrl, body, nesting, callback) {
if(nesting === 0) {
return process.nextTick(callback);
}
const links = utilities.getPageLinks(currentUrl, body);
if(links.length === 0) {
return process.nextTick(callback);
}
let completed = 0, hasErrors = false;
links.forEach(link => {
downloadQueue.pushTask(done => {
spider(link, nesting - 1, err => {
if(err) {
hasErrors = true;
return callback(err);
}
if(++completed === links.length && !hasErrors) {
callback();
}
done();
});
});
});
}
view raw webSpider_v4.js hosted with ❤ by GitHub


Vana Yun

Written by Vana Yun