비동기 프로그래밍의 어려움
모듈화, 재사용성, 유지보수성 같은 특성을 희생시키다 보면 callback 중첩이 확산되어 코드가 엉망이 된다.
웹 스파이더
웹 URL을 입력 받아 URL의 내용을 로컬 파일로 다운로드 하는 기능을 구현해보자.
"use strict"; | |
const request = require('request'); | |
const fs = require('fs'); | |
const mkdirp = require('mkdirp'); | |
const path = require('path'); | |
const utilities = require('./utilities'); | |
function spider(url, callback) { | |
const filename = utilities.urlToFilename(url); | |
fs.exists(filename, exists => { //[1] | |
if(!exists) { | |
console.log(`Downloading ${url}`); | |
request(url, (err, response, body) => { //[2] | |
if(err) { | |
callback(err); | |
} else { | |
mkdirp(path.dirname(filename), err => { //[3] | |
if(err) { | |
callback(err); | |
} else { | |
fs.writeFile(filename, body, err => { //[4] | |
if(err) { | |
callback(err); | |
} else { | |
callback(null, filename, true); | |
} | |
}); | |
} | |
}); | |
} | |
}); | |
} else { | |
callback(null, filename, false); | |
} | |
}); | |
} | |
spider(process.argv[2], (err, filename, downloaded) => { | |
if(err) { | |
console.log(err); | |
} else if(downloaded){ | |
console.log(`Completed the download of "${filename}"`); | |
} else { | |
console.log(`"${filename}" was already downloaded`); | |
} | |
}); |
- fs.exists(filename, exists => … 파일이 존재하는지 체크
- request(url, (err, response, body) => … 파일이 없으면 URL 다운로드
- mkdirp(path.dirname(filename), err => … 그 다음 파일을 저장할 디렉토리가 있는지 확인
- fs.writeFile(filename, body, err => … HTTP 응답의 내용을 파일 시스템에 쓴다.
The Callback hell
앞서 정의한 spider() 함수는 여러 수준의 들여 쓰기로 인해 가독성이 떨어진다. 직접 스타일의 블로킹 API를 사용하여 유사한 기능을 구현하는 것이 더 간단할 수 있다. 많은 클로저와 내부 callback 정의가 많고 관리할 수 없을 정도로 된 상황을 콜백 헬 (Callback hell)이라고 한다. 심각한 안티패턴 중 하나이다.
안티패턴의 예
문제는 가독성이다. 추적하기가 어렵다. 또 다른 문제는 각 스코프에서 사용된 변수 이름의 중복이 발생한다는 점이다.(err등) 클로저가 성능 및 메모리 소비 면에서 비용이 적게 든다는 점을 명심해야 한다. 또한 활성 클로저가 참조하는 컨텍스트가 Garbage수집 시 유지되기 때문에 식별하기 어려운 메모리 누수가 발생할 수 있다.