서버 운영에 있어서 성능과 안정성은 매우 중요한 요소들 중 하나입니다. 이 글에선 Express 서버를 이용할 때 여러 워커를 클러스터 구성을 이용해 서비스하고 NGINX를 이용해 이중화 구성을 하는 방법에 대해 알아보도록 합니다.

 

 

 

0. 사전 준비.

 

이 글에서 웹서버는 React와 Express를 사용해 배포한 서버를 사용할 예정입니다. 실제 배포는  Docker를 사용해 도커 이미지를 생성한 후 실행시키도록 할 예정입니다. 미리 NPM와 Docker를 준비합시다.

마지막으로 모든 작업 결과물을 한번에 구동시키기 위해 Docker compose를 사용할 예정이니 Docker compose도 준비해 주세요.

 

 

 

1. NodeJS

 

 

NodeJS는 구글이 구글 크롬에 사용하려고 제작한 V8 오픈소스 자바스크립트 엔진을 기반으로 제작된 자바스크립트 런타임입니다. NodeJS는 다음과 같은 특징이 있습니다.

  1. 단일 스레드.
  2. 비동기 방식.
  3. 이벤트 루프를 사용.
  4. NPM.

NodeJS는 싱글 스레드이기 때문에 하나의 CPU를 여럿이 나눠 갖는 건 비효율적입니다. 따라서 CPU 숫자에 맞춰서 서버를 띄워보겠습니다.

 

 

 

2. 웹서버 생성.

 

 

우선 프론트를 준비합니다. 다음 명령어로 react app을 생성해 주세요.

 

$ npx create-reac-app my-react

 

이 앱을 Express 서버로 서비스할 예정입니다. 폴더로 들어가서 다음 패키지를 설치하세요.

 

$ cd my-react

 

$ npm i express express-favicon 

 

이제 express 서버를 실행할 코드를 작성해야 합니다. server 폴더에 index.js파일을 만드신 후 다음과 같이 코딩해 주세요.

 

//// ./server/index.js

const express = require('express');
const favicon = require('express-favicon');
const path = require('path');

const app = express();
const port = 3000;
const server = app.listen(port, () => {
    console.log(`Server is listening on port ${server.address().port}.`);
});

app.use(favicon(path.join(__dirname, '../public/favicon.ico')));
app.use(express.static(__dirname));
app.use(express.static(path.join(__dirname, '../build')));

app.get('/ping', (req, res) => {
    return res.send('pong');
});
app.get('/*', (req, res) => {
    return res.sendFile(path.join(__dirname, '../build', 'index.html'));
});

 

이제 기본으로 생성된 react를 빌드해 줍시다.

 

$ npm run build

 

여기까지 수행하셨으면 폴더 구조가 다음과 같을 겁니다.

 

 

우리가 작성한 express서버가 react app을 잘 서빙하는지 확인해 봅시다. localhost:3000으로 이동해 react 앱을 확인해 보세요. 그리고 localhost:3000/ping으로 이동해 pong 메시지를 잘 수신하는지도 확인합시다.

 

 

 

3. 클러스터 구성.

 

앞서 설명한 대로 NodeJS는 단일 스레드입니다. 이제 우린 서버의 CPU개수에 맞춰서 여러 워커를 띄어서 서버를 운영해 보도록 하겠습니다.

 

여러 워커가 돌아갈 때 워커가 동작하는 서버를 구분하기 위해 uuid 패키지를 설치합니다.

 

$ npm i uuid

 

그리고./server/index.js를 다음과 같이 수정해주세요.

 

//// ./server/index.js
const cluster = require('cluster');
const os = require('os');
const uuid = require('uuid');

const port = 3000;
const instance_id = uuid.v4();

//// Create worker.
const cpu_count = os.cpus().length;
const worker_count = cpu_count / 2;

//// If master, create workers and revive dead worker.
if(cluster.isMaster){
    console.log(`Server ID: ${instance_id}`);
    console.log(`Number of server's CPU: ${cpu_count}`);
    console.log(`Number of workers to create: ${worker_count}`);
    console.log(`Now create total ${worker_count} workers ...`);

    //// Message listener
    const workerMsgListener = (msg) => {
        const worker_id = msg.worker_id;
        //// Send master's id.
        if(msg.cmd === 'MASTER_ID'){
            cluster.workers[worker_id].send({cmd: 'MASTER_ID', master_id: instance_id});
        }
    }

    //// Create workers
    for(var i = 0; i < worker_count; i ++){
        const worker = cluster.fork();
        console.log(`Worker is created. [${i +1}/${worker_count}]`);
        worker.on('message', workerMsgListener);
    }

    //// Worker is now online.
    cluster.on('online', (worker) => { console.log(`Worker is now online: ${worker.process.pid}`); });

    //// Re-create dead worker.
    cluster.on('exit', (deadWorker) => {
        console.log(`Worker is dead: ${deadWorker.process.pid}`);
        const worker = cluster.fork();
        console.log(`New worker is created.`);
        worker.on('message', workerMsgListener);
    });
}
//// If worker, run servers.
else if(cluster.isWorker){
    const express = require('express');
    const favicon = require('express-favicon');
    const path = require('path');

    const app = express();
    const worker_id = cluster.worker.id;
    const server = app.listen(port, () => { console.log(`Server is listening on port ${server.address().port}.`); });

    let master_id = "";

    //// Request master's id to master.
    process.send({worker_id: worker_id, cmd: 'MASTER_ID'});
    process.on('message', (msg) => {
        if(msg.cmd === 'MASTER_ID'){
            master_id = msg.master_id;
        }
    });

    app.use(favicon(path.join(__dirname, '../public/favicon.ico')));
    app.use(express.static(__dirname));
    app.use(express.static(path.join(__dirname, '../build')));

    app.get('/ping', (req, res) => { return res.send('pong'); });
    app.get('/where', (req, res) => { return res.send(`Running server: ${master_id} \n Running worker: ${worker_id}`); });
    app.get('/kill', (req, res) => { cluster.worker.kill(); return res.send(`Called worker killer.`);})
    app.get('/*', (req, res) => { return res.sendFile(path.join(__dirname, '../build', 'index.html')); });
}

 

소스가 길어졌으니 주석을 확인하면서 차근차근 코딩해 주세요. 간단히 설명해드리자면 다음과 같습니다.

  1. 우리는 한 서버의 CPU개수의 절반만큼의 워커를 생성할 겁니다.
  2. 만약 현재 생성된 클러스터가 마스터라면 워커 클러스터를 생성하고 관리하는 역할을 수행합니다.
  3. 마스터는 미리 정해진 개수만큼 워커를 생성하고 만약 죽은 워커가 발견된 경우 새 워커를 생성시켜 항상 일정 개수의 워커가 서버에서 동작할 수 있도록 합니다.
  4. 워커는 express 서버를 구동합니다.
  5. 워커에는 어느 서버에서 수행되고 있는지 확인할 수 있는 기능과 현재 워커를 죽일 수 있는 기능이 추가되었습니다.

코딩이 끝났다면 다시 서버를 실행시켜 보도록 합시다.

 

$ node server

 

콘솔에서 다음과 같은 메시지를 확인할 수 있습니다.

 

 

이제 localhost:3000/where로 이동해 보세요. 현재 우리가 접속한 서버와 워커의 번호를 확인할 수 있습니다. 여기서 localhost:3000/kill로 이동하면 워커를 죽일 수 있으며 이때 마스터는 죽은 워커를 확인 해 새 워커를 생성합니다. 

 

 

이후 다시 localhost:3000/where로 이동하면 워커의 번호가 변경된 것을 확인할 수 있습니다.

 

 

 

4. Docker를 이용한 서버 구동

 

 

이제 우리가 작성한 서버를 도커를 이용해 배포해봅시다. 간단히 배포하기 위해 Dockerfile을 사용해 배포할 예정입니다. 

 

Dockerfile 파일을 생성 한 뒤 다음과 같이 작성해 주세요.

 

# ./Dockerfile

FROM node:slim

# app 폴더 생성.
RUN mkdir -p /app

# 작업 폴더를 app폴더로 지정.
WORKDIR /app

# dockerfile과 같은 경로의 파일들을 app폴더로 복사
ADD ./ /app

# 패키지 파일 설치.
RUN npm install

# 환경을 배포 환경으로 변경.
ENV NODE_ENV=production

#빌드 수행
RUN npm run build

ENV HOST=0.0.0.0 PORT=3000
EXPOSE ${PORT}

#서버 실행
CMD ["node", "server"]

 

아주 간단한 Dockerfile을 작성했습니다. 주석을 참고하시면 모두 이해하실 수 있을 겁니다. 이제 다음 명령어를 통해 작성한 Dockerfile을 이용해 도커 이미지를 생성합니다.

 

$ docker build . -t my-react:0.0.1

 

 

도커 이미지가 정상적으로 생성된 것을 확인할 수 있습니다. 다음 명령어를 통해 직접 실행시켜 주세요.

 

$ docker run -itd -p 8080:3000 my-react:0.0.1

 

이제 localhost:8080로 이동하면 모든 기능을 정상적으로 사용할 수 있습니다.

 

 

 

 

 

 

 

Jenkins로 NodeJS 프로젝트를 빌드해 봅니다.

 

 

 

0. 사전 준비

 

다음 글을 참고하여 빌드 준비를 합니다.

 

 

 

1. NodeJS 플러그인 설치.

 

NodeJS 프로젝트를 빌드하기 위해선 먼저 플러그인이 필요합니다. 

 

 

위의 그림과 같이 Jenkins > Plugin Manager > Nodejs를 검색해 플러그인을 설치합니다.

 

 

 

2. 빌드 툴 설정.

 

플러그인을 설치했으면 이제 빌드에 사용할 NodeJS 버전을 정의해야 합니다. Jenkins > Jenkins 관리 > Global Tool Configuration으로 이동합니다.

 

 

아래로 내려보시면 NodeJS 버전과 관련된 메뉴가 있습니다. Add NodeJS 버튼을 눌러 빌드에 사용할 NodeJS 버전을 정의해 주세요.

 

 

 

 

3. 프로젝트 구성 설정.

 

이제 위에서 설정한 NodeJS를 프로젝트에서 사용할 수 있습니다. JenkinsItem을 하나 생성합니다. 이 예시에서는 Freestyle project로 생성하였습니다. 프로젝트 생성이후 생성한 Item > 구성 > Build로 이동합니다.

 

 

Add build step > Execute NodeJS script 를 선택합니다.

 

 

NodeJS Installation에 사용할 NodeJS 버전을 선택합니다. 해당 리스트는 위의 Global Tool Configuration에서 설정한 NodeJS 버전 리스트가 나타납니다. 지금 바로 NodeJS Script는 사용하지 않겠습니다. 

 

 

NodeJS 버전을 선택한 뒤 Add build step > Execut shell을 선택합니다.

 

 

이제 NPM 빌드 관련 명령어를 입력해 줍니다.

cd /var/lib/jenkins/workspace/lb-4-docker/
npm install
npm run build

 

먼저 해당 프로젝트가 있는 폴더로 이동후 필요한 패키지를 설치합니다. 그 후 빌드를 수행하는 명령어입니다.

 

 

 

4. 빌드 수행.

 

빌드를 수행할 차례입니다. Jenkins 프로젝트로 이동 후 좌측의 Build Now를 눌러 빌드를 수행합니다.

 

 

정상적으로 빌드되었다면 푸른색 원이 보입니다. 좌측의 Console Output메뉴로 이동해 빌드 로그를 확인할 수 있습니다.

 

 

 

 

 

루프백을 사용하는 방법에 대해 알아봅니다.

 

 

 

 

1. Loopback

 

루프백을 한마디로 설명하자면 다음과 같습니다.

 

LoopBack은 Node.js에서 API 및 마이크로 서비스를 구축하기 위한 플랫폼입니다.

 

또한 공식 문서에서 추가적으로 루프백에 대해 다음과 같이 설명하고 있습니다.

 

LoopBack은 Express를 기반으로하는 확장 성이 뛰어난 오픈 소스 Node.js 및 TypeScript 프레임 워크로, 데이터베이스 및 SOAP 또는 REST 서비스와 같은 백엔드 시스템으로 구성된 API 및 마이크로 서비스를 신속하게 작성할 수 있습니다.

 

아래 다이어그램은 어떻게 LoopBack이 들어오는 요청과 나가는 결과들 사이에 다리 역할을 하고 있는지 보여줍니다. 또한 루프백이 제공하는 다양한 기능들도 보여줍니다.

 

 

이 글을 작성하는 시점에서 최신 버전인 Loopback4를 사용하도록 하겠습니다. Loopback3은 현재 여전히 마이그레이션 중이며 몇몇 기능은 루프백 4에서 동작하지 않을 수 있습니다. 이러한 차이점에 대해선 별도로 다루지 않으며 이 글을 참고해 주시기 바랍니다.

 

 

 

2. LoopBack4 시작하기

 

루프백4는 NodeJs 버전 10 이상을 요구합니다. 미리 설치해 줍시다.

 

NodeJs를 설치하고 난 뒤 아래의 명령어를 통해 LoopBack4 CLI를 설치합니다.

 

> npm i -g @loopback/cli

 

이 LoobBack CLI 도구는 프로젝트를 스캐폴딩 하고 TypeScript 컴파일러를 구성하며 필요한 모든 종속성을 설치합니다. 이제 다음과 같이 명령어를 입력해 프로젝트를 생성합니다.

 

> lb4 app

 

 

옵션을 선택해 프로젝트를 구성합니다. 구성이 완료되면 만들어진 프로젝트 폴더로 이동해 루프백을 실행합니다.

 

> cd loopback

> npm start

 

정상적으로 실행되었는지 확인해보기 위해 http://localhost:3000/ping 로 이동합니다. "greeting":"Hello from LoopBack"이 포함된 json 응답이 보이면 정상적으로 실행된 것입니다.

 

 

 

3. 컨트롤러 추가하기.

 

이제 간단하게 새로운 컨트롤러를 추가해 봅시다. 일단 Ctrl + C를 눌러 실행 중인 루프백을 먼저 종료해 줍니다. 그런 뒤 다음 명령어를 통해 새로운 컨트롤러를 추가합니다.

 

> lb4 controller

 

 

안내에 따라 컨트롤러 이름을 정한 뒤 유형을 선택합니다. 예시에서는 빈 컨트롤러를 선택하였습니다.

 

이제 디렉터리에 우리가 생성한 컨트롤러가 추가된 것을 확인할 수 있습니다. .\src\controllers\hello.controller.ts 파일을 열어보세요.

 

// Uncomment these imports to begin using these cool features!

// import {inject} from '@loopback/context';


export class HelloController {
  constructor() {}
}

 

빈 컨트롤러를 선택했기 때문에 아무런 동작을 하지 않습니다. 이제 여기에 hello에 대한 응답으로 "Hello world!"를 보내주도록 코드를 작성해 봅시다.

 

import { get } from "@loopback/rest";

export class HelloController {
    @get("/hello")
    hello(): string {
        return "Hello world!";
    }
}

 

이제 다시 루프백을 실행 후 http://localhost:3000/hello 로 이동합니다. 

 

 

정상적으로 보이시나요? 이렇게 원하는 컨트롤러를 추가해줄 수 있습니다.

 

 

 

 

 

 

Express 서버와 pg 패키지를 이용해 PostgreSql 커넥터를 만들고 DB에서 데이터를 가져옵니다.

 

 

 

0. Postgre DB 준비.

 

코딩에 앞서 먼저 DB를 준비합니다. 이 글에서 별도로 Postgre DB를 설치하는 방법에 대해 작성하진 않습니다.

 

이 글을 참조하셔서 미리 DB를 준비해 주시기 바랍니다.

 

 

 

1. Express서버 준비.

 

express 패키지를 설치 한 뒤 다음과 같이 코딩해 express서버를 준비합니다.

 

> npm intall express

 

var express = require('express');
var app = express();

app.get('/', function(req, res){
    res.send('Hello World!');
});

app.listen(3300, function(){
    console.log("Express server is listening on port 3300.");
});

 

 

 

2. PostgreSql 커넥터 작성.

 

pg 패키지를 설치한 뒤 다음과 같이 코딩해 Postgresql 커넥터를 준비합니다.

 

> npm install pg

 

const vals =  require('./const.js');
const { Pool, Client } = require('pg');

const client = new Client({
    user: vals.user, password: vals.pass,
    host: vals.host, port: vals.port,
    database: vals.db
});

function GetUserList() {
    client.connect();
    client.query('SELECT * FROM users', (err, res) => {
        console.log(res);
        client.end();
    });
};
 
module.exports = {
    getUserList: GetUserList
}

 

** const.js에는 DB 접속에 필요한 정보가 담겨있습니다.

** 별도의 js 파일이 아닌 해당 값을 직접 user, password, hose, port, databse에 넣어주셔도 됩니다.

 

 

 

3. Express에서 커넥터 호출하기

 

작성한 커넥터를 호출하기 위해 Express의 코드를 수정합니다.

 

var express = require('express');
var app = express();

var pgDBConn = require('./pgDBConn.js');

pgDBConn.getUserList();

app.get('/', function(req, res){
    res.send('Hello World!');
});

app.listen(3300, function(){
    console.log("Express server is listening on port 3300.");
});

 

이제 Express 서버를 실행시키면 콘솔 창에 정상적으로 데이터를 가져오는 것을 확인할 수 있습니다.

 

 

 

 

 

+ Recent posts