kakasoo

Express 내부 코드 들여보기 본문

프로그래밍/Backend

Express 내부 코드 들여보기

카카수(kakasoo) 2022. 10. 2. 18:29
반응형

대학교 기술 세미나에서 발표할 일이 있을 때, 선후배 동문들을 상대로 발표한 적이 있는데, 그 때 코드를 최근 공유할 일이 생겼다.

그래서 블로그에도 올려서, 누구나 볼 수 있게 한다. ( Repository에도 올렸다. )

지금 가르치는 학생분들을 포함해, Node.js에서 기본적인 서버 구현이 가능한 사람이라면 충분히 배워볼 만 하다.

 

STEP 0. Express 코드

const express = require("express");
const http = require("http");
const path = require("path");

const app = express();

app.use(express.static("public"));

const server = http.createServer(app);

app.get("/", (req, res) => {
    res.render("index", { title: "test" });
});

app.post("/login", (req, res) => {
    res.send({ data: "test" });
});

server.listen(3000, () => console.log("server opened."));

 

기본적인 Express 코드는 위와 같이 작성된다.

서버 코드는 대개 프레임워크와, 그 서버 프레임워크를 이용해 작성한 비즈니스 로직 ( 여기서는 서비스보다 넓은 의미 ) 으로 나뉜다.

우리는 보통 프레임워크 내부의 동작에는 관심을 두지 않지만, Node.js와 같이 프레임워크가 계속 발전 중이라면 공부는 필연이 된다.

 

STEP 1. Express 없는 서버 구현

/**
 * 1. node.js에서 서버를 여는 법
 */

const http = require("http");
const server = http.createServer(KAKASOO); // ERROR : KAKASOO is not defined.
server.listen(3000);

 

Node.js는 사실, 정확히는 언어가 아니라 브라우저를 렌더링하기 위한 엔진이다.

그렇기 때문에 core module만 가지고도 자체적으로 서버를 열 수가 있는데, 이 때 필요한 라이브러리가 바로 http다.

http는 createServer 라는, 기존에 구현되어 있는 라이브러리만 가지고도 서버를 열 수 있는데,

이 메서드는 다시 Req와 Res를 받아 처리하는 함수를 받는다.

이 함수를 우리는 'handler' 라고 부를 것이다.

 

    /**
     * Returns a new instance of {@link Server}.
     *
     * The `requestListener` is a function which is automatically
     * added to the `'request'` event.
     * @since v0.1.13
     */
    function createServer(requestListener?: RequestListener): Server;

 

type RequestListener = (req: IncomingMessage, res: ServerResponse) => void;

 

Node.js가 서버를 열기 위한 엔진인 만큼, 그 구현체는 0.1.13 부터 존재한다.

createServer는 requestListener라는 함수를 받는데, 그 정의는 위에 말했다시피 Req, Res를 처리, 정확히는 제어하는 함수이다.

또한 이 리스너라는 표현을 통해, 우리는 서버가 거대한 EventEmitter라는 사실을 추론해볼 수 있을 것이다.

반환 값이 Server 그 자체라는 것도 충분히 재밌다.

이 단계는 서버가 어떻게 작성되는지를 볼 뿐이지, 현재로서는 `KAKASOO` 라는 handler가 정의되지 않은 이상 동작하지 않는다.

 

STEP 2. Express 서버 동작을 대신할 KAKASOO 구현

/**
 * 2. KAKASOO 정의
 */
const http = require("http");

/**
 *
 * @param {http.IncomingMessage} req
 * @param {http.Server Response} res
 * @returns void
 */
const KAKASOO = (req, res) => {
    res.setHeader("Content-Type", "text/plain");
    res.end("dogs");
};

const server = http.createServer(KAKASOO);
server.listen(3000, () => console.log("server listen."));

 

카카수를 정의하면 다음과 같이 된다.

먼저 반환할 컨텐츠의 타입을 명시해주고, 응답을 내려주면 된다.

이렇게 정의한 서버는 항상 "dogs" 라는 응답밖에 주지 못하기 때문에, 서버는 더 다채로운 응답을 주기 위해 확장될 필요가 있다.

여기서 응답을, 분기 처리를 통해서 여러 개로 늘린다고 가정하면 STEP 3이 된다.

 

STEP 3. URL 분기에 따른 결과가 나오는 서버

/**
 * 3. URL 경로에 따라 다르게 동작하는 서버
 */
const http = require("http");

/**
 *
 * @param {http.IncomingMessage} req
 * @param {http.ServerResponse} res
 * @returns void
 */
const KAKASOO = (req, res) => {
    res.setHeader("Content-Type", "text/plain");

    // console.log(req);

    if (req.url === "/") {
        return res.end("/");
    }

    if (req.url === "/cats") {
        return res.end("/cats");
    }

    if (req.url === "/dogs") {
        return res.end("/dogs");
    }
};

const server = http.createServer(KAKASOO);
server.listen(3000, () => console.log("server listen."));

 

이제 URL에 따라서 분기처리된 결과를 볼 수 있게 된다.

하지만 여기서도, 서버 개발자가 복잡한 if문들을, 하나의 파일에서 구현해야 한다는 문제가 있다.

이 상태에서는 개발 협업은 커녕 혼자 개발하는 것도 복잡하다.

이 단계는 다르게 말하면, 아직 서버 프레임워크와, 서버 개발자가 작성해나갈 비즈니스 로직이 분리되지 않은 단계라고 할 수 있다.

분리를 위해서는 조금 더 서버 프레임워크를 고도화할 필요가 있다.

 

STEP 4. KAKASOO를 어플리케이션을 생성하는 함수라고 생각해보기

/**
 * 4. KAKASOO app을 생성하는 함수로 변경
 */

const http = require("http");

/**
 *
 * @returns Application is created by KAKASOO
 */
const KAKASOO = () => {
    return new (class App {
        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        someFunc(req, res) {}
    })();
};

const app = KAKASOO();
const server = http.createServer((req, res) => app.someFunc(req, res));
server.listen(3000, () => console.log("server listen."));

 

고도화를 위해, 아직 돌아가지 않을 거라는 사실을 알지만, 일보 후퇴한다.

KAKASOO를 그냥 요청과 응답을 처리하는 함수에서, 요청과 응답을 처리하는 어떤 '어플리케이션'을 반환하는 거라 생각해보자.

다음과 같이 실행과 동시에 Application instance를 반환해야 할 것이며, 어떤 함수를 하나 지니고 있을 것이다.

앞으로 서버의 모든 실행은 이 someFunc(req, res) 를 통해 이루어질 거라고 생각해볼 수 있겠다.

실행이 이루어지는 부분은 구현했으니, 이제 서버를 구현하기 위한 구현체를 작성해야 한다.

 

STEP 5. 어플리케이션의 handle 부분 작성하기

/**
 * 5. handler를 호출하는 handle 함수 만들기
 */

const http = require("http");

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.layer = {}; // 5-3
            console.log("App created.");
        }

        // NOTE : 실행을 위한 함수
        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            const url = req.url;
            this.layer[url](req, res); // 저장된 handler를 찾아서 호출한다.
        }
    })();
};

const app = KAKASOO();
const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

 

아까와 같이 동작하게 하기 위해서는, 이제 이 someFunc(req, res) 내부를 구현해줄 필요가 있는데,

이 함수는 이제 실행 부를 담당할 것인 즉, URL을 받아, 어떤 객체로부터 그 URL을 키로 하는 어떤 값 ( = 함수)를 호출해야 할 것이다.

이 때 호출되는 어떤 함수가 handler라는 것은 이미 모두 동의할 것이다.

동작 방식을 볼 때 이 함수가 handle라고 명명될 수 있다는 건 위 코드를 봤다면, 마찬가지로 모두 동의할 수 있을 것이다.

 

여기서 Layer라는 개념이 등장하는데, Layer는 계층을 의미한다.

controller, service 등도 물론 계층이지만, 조금 더 로우한 레벨에서도 Layer 라는 표현을 사용하고 있다.

어쩌면 이게 Express의 내부 코드를 ES6로 재구성한 것이니 만큼, Layer라는 개념의 시작은 여기일지도 모른다.

 

내부 코드를 보니 서버 프레임워크와 서버 구현에 대한 인사이트들이 정말 많지 않나?

 

STEP 6. 서버를 구현하기 위한 메서드 제공 / GET 메서드에 대한 handler 정의 함수 구현

/**
 * 6. handler를 저장하는 get 함수 만들기 ( if으로 작성된 로직 대체하기 )
 */

const http = require("http");

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.layer = {};
            console.log("App created.");
        }

        get(url, handler) {
            this.layer[url] = handler; // Application의 layer에 url 경로를 key 값으로 handler 저장
        }

        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            const url = req.url;
            this.layer[url](req, res); // 저장된 handler를 찾아서 호출한다.
        }
    })();
};

const app = KAKASOO();
const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

 

이제 get 이라는 메서드를 통해서 application은 레이어에 url을 키로 하는 handler 함수를 저장할 수 있게 되었다.

여기서부터는 이제 프레임워크라는 말을 쓸 수 있다.

프레임워크와 라이브러리의 차이는, 프레임워크는 개발자로 하여금 자신의 문법을 따르게끔 강제한다는 점이다.

많은 라이브러리들이 개발자의 코드에 얽혀 기능을 개발하게끔 도움을 주지만, 전체 코드의 컨벤션을 바꿔 버리지는 않는다.

하지만 이제 이 KAKASOO 함수는 무조건 get 메서드를 통해서만 GET API를 만들 수 있게 강제할 것이다.

사실 이 한 개로는 강제라고 하기도 애매하지만.

 

STEP 7. handler가 없을 경우, 404 내보내기

/**
 * 7. application 메서드를 통한 내부 로직 재정의
 */

const http = require("http");

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.layer = {};
            console.log("App created.");
        }

        get(url, handler) {
            this.layer[url] = handler; // Application의 layer에 url 경로를 key 값으로 handler 저장
        }

        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            const url = req.url;
            if (!this.layer[url]) {
                console.log(`${url} 경로에 저장된 handler가 없습니다.`);
                return;
            }
            this.layer[url](req, res);
        }
    })();
};

const app = KAKASOO();

app.get("/", (req, res) => res.end("hi"));

const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

// 7-1. 우리가 아는 Express의 모습이 생성된다.

 

handle을 수정하여, 정해진 handler가 없는 경우 콘솔을 출력하도록 수정했다.

해당 부분을 어떻게 수정하느냐에 따라, 기본적인 index.html을 찾도록 할 수도 있을 것이고, 404를 반환할 수도 있을 것이다.

이번에 가르치는 학생 중에, 왜 자동으로 index.html으로 가는지에 대한 질문을 한 친구가 있어서, 이러한 설명을 추가해봤다.

이러한 구현은, 사실 Express 뿐만 아니라, 웹 서버 어플리케이션 전체에 대한 약속이다.

 

STEP 8. 프레임워크 활용

/**
 * 8. App의 get method를 이용한 handler 정의
 */

const http = require("http");

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.layer = {};
            console.log("App created.");
        }

        get(url, handler) {
            this.layer[url] = handler; // Application의 layer에 url 경로를 key 값으로 handler 저장
        }

        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            const url = req.url;
            if (!this.layer[url]) {
                console.log(`${url} 경로에 저장된 handler가 없습니다.`);
                return;
            }
            this.layer[url](req, res);
        }
    })();
};

const app = KAKASOO();

app.get("/favicon.ico", (req, res) => res.end("favicon"));

app.get("/", (req, res) => res.end("hi! my name is app!"));
app.get("/dogs", (req, res) => res.end("bark!"));
app.get("/cats", (req, res) => res.end("meow~"));

const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

 

이제 프레임워크와 서버 코드가 분리된 것을 확인할 수 있다.

만약 KAKASOO 라는 프레임워크가 배포된다면, 개발자들은 내부 코드에 관심 가질 필요없이 서버를 구현할 수 있게 될 것이다.

 

STEP 9. POST 메서드에 대한 handler 정의 함수 구현

/**
 * 9. get 외의 메서드 ( 여기서는 post ) 구현하기
 */

const http = require("http");

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.layer = {};
            console.log("App created.");
        }

        get(url, handler) {
            if (!this.layer[url]) {
                this.layer[url] = {};
            }
            this.layer[url]["get"] = handler; // Application의 layer에 url 경로를 key 값으로 handler 저장
        }

        post(url, handler) {
            if (!this.layer[url]) {
                this.layer[url] = {};
            }
            this.layer[url]["post"] = handler; // Application의 layer에 url 경로를 key 값으로 handler 저장
        }

        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            const url = req.url;
            const method = req.method.toLowerCase(); // 소문자로 변경

            if (!this.layer[url][method]) {
                console.log(`${url} 경로에 저장된 handler가 없습니다.`);
                return;
            }
            this.layer[url][method](req, res);
        }
    })();
};

const app = KAKASOO();

app.get("/favicon.ico", (req, res) => res.end("favicon"));

app.get("/", (req, res) => res.end("hi! my name is app!"));
app.get("/dogs", (req, res) => res.end("bark!"));

app.get("/cats", (req, res) => res.end("meow~"));

const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

// 9-1. 보다시피 경로 상의 method만 달라질 뿐 handler 등록 메서드 (get, post)의 내부 로직은 동일하다.

 

메서드를 추가했다고는 하지만, 사실 모든 구현이 똑같다.

그렇다면 자연스럽게 STEP 10에 대한 아이디어가 생각날 것이다.

 

STEP 10. 모든 METHODS에 대한 handler 정의 함수 구현

/**
 * 10. 한꺼번에 모든 메서드 등록하기
 */

const http = require("http");
const { METHODS } = require("http");

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.layer = {};

            METHODS.forEach((METHOD) => {
                const method = METHOD.toLocaleLowerCase();

                this[method] = function (url, handler) {
                    if (!this.layer[method]) {
                        this.layer[method] = {};
                    }
                    this.layer[method][url] = handler;
                };
            });

            console.log("App created.");
        }

        // get(url, handler) {
        //     if (!this.layer[url]) {
        //         this.layer[url] = {};
        //     }
        //     this.layer[url]["get"] = handler; // Application의 layer에 url 경로를 key 값으로 handler 저장
        // }

        // post(url, handler) {
        //     if (!this.layer[url]) {
        //         this.layer[url] = {};
        //     }
        //     this.layer[url]["post"] = handler; // Application의 layer에 url 경로를 key 값으로 handler 저장
        // }

        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            const url = req.url;
            const method = req.method.toLowerCase();

            if (!this.layer[method][url]) {
                console.log(`${url} 경로에 저장된 handler가 없습니다.`);
                return;
            }
            this.layer[method][url](req, res);
        }
    })();
};

const app = KAKASOO();

app.get("/favicon.ico", (req, res) => res.end("favicon"));

app.get("/", (req, res) => res.end("hi! my name is app!"));
app.get("/dogs", (req, res) => res.end("bark!"));
app.get("/cats", (req, res) => res.end("meow~"));
app.post("/dogs", res.end("create dogs"));

const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

// 10-1. layer의 method와 url 순서를 바꿔줘서 매핑을 하고, forEach문으로 한꺼번에 handler 등록함수 생성

// 10-2. handle method에서도 method, url key 순서를 바꿔준다.

 

마찬가지로 http 라이브러리에서 제공되는 모든 메서드를 담은 배열을 이용해 한 번에 handler 정의 함수들을 구현했다.

이 배열에는 현재 쓰이지 않는 모든 메서드들이 담겨 있기 때문에, KAKASOO는 모던 프레임워크보다는 더 많은 메서드를 다룬다.

 

STEP 11. Router 구현

/**
 * 11. Router 정의하기 // 분기 처리를 담당하는 큰 갈래
 */

const http = require("http");
const { METHODS } = require("http");

class Router {
    constructor() {
        this.stack = [];
    }

    route(url) {
        const route = {};
        route[url] = {};
        this.stack.push(route);

        return route[url];
    }

    handle(req, res) {
        const method = req.method.toLocaleLowerCase();
        const url = req.url;

        /**
         * this.stack
         *
         * [
         *   { '/favicon.ico': { get: [Function (anonymous)] } },
         *   { '/': { get: [Function (anonymous)] } },
         *   { '/dogs': { get: [Function (anonymous)] } },
         *   { '/cats': { get: [Function (anonymous)] } }
         * ]
         */
        for (const route of this.stack) {
            if (route[url] && route[url][method]) {
                route[url][method](req, res);
                return;
            }
        }

        console.log(`${url} 경로에 저장된 handler가 없습니다.`);
    }
}

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.router = new Router();

            METHODS.forEach((METHOD) => {
                const method = METHOD.toLocaleLowerCase();

                this[method] = function (url, handler) {
                    // if (!this.layer[method]) {
                    //     this.layer[method] = {};
                    // }
                    // this.layer[method][url] = handler;

                    const route = this.router.route(url);
                    route[method] = handler;
                };
            });

            console.log("App created.");
        }

        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            // const url = req.url;
            // const method = req.method.toLowerCase();

            // if (!this.layer[method][url]) {
            //     console.log(`${url} 경로에 저장된 handler가 없습니다.`);
            //     return;
            // }

            // if (!this.router) {
            //     console.log(`${url} 경로에 저장된 handler가 없습니다.`);
            //     return;
            // }

            // this.layer[method][url](req, res);

            this.router.handle(req, res);
        }
    })();
};

const app = KAKASOO();

app.get("/favicon.ico", (req, res) => res.end("favicon"));

app.get("/", (req, res) => res.end("hi! my name is app!"));
app.get("/dogs", (req, res) => res.end("bark!"));
app.get("/cats", (req, res) => res.end("meow~"));

const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

/**
 *  Router의 stack에 저장된 Route(s) ( Routes === Layer )
 *
 *  { "/favicon.ico" : { get : [Function (anoymous)] } }
 *  { "/" : { get : [Function (anoymous)] } }
 *  { "/dogs" : { get : [Function (anoymous)] } }
 *  { "/cats" : { get : [Function (anoymous)] } }
 */

/**
 * Route(s)에 저장된 handler(s) ( handlers === Layer )
 *
 * { get : [Function (anoymous)] }
 */

// 11-1. 엄밀히 말해 Router 내부에 각 메서드 단위로 Layer가 존재하고, 그 이후 경로로 Route가 존재한다.

// 11-2. Router -> Layer -> Route // 여기서는 Router에서 바로 Route로 직행하게끔 작성되었다.

// 11-3. 더 정확히 말하면 Router는 Layer를 가지고 Layer는 Route들을 가지는데, Route는 다시 Layer를 가진다.
// 11-4. 즉, Layer는 한 경로마다 생성되는 node 이다.

 

나무의 줄기는 나뭇가지인가?

나는 이런 비유로 후배들에게 설명을 하는데, 후배들은 마치 내가 종교 지도자처럼 보였을지도 모르겠다.

라우터를 이어 붙여서 만들어진 서버, 그 서버 어플리케이션도 거대한 라우터일 것이다.

나뭇가지들을 이어 붙여 만든 거대한 줄기 역시 나뭇가지라고 부를 수 있을 것이고, 그 구분은 그저 언어학적인 걸지도 모르겠다.

 

서버는 라우터들의 집합으로 구성된다.

그렇기 때문에, 약간의 급발진일 수는 있지만 라우터를 정의한다.

어플리케이션은 handler 정의 함수만을 가지게 하고, 실질적인 분기 처리와 실행은 일단 라우터가 다루게 하는 것이다.

이 개념이 어려울 수 있겠다.

왜냐하면 이 개념은, Express의 실질적인 구현과 매우 가까이, 맞닿아 있기 때문이다.

 

참고로 이 라우터는 진짜 Express 라우터와는 약간 다르게 구현되어 있는데, 이는 URL 경로가 완전히 일치해야 동작하기 때문이다.

실제 Express는 하위 URL을 포함하는 포괄적인 URL이 있다면, 그 URL에 대해서 동작하게끔 되어 있다.

API를 작성하다 보면 분명 제대로 작성했음에도 Request가 다른 API로 흘러간 경험이 있을 것이다.

 

STEP 12. Layer & Route 정의하기

// /**
//  * 12. Layer & Route 정의하기
//  */

const http = require("http");
const { METHODS } = require("http");

class Route {
    constructor(url) {
        this.url = url;
    }
}

class Layer {
    constructor(url, route) {
        this.url = url;
        this.route = route;
    }
}

class Router {
    constructor() {
        this.stack = [];
    }

    route(url) {
        const route = new Route(url);
        // NOTE : 달라진 부분은 여기, route를 가리키기 위한 Layer를 stack에 쌓는 점이다.
        const layer = new Layer(url, route);
        this.stack.push(layer);

        return route;
    }

    handle(req, res) {
        const method = req.method.toLocaleLowerCase();
        const url = req.url;

        for (const layer of this.stack) {
            if (layer.url === url && layer.route[method]) {
                layer.route[method](req, res);
                return;
            }
        }

        console.log(`${url} 경로에 저장된 handler가 없습니다.`);
    }
}

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.router = new Router();

            METHODS.forEach((METHOD) => {
                const method = METHOD.toLocaleLowerCase();

                this[method] = function (url, handler) {
                    const route = this.router.route(url);
                    route[method] = handler;
                };
            });

            console.log("App created.");
        }

        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            this.router.handle(req, res);
        }
    })();
};

const app = KAKASOO();

app.get("/favicon.ico", (req, res) => res.end("favicon"));

app.get("/", (req, res) => res.end("hi! my name is app!"));
app.get("/dogs", (req, res) => res.end("bark!"));
app.get("/cats", (req, res) => res.end("meow~"));

const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

 

굉장히 초기 단계의 Layer, Route인데, 쉽게 말하면 URL의 분기 처리와 handler를 서로 다른 저장소로 나눈 것이다.

하지만 미들웨어 개념이 포함되기 전이기 때문에 이 저장소라는 개념이 아직 미흡하다.

 

STEP 13. Layer & Route 구현하기

/**
 * 13. Router -> Layer -> Route -> Layer 구조 만들기 ( Router는 path에 따른 분기, Route는 method에 따른 분기)
 */

const http = require("http");
const { METHODS } = require("http");

class Route {
    constructor(url) {
        this.url = url;
        this.stack = [];

        METHODS.forEach((METHOD) => {
            const method = METHOD.toLocaleLowerCase();

            this[method] = function (...handlers) {
                for (const handler of handlers) {
                    const layer = new Layer(method, null, handler);
                    this.stack.push(layer);
                }
            };
        });
    }
}

class Layer {
    constructor(url, route, method) {
        this.url = url;
        this.route = route;
        this.method = method;
    }

    handle(req, res) {
        this.method(req, res);
    }
}

class Router {
    constructor() {
        this.stack = [];
    }

    route(url) {
        const route = new Route(url);
        // NOTE : 달라진 부분은 여기, route를 가리키기 위한 Layer를 stack에 쌓는 점이다.
        const layer = new Layer(url, route);
        this.stack.push(layer);

        return route;
    }

    handle(req, res) {
        const method = req.method.toLocaleLowerCase();
        const url = req.url;

        for (const layer of this.stack) {
            if (layer.url === url) {
                for (const methodLayer of layer.route.stack) {
                    if (methodLayer.url === method) {
                        methodLayer.handle(req, res);
                        return;
                    }
                }
            }
        }

        console.log(`${url} 경로에 저장된 handler가 없습니다.`);
    }
}

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.router = new Router();

            METHODS.forEach((METHOD) => {
                const method = METHOD.toLocaleLowerCase();

                this[method] = function (url, handler) {
                    const route = this.router.route(url);
                    // route[method] = handler;
                    route[method](handler);
                };
            });

            console.log("App created.");
        }

        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            this.router.handle(req, res);
        }
    })();
};

const app = KAKASOO();

app.get("/favicon.ico", (req, res) => res.end("favicon"));

app.get("/", (req, res) => res.end("hi! my name is app!"));
app.get("/dogs", (req, res) => res.end("bark!"));
app.get("/cats", (req, res) => res.end("meow~"));

const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

 

Layer의 실질적인 함수 실행은 Route가 다룬다.

Route는 마치 리볼버 권총 ( 과연 이게 적절한 비유인가? ) 처럼 아직 정의되지 않은 HTTP Method에 대한 Route들도 지닌다.

서버 프레임워크는 이제 Route의 탄창을 채우는 거랑 유사해졌다.

 

STEP 14. 코드 다듬기

/**
 * 14. 코드 중간 점검 겸 리팩터링 / Route의 hasMethod와 handle을 작성하여 Router handle의 for문을 분리
 */

const http = require("http");
const { METHODS } = require("http");

class Route {
    constructor(path) {
        this.path = path;
        this.stack = [];
        this.methods = {};

        METHODS.forEach((METHOD) => {
            const method = METHOD.toLocaleLowerCase();

            this[method] = (...handlers) => {
                for (const handler of handlers) {
                    const layer = new Layer(method, null, handler);

                    this.methods[method] = true;
                    this.stack.push(layer);
                }
            };
        });
    }

    hasMethod(method) {
        return this.methods[method] ? true : false;
    }

    handle(req, res) {
        const method = req.method.toLocaleLowerCase();
        for (const layer of this.stack) {
            if (layer.path === method) {
                layer.handle(req, res);
            }
        }
    }
}

class Layer {
    constructor(path, route, method) {
        this.path = path;
        this.route = route;
        this.method = method;
    }

    handle(req, res) {
        this.method(req, res);
    }
}

class Router {
    constructor() {
        this.stack = [];
    }

    route(path) {
        const route = new Route(path);
        const layer = new Layer(path, route);
        this.stack.push(layer);

        return route;
    }

    handle(req, res) {
        const method = req.method.toLocaleLowerCase();
        const path = req.path;

        for (const layer of this.stack) {
            if (layer.path === path && layer.route.hasMethod(method)) {
                // for (const methodLayer of layer.route.stack) {
                //     if (methodLayer.path === method) {
                //         methodLayer.handle(req, res);
                //         return;
                //     }
                // }

                // NOTE : 동일한 경로에 동일한 메서드는 한 개만 존재할 수 있다.
                layer.route.handle(req, res);
                return;
            }
        }

        console.log(`${path} 경로에 저장된 handler가 없습니다.`);
    }
}

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.router = new Router();

            METHODS.forEach((METHOD) => {
                const method = METHOD.toLocaleLowerCase();

                this[method] = (path, handler) => {
                    const route = this.router.route(path);
                    route[method](handler);
                };
            });

            console.log("App created.");
        }

        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            this.router.handle(req, res);
        }
    })();
};

const app = KAKASOO();

app.get("/favicon.ico", (req, res) => res.end("favicon"));

app.get("/", (req, res) => res.end("hi! my name is app!"));
app.get("/dogs", (req, res) => res.end("bark!"));
app.get("/cats", (req, res) => res.end("meow~"));

const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

/**
 * Router와 Route의 차이는?
 *
 * 분기 기준이 무엇이냐의 차이가 있을 뿐, 둘의 구현은 거의 유사하다.
 *
 * Router도 서버인가?
 *
 * Router도 어떻게 보면 하나의 서버를 담당할 수 있고,
 * 하나의 서버도 다르게 생각하면 Router라, 서로 다른 서버를 합칠 수도 있겠다.
 */

// NOTE : 이제 분기 처리가 url만 있는 게 아니므로 모두 path 라는 식별자로 변경한다.

 

STEP 15. 미들웨어 동작

/**
 * 15. middleware는 어떻게 동작하는가?
 */

const http = require("http");
const { METHODS } = require("http");

class Route {
    constructor(path) {
        this.path = path;
        this.stack = [];
        this.methods = {};

        METHODS.forEach((METHOD) => {
            const method = METHOD.toLocaleLowerCase();

            /**
             * 사실 middleware가 동작할 수 있도록 미리 작성해두었다.
             * 함수를 배열로 받아서, 일치하는 경우 순서대로 모두 실행시키도록 한 것.
             */
            this[method] = (...handlers) => {
                for (const handler of handlers) {
                    const layer = new Layer(method, null, handler);

                    this.methods[method] = true;
                    this.stack.push(layer);
                }
            };
        });
    }

    hasMethod(method) {
        return this.methods[method] ? true : false;
    }

    handle(req, res) {
        const method = req.method.toLocaleLowerCase();
        for (const layer of this.stack) {
            if (layer.path === method) {
                layer.handle(req, res);
            }
        }
    }
}

class Layer {
    constructor(path, route, method) {
        this.path = path;
        this.route = route;
        this.method = method;
    }

    handle(req, res) {
        this.method(req, res);
    }
}

class Router {
    constructor() {
        this.stack = [];
    }

    route(path) {
        const route = new Route(path);
        const layer = new Layer(path, route);
        this.stack.push(layer);

        return route;
    }

    handle(req, res) {
        const method = req.method.toLocaleLowerCase();
        const path = req.path;

        for (const layer of this.stack) {
            if (layer.path === path && layer.route.hasMethod(method)) {
                layer.route.handle(req, res);
                return;
            }
        }

        console.log(`${path} 경로에 저장된 handler가 없습니다.`);
    }
}

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.router = new Router();

            METHODS.forEach((METHOD) => {
                const method = METHOD.toLocaleLowerCase();

                // NOTE : 여러 개의 함수를 받을 수 있도록 수정
                this[method] = (path, ...handlers) => {
                    const route = this.router.route(path);
                    route[method](handlers);
                };
            });

            console.log("App created.");
        }

        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            this.router.handle(req, res);
        }
    })();
};

const app = KAKASOO();

app.get("/favicon.ico", (req, res) => res.end("favicon"));

app.get(
    "/",
    (req, res) => console.log(123),
    (req, res) => res.end("hi! my name is app!")
);
app.get(
    "/dogs",
    (req, res) => console.log(123),
    (req, res) => res.end("bark!")
);
app.get("/cats", (req, res) => res.end("meow~"));

const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

 

미들웨어는 사실, 함수를 여러 개 받아 차례대로 실행할 뿐이다.

안타깝게도 KAKASKOO의 Application 실행 부분이 for await로 작성되어 있었더라면 좋았을 것을,

Express는 비동기를 제대로 제어하지 못하는 코드가 되었다.

사실 당시에는 async/await 라는 개념 자체가 존재하지 않았다.

 

STEP 16. 에러 캐치

/**
 * 16. 서버 로직 상의 에러를 처리하는 방법
 */

const http = require("http");
const { METHODS } = require("http");

class Route {
    constructor(url) {
        this.url = url;
        this.stack = [];
        this.methods = {};

        METHODS.forEach((METHOD) => {
            const method = METHOD.toLocaleLowerCase();

            /**
             * 사실 middleware가 동작할 수 있도록 미리 작성해두었다.
             * 함수를 배열로 받아서, 일치하는 경우 순서대로 모두 실행시키도록 한 것.
             */
            this[method] = (...handlers) => {
                for (const handler of handlers) {
                    const layer = new Layer(method, null, handler);

                    this.methods[method] = true;
                    this.stack.push(layer);
                }
            };
        });
    }

    hasMethod(method) {
        return this.methods[method] ? true : false;
    }

    handle(req, res) {
        const method = req.method.toLocaleLowerCase();
        for (const layer of this.stack) {
            if (layer.url === method) {
                layer.handle(req, res);
            }
        }
    }
}

class Layer {
    constructor(url, route, method) {
        this.url = url;
        this.route = route;
        this.method = method;
    }

    handle(req, res) {
        this.method(req, res);
    }
}

class Router {
    constructor() {
        this.stack = [];
    }

    route(url) {
        const route = new Route(url);
        const layer = new Layer(url, route);
        this.stack.push(layer);

        return route;
    }

    handle(req, res) {
        const method = req.method.toLocaleLowerCase();
        const url = req.url;

        for (const layer of this.stack) {
            if (layer.url === url && layer.route.hasMethod(method)) {
                layer.route.handle(req, res);
                return;
            }
        }

        throw new Error(`${url} 경로에 저장된 handler가 없습니다.`);
    }
}

const KAKASOO = () => {
    return new (class App {
        constructor() {
            this.router = new Router();

            METHODS.forEach((METHOD) => {
                const method = METHOD.toLocaleLowerCase();

                this[method] = (url, ...handlers) => {
                    const route = this.router.route(url);
                    route[method](handlers);
                };
            });

            console.log("App created.");
        }

        // NOTE : 서버의 모든 에러는 Application의 try/catch 문에서 잡히게 되어 있다.

        /**
         *
         * @param {http.IncomingMessage} req
         * @param {http.ServerResponse} res
         * @returns void
         */
        handle(req, res) {
            try {
                this.router.handle(req, res);
            } catch (err) {
                console.error("ERROR : ", err.message);
            }
        }
    })();
};

const app = KAKASOO();

app.get("/favicon.ico", (req, res) => res.end("favicon"));

app.get(
    "/",
    (req, res) => console.log(123),
    (req, res) => res.end("hi! my name is app!")
);
app.get(
    "/dogs",
    (req, res) => console.log(123),
    (req, res) => res.end("bark!")
);
app.get("/cats", (req, res) => res.end("meow~"));

const server = http.createServer((req, res) => app.handle(req, res));
server.listen(3000, () => console.log("server listen."));

 

어플리케이션에서의 전역적인 에러 캐치는, 그냥 하나의 try catch 문만 있으면 가능하다.

밥 먹어야 해서 설명을 마친다.

반응형