콜백 규칙
비동기 코드를 작성할 때 명심해야 할 규칙
- 콜백을 정의할 때 함부로 클로저를 사용하지 않는 것.
- 모듈화 및 재사용을 고려해야 한다.
코드를 개선하는데 도움이 되는 기본 원칙
- 가능한 빨리 종료한다. return, continue, break 사용으로 중첩문을 대신하여 현재 문을 빠져 나가도록 한다. 코드를 얕게 유지.
- 콜백을 위해 명명된 함수를 생성하여 클로저 바깥에 배치하며 중간 결과를 인자로 전달한다.
- 코드를 모듈화한다. 작고 재사용 가능한 함수들로 분할한다.
콜백 규칙 적용
else문 제거
iferr;else// 오류가 없을 때 실행할 코드iferrreturn ;// 오류가 없을 때 실행할 코드
콜백이 호출된 후에 함수를 종료하는 것을 잊는 것에 주의!
이전 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); | |
}) | |
}); | |
} |
순차 실행
순차적으로 실행한다는 것은 한 번에 하나씩 실행한다는 것을 의미한다. 실행 순서가 중요하다. 이 흐름에는 다음과 같은 변형이 있다.
- 결과를 전달하거나 전파하지 않고 일련의 알려진 작업을 순서대로 실행.
- 작업의 출력을 다음 작업의 입력으로 사용.(체인, 파이프라인, 폭포수)
- 순차적으로 각 요소에 대해 비동기 작업을 실행하면서 일련의 작업들을 반복.
알려진 일련의 작업에 대한 순차 실행
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); | |
}); | |
} |
function iterate(index) { | |
if(index === tasks.length) { | |
return finish(); | |
} | |
const task = tasks[index]; | |
task(function() { | |
iterate(index + 1); | |
}); | |
} | |
function finish() { | |
// 반복 작업이 완료된 후 처리 | |
} | |
iterate(0); |
패턴(순차 반복자) 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); | |
}); | |
} |
동시 작업에서의 경쟁 조건 조정
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++; | |
} | |
} | |
}; |
웹 스파이더 버전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(); | |
}); | |
}); | |
}); | |
} |