[Node.js 마이크로서비스 코딩 공작소 #1] - 모놀리스 구현하기

마이크로서비스에 대해서 말로만 들어봤지 직접 구축해본 적이 없었기에, 이를 구현해보고자 했습니다.

블로그 등의 레퍼런스보다 책을 읽으며 구현하는 것이 더 자세히 배울 수 있을거라 판단하여 thebook의 'Node.js 마이크로서비스 코딩 공작소'라는 책을 읽으며 공부 중입니다.

 

node.js의 http 모듈을 사용하여 Row level에서부터 구현하도록 되어있으며, 책에서는 MariaDB를 사용하였으나 저는 PostgreSQL을 사용하여 구현하였습니다.

 

책에서 만들 프로젝트는 간단한 상품구매 서비스이며 아키텍처는 다음과 같습니다.

 

 

사용자에게 요청을 받아 회원 관리와 상품 관리, 구매 관리 비즈니스 로직을 처리하는 서비스이며, 하나의 애플리케이션 프로세스에서 이 3가지를 모두 구현하는 모놀리스 구조부터 구현할 예정입니다.

 

 

 

 

 

 

 

 

 

 

 

 

1. 데이터베이스 만들기

PostgreSQL을 GUI로 조작할 수 있는 PgAdmin4를 통해 데이터 베이스를 만들었습니다.

 

데이터베이스를 우클릭을 통해 Database를 만들기
Template을 template0로 해줘야 Locale 에러가 발생하지 않는다...!

 

members 테이블과 goods 테이블을 만들고, members와 goods 테이블 간의 연결을 담당하는 다리 역할을 하는 purchases 테이블을 만들도록 하겠습니다.

테이블 구조를 도식화하면 다음과 같습니다.

 

1) Members 테이블 만들기

CREATE TABLE IF NOT EXISTS goods (
  id SERIAL PRIMARY KEY,
  name VARCHAR(128) NOT NULL,
  category VARCHAR(128) NOT NULL,
  price INT NOT NULL,
  description TEXT NOT NULL
);

 

잘 만들어졌는지 확인하기

select * from goods;

 

 

2) Goods 테이블 만들기

CREATE TABLE IF NOT EXISTS members (
  id SERIAL PRIMARY KEY,
  username VARCHAR(128) NOT NULL UNIQUE,
  password VARCHAR(256) NOT NULL
);

 

잘 만들어졌는지 확인하기

select * from members

 

3) Purchases 테이블 만들기

CREATE TABLE IF NOT EXISTS purchases (
  id SERIAL PRIMARY KEY,
  userid INT NOT NULL REFERENCES members(id),
  goodsid INT NOT NULL REFERENCES goods(id),
  date TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);

 

잘 만들어졌는지 확인하기

select * from purchases;

 

2. 간단한 서버 애플리케이션 만들기

보일러플레이트를 통해 간단한 서버 애플리케이션을 만들었습니다. 

 

로직을 간결히 살펴보면 HTTP Method가 POST, PUT인지 아닌지에 따라 사용자가 보낸 데이터를 가공하는 부분에 차이가 있음을 알 수 있습니다.

// server.js
const http = require("http");
// URL와 쿼리스트링 모듈을 통해 URI 정보를 파싱할 수 있음.
const querystring = require("querystring");
const url = require("url");

const server = http
  .createServer((req, res) => {
    const method = req.method;
    const uri = url.parse(req.url, true);
    const pathname = uri.pathname; // REST API를 위한 URI 정보 추출

    if (method === "POST" || method === "PUT") {
      let body = "";
      req.on("data", (data) => {
        body += data;
      });
      req.on("end", () => {
        let params;
        if(req.headers['content-type'] === 'application/json') {
          params = JSON.parse(body);
        } else {
          params = querystring.parse(body);
        }
        onRequest(res, method, pathname, params);
      });
    } else {
      // GET과 Delete면 query 정보를 읽음.
      onRequest(res, method, pathname, uri.query);
    }
  })
  .listen(8080, () => {
    console.log("Server started on http://localhost:8080");
  });

function onRequest(res, method, pathname, params) {
  res.end("response!");
}

 

 

 

3. 각 서비스별로 모듈 처리하기

 

1) `onRequest` 함수 안에서 모듈처리하기

`onRequest` 함수 내부에서 api에 따라 요청 분기 처리를 하고, 각 모듈별로 따로 요청처리를 할 수 있도록 구현을 해보자.

// server.js 

const http = require("http");
// URL와 쿼리스트링 모듈을 통해 URI 정보를 파싱할 수 있음.
const querystring = require("querystring");
const url = require("url");

const members = require("./members/monolithic_members.cjs");
const goods = require("./goods/monolithic_goods.cjs");
const purchases = require("./purchases/monolithic_purchases.cjs");

/**
 * HTTP 서버를 생성하고 요청 처리
 */
const server = http
  .createServer((req, res) => {
    const method = req.method;
    const uri = url.parse(req.url, true);
    const pathname = uri.pathname; // REST API를 위한 URI 정보 추출

    if (method === "POST" || method === "PUT") {
      let body = "";
      req.on("data", (data) => {
        body += data;
      });
      req.on("end", () => {
        let params;
        if (req.headers["content-type"] === "application/json") {
          params = JSON.parse(body);
        } else {
          params = querystring.parse(body);
        }
        onRequest(res, method, pathname, params);
      });
    } else {
      // GET과 Delete면 query 정보를 읽음.
      onRequest(res, method, pathname, uri.query);
    }
  })
  .listen(8080, () => {
    console.log("Server started on http://localhost:8080");
  });

/**
 * 요청에 대한 회원 관리, 상품 관리 구매관리 모듈별로 분기
 * @param res response 객체
 * @param method 메서드
 * @param pathname URI
 * @param params 입력 파라미터
 */
function onRequest(res, method, pathname, params) {
  switch (pathname) {
    case "/members":
      members.onRequest(res, method, pathname, params, response);
      break;
    case "/goods":
      goods.onRequest(res, method, pathname, params, response);
      break;
    case "/purchases":
      purchases.onRequest(res, method, pathname, params, response);
      break;
    default:
      res.writeHead(404);
      return res.end(); // 정의되지 않은 요청에 404 에러 응답
  }
}

/**
 * HTTP 헤더에 JSON 형식으로 응답
 * @param {*} res : response 객체
 * @param {*} packet : 결과 파라미터
 */
function response(res, packet) {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify(packet));
}

 

2) `members` 서비스 비즈니스 로직 구현하기

 

PostgreSQL 데이터베이스에 연동하기 위해 `pg` 라이브러리를 설치하였습니다.

npm i pg

 

각 쿼리문을 통해 회원가입, 인증, 탈퇴 서비스를 구현하였습니다. 

 

이 때, 회원가입을 할 때, 비밀번호를 암호화하기 위해 `crypt` 함수를 사용합니다.
이를 사용하기 위해서는 pgAdmin4에서 다음과 같은 SQL문을 통해 익스텐션을 설치해야한다.

CREATE EXTENSION IF NOT EXISTS pgcrypto;

 

 

// members/monolithic_members.cjs

// DB 연동
const pg = require("pg");
const config = {
  user: 환경변수처리,
  host: "localhost",
  database: "monolith",
  password: 환경변수처리,
  type: "postgres",
  port: 5432,
}; 

exports.onRequest = function (res, method, pathname, params, cb) {
  switch (method) {
    case "POST":
      return register(method, pathname, params, (response) => {
        process.nextTick(cb, res, response);
      });
    case "GET":
      return inquiry(method, pathname, params, (response) => {
        process.nextTick(cb, res, response);
      });
    case "DELETE":
      return unregister(method, pathname, params, (response) => {
        process.nextTick(cb, res, response);
      });
    default:
      // 정의되지 않은 메서드인 경우 Null 리턴
      return process.nextTick(cb, res, null);
  }
};

function register(method, pathname, params, cb) {
  // REST API의 URI로부터 목록 조회
  const response = {
    key: "register",
    errorcode: 0,
    errormessage: "success",
  };
  if (params.username == null || params.password == null) {
    response.errorcode = 1;
    response.errormessage = "Invalid Parameters";
    cb(response);
  } else {
    const db = new pg.Client(config);
    const query =
      "INSERT INTO members(username, password) VALUES($1, crypt($2, gen_salt('bf')))";
    db.connect(); // DB 연결
    db.query(
      // query 실행
      query,
      [params.username, params.password],
      (error, results, fields) => {
        if (error) {
          response.errorcode = 1;
          response.errormessage = error;
        }
        console.log("results", results, "fields", fields, "error", error);
        cb(response);

        // 쿼리가 끝난 후에 연결 종료
        db.end((endError) => {
          if (endError) {
            console.error("Error disconnecting from the database:", endError);
          }
        });
      }
    );
  }
}

function inquiry(method, pathname, params, cb) {
  // REST API의 URI로부터 목록 조회
  const response = {
    key: "register",
    errorcode: 0,
    errormessage: "success",
  };

  if (params.username == null || params.password == null) {
    response.errorcode = 1;
    response.errormessage = "Invalid Parameters";
    cb(response);
  } else {
    const db = new pg.Client(config);
    const query =
      "SELECT id FROM members WHERE username = $1 AND password = crypt($2, password)";

    db.connect(); // DB 연결
    db.query(
      // query 실행
      query,
      [params.username, params.password],
      (error, results, fields) => {
        if (error || results.length === 0) {
          response.errorcode = 1;
          response.errormessage = error ? error : "Invalid";
        } else {
          response.userId = results.rows[0].id;
        }
        cb(response);

        // 쿼리가 끝난 후에 연결 종료
        db.end((endError) => {
          if (endError) {
            console.error("Error disconnecting from the database:", endError);
          }
        });
      }
    );
  }
}

function unregister(method, pathname, params, cb) {
  // REST API의 URI로부터 목록 조회
  const response = {
    key: "register",
    errorcode: 0,
    errormessage: "success",
  };
  if (params.username == null) {
    response.errorcode = 1;
    response.errormessage = "Invalid Parameters";
    cb(response);
  } else {
    const db = new pg.Client(config);
    const query = "delete from members where username = $1";
    console.log(params.username);
    db.connect(); // DB 연결
    db.query(
      // query 실행
      query,
      [params.username],
      (error, results, fields) => {
        if (error) {
          response.errorcode = 1;
          response.errormessage = error;
        }
        cb(response);

        // 쿼리가 끝난 후에 연결 종료
        db.end((endError) => {
          if (endError) {
            console.error("Error disconnecting from the database:", endError);
          }
        });
      }
    );
  }
}

 

3) `goods` 서비스 비즈니스 로직 구현하기

// DB 연동
const pg = require("pg");
const config = {
  user: 환경변수,
  host: "localhost",
  database: "monolith",
  password: 환경변수,
  type: "postgres",
  port: 5432,
}; // 숨김 처리하기!

/**
 * 상품관리의 각 기능 별로 분기
 * @param {*} res
 * @param {*} method
 * @param {*} pathname
 * @param {*} params
 * @param {*} cb
 * @returns
 */
exports.onRequest = function (res, method, pathname, params, cb) {
  switch (method) {
    case "POST":
      return register(method, pathname, params, (response) => {
        process.nextTick(cb, res, response);
      });
    case "GET":
      return inquiry(method, pathname, params, (response) => {
        process.nextTick(cb, res, response);
      });
    case "DELETE":
      return unregister(method, pathname, params, (response) => {
        process.nextTick(cb, res, response);
      });
    default:
      // 정의되지 않은 메서드인 경우 Null 리턴
      return process.nextTick(cb, res, null);
  }
};

function register(method, pathname, params, cb) {
  // REST API의 URI로부터 목록 조회
  const response = {
    key: "register",
    errorcode: 0,
    errormessage: "success",
  };
  if (
    params.name == null ||
    params.category == null ||
    params.price == null ||
    params.description == null
  ) {
    response.errorcode = 1;
    response.errormessage = "Invalid Parameters";
    cb(response);
  } else {
    const db = new pg.Client(config);
    // const query =  "insert into goods(name, category, price, description) values(?, ?, ?, ?)";
    const query =
      "insert into goods(name, category, price, description) values($1, $2, $3, $4)";
    db.connect(); // DB 연결
    db.query(
      // query 실행
      query,
      [params.name, params.category, params.price, params.description],
      (error, results, fields) => {
        if (error) {
          response.errorcode = 1;
          response.errormessage = error;
        }
        cb(response);

        // 쿼리가 끝난 후에 연결 종료
        db.end((endError) => {
          if (endError) {
            console.error("Error disconnecting from the database:", endError);
          }
        });
      }
    );
  }
}

function inquiry(method, pathname, params, cb) {
  // REST API의 URI로부터 목록 조회
  const response = {
    key: "register",
    errorcode: 0,
    errormessage: "success",
  };
  const db = new pg.Client(config);
  db.connect(); // DB 연결
  db.query(
    // query 실행
    "select * from goods",
    (error, results, fields) => {
      if (error || results.length === 0) {
        response.errorcode = 1;
        response.errormessage = error ? error : "no data";
      } else {
        response.results = results;
      }
      cb(response);

      // 쿼리가 끝난 후에 연결 종료
      db.end((endError) => {
        if (endError) {
          console.error("Error disconnecting from the database:", endError);
        }
      });
    }
  );
}

function unregister(method, pathname, params, cb) {
  // REST API의 URI로부터 목록 조회
  const response = {
    key: "register",
    errorcode: 0,
    errormessage: "success",
  };
  if (params.id == null) {
    response.errorcode = 1;
    response.errormessage = "Invalid Parameters";
    cb(response);
  } else {
    const db = new pg.Client(config);
    db.connect(); // DB 연결
    db.query(
      // query 실행
      "delete from goods where id = $1",
      [params.id],
      (error, results, fields) => {
        if (error) {
          response.errorcode = 1;
          response.errormessage = error;
        }
        cb(response);

        // 쿼리가 끝난 후에 연결 종료
        db.end((endError) => {
          if (endError) {
            console.error("Error disconnecting from the database:", endError);
          }
        });
      }
    );
  }
}

 

4) `purchases` 서비스 비즈니스 로직 구현하기

// DB 연동
const pg = require("pg");
const config = {
  user: "postgres",
  host: "localhost",
  database: "monolith",
  password: "postgres",
  type: "postgres",
  port: 5432,
}; // 숨김 처리하기!

exports.onRequest = function (res, method, pathname, params, cb) {
  switch (method) {
    case "POST":
      return register(method, pathname, params, (response) => {
        process.nextTick(cb, res, response);
      });
    case "GET":
      return inquiry(method, pathname, params, (response) => {
        process.nextTick(cb, res, response);
      });
    default:
      // 정의되지 않은 메서드인 경우 Null 리턴
      return process.nextTick(cb, res, null);
  }
};

function register(method, pathname, params, cb) {
  // REST API의 URI로부터 목록 조회
  const response = {
    key: "register",
    errorcode: 0,
    errormessage: "success",
  };
  if (params.userid == null || params.goodsid == null) {
    response.errorcode = 1;
    response.errormessage = "Invalid Parameters";
    cb(response);
  } else {
    const db = new pg.Client(config);
    const query = "insert into purchases(userid, goodsid) values($1, $2)";
    db.connect(); // DB 연결
    db.query(
      // query 실행
      query,
      [params.userid, params.goodsid],
      (error, results, fields) => {
        if (error) {
          response.errorcode = 1;
          response.errormessage = error;
        }
        cb(response);

        // 쿼리가 끝난 후에 연결 종료
        db.end((endError) => {
          if (endError) {
            console.error("Error disconnecting from the database:", endError);
          }
        });
      }
    );
  }
}

function inquiry(method, pathname, params, cb) {
  // REST API의 URI로부터 목록 조회
  const response = {
    key: "register",
    errorcode: 0,
    errormessage: "success",
  };
  if (params.userid == null) {
    response.errorcode = 1;
    response.errormessage = "Invalid Parameters";
    cb(response);
  } else {
    const db = new pg.Client(config);
    const query = "select id, goodsid, date from purchases where userid = $1";
    db.connect(); // DB 연결
    db.query(
      // query 실행
      query,
      [params.userid],
      (error, results, fields) => {
        if (error || results.length === 0) {
          response.errorcode = 1;
          response.errormessage = error ? error : "no data";
        } else {
          response.results = results;
        }
        cb(response);

        // 쿼리가 끝난 후에 연결 종료
        db.end((endError) => {
          if (endError) {
            console.error("Error disconnecting from the database:", endError);
          }
        });
      }
    );
  }
}

 

 

Postman으로 테스트해본 결과 잘 되는 것을 확인할 수 있었습니다. 

다만, `http` 모듈로 구현하는 것이 너무 오랜만이라 귀찮긴 했던 것 같습니다.