본문 바로가기

Back-end/Node.js

콜백(Callback)의 비동기 Control flow 패턴 (1) - 콜백 지옥(Callback Hell)

* 이 글은 Mario Casciaro, Luciano Mammino가 저서한 <Node.js 디자인 패턴 바이블> 서적을 참고한 게시글입니다.

 

서적 정보 : http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788931464283

 

Node.js 디자인 패턴 바이블 - 교보문고

검증된 패턴과 기술을 이용한 수준 높은 Node.js | 이 책은 이미 Node.js를 처음 접한 후 이제 생산성, 디자인 품질 및 확장성 측면에서 최대한 활용하고자 하는 개발자를 대상으로 합니다. 이 책은

www.kyobobook.co.kr

 


 

콜백의 디자인 패턴에 대해 공부하면서 정리해보았다.

 

콜백 지옥부터 병렬실행, TaskQueue 적용 정도까지 살펴볼까 한다.

 

 

 

 

 

0.  Main code : Web Spider (v0)

 

이번 콜백 패턴과 여정을 함께할 코드를 작성해보았다. 

 

주어진 url의 내용을 로컬 파일로 다운로드하는 간단한 웹 스파이더를 작성해보았다.

 

// spider.mjs
import fs from "fs";
import path from "path";

// 웹 스파이더에서 사용할 패키지
import superagent from "superagent";

// url을 file 프로토콜에 맞게 변경해서 반환하는 함수
const urlToFilename = (url) => {
    const filename = url.split("/").pop();
    return filename.split(".")[1] + "/" + filename;
};

// 웹 스파이더
export const spider = (url, callback) => {
    const filename = urlToFilename(url);

    fs.access(filename, (err) => {
        // 만약 파일이 존재하지 않으면 다운로드한 적이 없으므로 다운로드를 수행한다.
        if (err && err.code === "ENOENT") {
            console.log(`Downloading ${url} into ${filename}`);
            superagent.get(url).end((err, res) => {
                if (err) {
                    callback(err);
                } else {
                    // 도메인에 맞는 폴더 생성
                    fs.mkdir(path.dirname(filename), (err) => {
                        if (err) {
                            callback(err);
                        } else {
                            // 파일 생성
                            fs.writeFile(filename, res.text, (err) => {
                                if (err) {
                                    callback(err);
                                } else {
                                    callback(null, filename, true);
                                }
                            });
                        }
                    });
                }
            });
        } else {
            callback(null, filename, false);
        }
    });
};

 

우선 url은 www.google.com 같이 단순한 도메인만 받는 것을 가정하고 구현했다. 

 

일단 벌써부터 보기가 힘들지만... 차근차근 해결해 나가는 것으로 하자.

 

그리고 이 spider함수를 사용할 간단한 cli도 구현했다.

 

import { spider } from "../spider.mjs";

spider(process.argv[2], (err, filename, downloaded) => {
    if (err) {
        console.error(err);
    } else if (downloaded) {
        console.log(`Completed the download of "${filename}"`);
    } else {
        console.log(`"${filename}" was already downloaded`);
    }
});

 

커멘드는 다음과 같이 사용하면 된다.

 

node --experimental-modules spider-cli.mjs http://www.google.com

 

 

시작해보자.

 

 

 

 

1.  콜백 지옥(Callback Hell)과 해소

 

이젠 너무나도 친숙한 단어이고, 심지어 요즘은 콜백을 디자인 패턴으로 사용하는 경우가 그리 많은 것 같지는 않다.

 

많은 클로저(Closure)와 in-place 콜백 정의가 코드의 가독성을 떨어뜨리고 코드 관리를 힘들게 하는 상황을 콜백 지옥(Callback Hell)이라고 하는데, 앞서 보여준 웹 스파이더 (v0)의 경우가 그러하다.

 

뭐 3 depth 정도야.. 눈감고 넘어가 줄 수는 있지만, 다른 기능이 추가될 수록 같은 패턴으로 작성하면 점점 지옥만 될 뿐이다.

 

우선 콜백 지옥을 해소하는 방법은 여러가지가 있다고(?) 하는데, 먼저 Return-Early-Pattern을 적용해보자.

 

 

Return-Early-Pattern은 사실 대단한 것이 아니다.

 

우리의 웹 스파이더(v0)의 코드를 살펴보면, 대부분 함수 스코프로 들어서서 다음과 같은 패턴을 가지고 있다.

 

if (err) {
    callback(err);
} else {
    // do something
}

 

여기에 Return-Early-Pattern을 씌워보면,

if (err) {
    return callback(err);
}

// do something

 

짧은 코드에서는 큰 변화가 없어보이지만, depth가 깊어질 수록 효과가 큰 패턴이다.

 

이 패턴에 대한 굉장히 자세한 설명이 논문 형식으로 나와있는 글이 있어 첨부해본다. (영어)

 

잘 쓰여져 있어 읽어볼 만 한 것 같다. 장/단점에 대해 모두 자세히 설명해주고 있다.

 

https://medium.com/swlh/return-early-pattern-3d18a41bba8 

 

Return Early Pattern

A rule that will make your code more readable.

medium.com

 

 

두 번째 해소 방법은 함수화이다. 너무 뻔한 방법이지만, 너무 중요한 습관이라고 생각한다.

 

우리의 웹 스파이더는 saveFile()과 download()라는 함수로 쪼개져서 다시 탄생할 것이다.

 

// saveFile
const saveFile = (filename, contents, callback) => {
    fs.mkdir(path.dirname(filename), (err) => {
        if (err) {
            return callback(err);
        }

        fs.writeFile(filename, res.text, callback);
    });
};
// download
const download = (url, filename, callback) => {
    console.log(`Downloading ${url} into ${filename}`);
    superagent.get(url).end((err, res) => {
        if (err) {
            return callback(err);
        }

        saveFile(filename, res.text, (err) => {
            if (err) {
                return callback(err);
            }

            console.log(`Downloaded and saved: ${url}`);
            callback(null, res.text);
        });
    });
};

 

기능엔 아무런 변화가 없다. 

 

확실히 해소를 하니 훨씬 깔끔해보인다.

 

마지막으로 spider도 바꿔보면,

export const spider = (url, callback) => {
    const filename = urlToFilename(url);

    fs.access(filename, (err) => {
        if (!err || err.code !== "ENOENT") {
            return callback(null, filename, false);
        }

        download(url, filename, (err) => {
            if (err) {
                return callback(err);
            }

            callback(null, filename, true);
        });
    });
};

 

심지어 더 좋아진 것은, saveFile()과 download() 역시 외부로 노출시킨 후 다른 곳에서 재사용할 수 있는 장점이 생겼다는 것이다.

 

이제 우리의 웹스파이더는 버전 1이 되었고, 리팩토링은 이쯤에서 끝내도록 하겠다.

 

다음엔 콜백의 비동기 control flow 중 순차 실행과 더불어 우리의 웹 스파이더를 업그레이드 해 보겠다.