기본 콘텐츠로 건너뛰기

[node.js] express framework 좀더 구조화 시키기

Express Frame

express는 node.js에서 제공하는 프레임워크. express-genderator를 설치를 통해 express 프로젝트 생성이 가능하다.
아래의 내용은 Mysql 혹은 MariaDB, MongoDb(mongodb모듈을 통해 로우단에서 mongodb를 다룬다.)를 기준으로 디비에 대한 설명을 다룰 것 이다.

List

  1. express 프로젝트 시작
  2. 생성 된 프로젝트 설명 및 수정
  3. 디렉토리 관리
  4. 설정파일 관리 및 정적파일 관리
  5. 클러스터링 설정
  6. DB 관리
  7. ORM 및 모델링 방법
  8. 413 응답코드 대처법
  9. 미들웨어의 활용방법
  10. file upload 방법
  11. test 코드 작성하기

1. express 프로젝트 시작

express-genderator설치
$ npm install -g express-generator
express-generator이외의 서버를 실행 시키기 위한 패키지를 받아보자.
  • nodemon : 서버 내부의 코드가 바뀌면 서버를 자동으로 재시작 해주는 패키지
  • forever : 서버를 백그라운드로 띄어주는 패키지
$ npm install -g nodemon
$ npm install -g forever
express 프로젝트 생성
$ express {project_name}
project_name이라는 디렉토리가 생성이 된다.
$ cd project_name  # 해당 프로젝트로 이동
$ npm install      # 모듈 설치
$ npm install -s mysql # mysql설치(sequelize설치시 필요)
$ npm install -s sequelize # sequelize설치(ORM)
$ npm install -s mongodb # mongodb설치(mongodb connector)

2. 생성 된 프로젝트 설명 및 수정

original Directory Structure

  • bin
    • www
  • public
    • images
    • javascript
    • stylesheets
  • routes
    • index.js
    • user.js
  • views
    • error.jade
    • index.jade
    • layout.jade
  • app.js
  • package.json
express를 통해 프로젝트를 생성을 하면 위와 같은 구조로 프로젝트가 생성이 된다. 위의 구조만을 가지고는 프로젝트를 관리하기가 아직은 불편하다. 디비, 각종 설정파일, 라우팅 관리 등이 잘 되어있지 않기 때문에 좀더 수정을 하였다. 수정을 하기 앞서 위의 프로젝트 구조에 대해서 약간의 설명을 해보겠다.
우선 4개의 디렉토리와 2개의 파일이 생성이 된다.
  • directory :
    • bin : 실제 서버가 실행
    • public : images, js, css파일과 같은 정적파일 관리
    • routes : 실제 서버 로직이 처리
    • views : html, jade, ejs같은 웹 페이지 관리
  • file :
    • app.js : 미들 웨어 설정
    • package.json : 프로젝트 관리

Modified Directory Structure

  • bin
    • www
  • config
    • RDB.json
    • MONGO.json
    • AWSconfig.json
  • models_mong
    • index.js
  • models_RDB
    • index.js
    • play.js
    • user.js
  • public
    • images
    • javascript
    • stylesheets
  • routes
    • ADMIN
    • API_V1
  • views -error.jade -index.jade -layout.jade
  • app.js
  • package.json
기존의 프로젝트에서 models와 config디렉토리를 생성을 하였다.

3. 디렉토리 관리

잠깐! node.js에서의 모듈을 호출되는 방식에 대해서 설명을 해보겠다.
첫번째로 node.js에서는 c++과 같은 컴파일 언어와 달리 require자체가 해당 파일을 객체로 가지고 오게 된다. 또한 해당 파일 객체를 중첩접으로 호출을 하지 않는다. 즉, require('module')을 아무리 많이 해도 module이라는 파일을 한번만 가지고 오게 된다. 이를 잘 명심하는 것이 중요하다.
두번째로 require를 할 때 디렉토리를 모듈로 가지고 올 경우 해당 디렉토리 내부에 있는 index.js를 자동으로 가지고 오게 된다. 즉 해당 프로젝트에서 require('models_RDB')를 항경우 models_RDB내부에 있는 index.js를 호출을 하게된다.

4. 설정파일 관리 및 각종 리소스 관리

  • config : 각종 디비에 연결되는 정보 및 aws에 연결되는 설정 정보는 config내부에 넣도록 한다. 또한 해당 디렉토리는 .gitignore에 포함하여 github나 원격 저장소에 올라가지 않도록 관리를 해준다.
    config/*
    node_modules/
  • 정적파일 관리 : javascript, css, image등을 public 내부에서 관리를 해준다.
    app.use(express.static(path.join(__dirname, 'public')));
app에서 static경로를 설정하여 다른 파일에서 js, image, css파일을 사용할 때 절대경로인 /가 app이 아닌 public경로로 잡힌다.

5. 클러스터링 설정

클러스터링이란 여러개의 서버를 띄어 부하분산을 하는 것이다. 클러스터링은 bin/www에서 해주면 된다.
// ./bin/www 중 일부 
const numCPUs = require('os').cpus().length; // CPU갯수 가져오기 
...중략
if (cluster.isMaster) {
    // Fork workers. 
    // master 영역 
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
}else{
    // 실제 서버가 실행되는 영역 
    // fork할 경우 해당 로직이 실행된다.(CPU갯수만큼) 
    server.listen(port);
    server.on('error', onError);
    server.on('listening', onListening);
}
...중략
cluster.fork()를 사용해서 server.listen(port) CPU갯수만큼 서버를 띄어주고 있다. 여기서 만약에 서버가 죽었을 때 다시 띄어주고 싶으면 다음과 같이 마스터 영역에 추가해주면 된다.
cluster.on('exit', ()=>{
        cluster.fork();
})

6. DB 관리

해당 프로젝트에서는 RDB:mysql, NoSQL:mongodb를 다룬다. mysql은 orm모듈인 sequelize를 사용하고 있고 mongodb는 연결 커넥터를 통해 직접 디비접속을 다루고 있다.

mysql

modelss_RDB에서 user.js, play.js와 같은 모델파일들을 만들어 준다. index.js는 모델들을 객체로 만들어주는 역할을 한다.
index.js는 config/RDB.json에 있는 DB 설정 객체를 가지고 온다.
// create model 
 "use strict";
module.exports = function(sequelize, DataTypes) {
    var play = sequelize.define("play", {
        playName :   DataTypes.STRING
    }, {
        classMethods: {
            associate: function(models) {
            }
        }
    });
    return play;
};
routes/API_V1/rdb.js에서 해당 모델을 사용을 해보자
// ./routes/API_V1/rdb.js 중 일부  
const {
        play,
        user 
      } = require('../../models_RDB');
잘 보면 해당 디렉토리를 객체로 가져오고 있다. 이때 중요한건 해당 디렉토리 index.js파일이 모델파일들을 객체로 가지고 오면서 직접 호출을 하지 않아도 된다. 저기서 const {model1, model2}처럼 모델 파일을 만들 때 return 하는 변수명을 그대로 가져다 쓰면 해당 모델을 사용할 수 있게 된다. 만약 모델을 추가 할 때, 모델파일을 만들고 쓰는 곳에서 const {추가된 모델 return변수 명} = require('models_RDB')해주면 된다.

mongodb

models-mong에서 mongodb와 연결 되는 커넥터 객체를 생성 및 객체를 반환을 해준다.
config/MONGO.json에 있는 DB 설정 객체를 가지고 온다.
// ./bin/www 중 일부 
const mongodb = require('../models_mong');
const url =require('../config/MONGO.json'); 
...중략
var server = http.createServer(app);
mongodb.connect(url, function(err){
    server.listen(port);
    server.on('error', onError);
    server.on('listening', onListening);
});
...중략
bin/www에서 서버가 띄어지는 부분을 위처럼 수정을 해주자.
초기에 서버가 생성이 되면 connect를 통해 객체를 생성을 해준다.
// ./models_mong/index.js 중 일부 
var state = {
  db: null,
}
exports.connect = function(url, done) {
  if (state.db) return done()
  MongoClient.connect(url, function(err, db) {
    if (err) return done(err)
    state.db = db
    done()
  })
}
해당 객체의 connect를 호출을 해면 state.db를 생성하게 된다. 이제 get()을 사용하여 해당 객체를 가져와 mongodb를 사용 할 수 있게된다.
// ./routes/API_V1/mongo.js 중 일부  
...중략
router.get('/',(req, res, next)=>{
    /*
        * mongodb에서 collection불러오기
        * collection은 RDB에서 table의 개념
    */
    mongoConnect.get().collection('');
    res.end('hello world');
});
...중략
get().collection()으로 해당 collection을 가져올 수 있다.

7. ORM 및 모델링 방법

ORM을 사용하게 되면 가장 귀찮은 부분 중 하나가 모델파일을 생성하는 것이다. 만약 기존에 이미 테이블이 존재하는 것이라면 모델링 파일을 생성하는 것이 굉장히 귀찮아진다. 하지만 이것을 쉽게 해주는 방법이 sequelize-auto를 이용하면 이미 존재하는 디비의 테이블들을 파일로 내려받을 수 있다.
설치
$ npm install -g sequelize-auto
사용방법
sequelize-auto -h  -d  -u  -x [password] -p [port]  --dialect [dialect] -c [/path/to/config] -o [/path/to/models]
옵션
-h, --host      IP/Hostname for the database.                                      [required]
-d, --database  Database name.                                                     [required]
-u, --user      Username for database.                                             [required]
-x, --pass      Password for database.
-p, --port      Port number for database.
-c, --config    JSON file for sending additional options to the Sequelize object.
-o, --output    What directory to place the models.
-e, --dialect   The dialect/engine that you're using: postgres, mysql, sqlite
예시
$ sequelize-auto -o "./models" -d database -h localhost -u root -p 3306 -x password -e mysql
./models내부에 모델파일들을 생성해준다.

8. 413 응답코드 대처법

추가적으로 express를 사용하다보면 POST요청시 가끔 413이라는 응답코드가 발생하는 경우가 있다. 이는 극히 드문경우이긴 하지만 파일의 데이터가 큰 경우 또는 데이터가 많이 오고가는 상황에서는 충분히 나타날 가능성이 높은 응답코드이다. 해당 응답코드는 body의 데이터가 제한이 되어 뜨는 응답코드이다. 사실 우리들은 post요청은 데이터의 제한이 없는 것으로 알고있다. 맞다. 알고있는 사실이 맞긴한데, 기본적으로 express는 이것을 100Kb로 설정이 되어있다. 이 부분은 수정이 가능한 부분이다. 아래와 같이 미들웨어를 수정을 하면 된다.
// express 4.x version 이상 (특정버전에서는 미들웨어 추가 방식이 다름.) 
var bodyParser = require('body-parser'); 
app.use(bodyParser.json(/*{limit: 5000000}*/));
app.use(bodyParser.urlencoded(/*{limit: 5000000, extended: true, parameterLimit:50000}*/));
위 코드에서 주석 부분을 풀면 서버 관리자가 원하는 데이터의 크기까지 제한을 줄 수 있다. 용량의 표기법은 50000000같이 명시를 해도 되지만, 5kb5mb5gb와 같은 방법도 제공이 된다.

9. 미들웨어의 활용방법

express는 다양한 미들웨어를 제공을 한다.
// app.js파일 중 일부 
...중략
// view engine setup 
app.set('views', path.join(__dirname, 'views')); // views파일 경로 설정 
app.set('view engine', 'jade');                  // templete engine 설정 
// uncomment after placing your favicon in /public 
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); // favicon 경로 설정 
app.use(logger('dev'));   //로그설정 
app.use(bodyParser.json(/*{limit: 5000000}*/)); //body parsing 설정 
app.use(bodyParser.urlencoded(/*{limit: 5000000, extended: true, parameterLimit:50000}*/)); //body parsing 설정 
app.use(cookieParser());  //쿠키 데이터 파싱설정 
app.use(express.static(path.join(__dirname, 'public'))); 정적 파일 경로 설정
...중략
app.use로 된부분을 미들웨어라고 한다. 미들웨어란 실제 로직이 처리되기 전에 반복되는 작업 전처리 작업이라고 생각하면 이해하기 쉽다. 하지만 express에서 제공을 하지 않은 미들웨어를 사용자가 직접 만들어 사용할 수 있다. 예를들어 로그인이 된 사용자만 가능한 기능들이 있다. 이럴 경우 매 기능이 작동하기 이전에 로그인을 확인을 해야 한다. 하지만 모든 각 기능에 대해서 반복적으로 로그인을 확인하는 코드가 발생할 것이다. 이것을 미들웨어를 이용하면 방복되는 작업을 쉽게 해결할 수 있다.
우선 미들웨어는 next()를 사용하여 다음 로직으로 데이터 처리권한을 넘겨준다.
간단한 예시를 보자.
var http = require('http');
var express = require('express'); 
var app = express(); 
//포트설정 
app.set('port', 3000); 
//첫번째 미들웨어 
app.use((req, res, next) =>{
    console.log('first middleware');
    next();
}); 
//두번째 미들웨어 
app.use((req, res, next) =>{
    console.log('seconds middleware');
    next();
});
app.get('/', (req, res,next)=>{
    console.log('response');
    res.end('hello world');
}) 
http.createServer(app).listen(3000, ()=>{
    console.log('server on port : ', app.get('port'));
})
$ node app.js # 서버실행
$ curl localhost:3000 # 클라이언트 서버 접속
hello world           # 서버의 응답
# 서버 모니터
first middleware
seconds middleware
response
get / 200 0.0 -
위처럼 뜰 것이다. 첫번째 미들웨어가 실행 된 후 app.get이 처리되었다. 각각의 미들웨어에서 next를 사용하여 다음 미들웨어로 넘겨주는 패턴이 사용되었다.
var http = require('http');
var express = require('express');
var app = express();
//포트설정 
app.set('port', 3000);
app.get('/test1', (req, res,next)=>{
    /*
        * 로그인이 필요한 작업
    */
    res.end('need login');
})
app.get('/test2', (req, res,next)=&{
    res.end('hello world');
})
app.get('/test3', (req, res,next)=>{
    /*
        * 로그인이 필요한 작업
    */
    res.end('need login');
})
http.createServer(app).listen(3000, ()=>{
    console.log('server on port : ', app.get('port'));
})
위 처럼 /test1, /test3은 로그인이 필요한 작업일 경우 로그인 인증 작업을 하게 되는데 이럴떄는 미들웨어를 사용하면 편하다.
우선 사용자 인증을 하는 function을 만들어 보도록 하자.
var http = require('http');
var express = require('express');
var app = express();
//포트설정 
app.set('port', 3000);
var userAuth = (req, res, next) =>{
    // 사용자 인증 로직 
    next(); 다음 로직에게 넘겨주기
}
app.get('/test1', userAuth, (req, res,next)=>{
    /*
        * 로그인이 필요한 작업
    */
    res.end('need login');
})
app.get('/test2', (req, res,next)=>{ 
    res.end('hello world');
})
app.get('/test3', userAuth, (req, res,next)=>{
    /*
        * 로그인이 필요한 작업
    */
    res.end('need login');
})
http.createServer(app).listen(3000, ()=>{
    console.log('server on port : ', app.get('port'));
})
위처럼 해주면 한번의 인증 코드 작성으로 여러곳에서 재사용이 가능 해 졌다.

10. file upload 방법

파일을 업로드 하기 위해서는 multer라는 모듈을 설치를 해준다.
$ npm install -s multer
// ./routes/API_V1/file.js 
const FILE_UPLOAD_PATH_LOCAL = "./uploads";
const FileUpload = require('multer')({ dest: FILE_UPLOAD_PATH_LOCAL }); 
const router = require('express').Router(); 
router.post('/upload', FileUpload.single('name'), (req, res, next)=>{
    console.log(req.file);
    res.end(' success file upload');
});
multer 모듈을 가지고 오면 해당 모듈에 첫번째 인자로 파일 업로드 경로를 적어준다. 파일을 처리하는 라우터에 미들웨어를 추가해 주면 된다. name은 해당 파일을 가지고 있는 키라고 생각하면 된다. {name : 파일} 요런 구조로 되있다. 만약 파일이 하나가 아닌 여러개일 경우 single대신에 array같은 메소드를 사용하면 된다.
multer git document참조 multer에 대한 자세한 설명은 문서를 참조하길 바란다.

11. test코드 작성하기

노드에서는 mocha라는 패키지로 테스트 코드를 작성 및 실행을 할 수 있다.
우선 test라는 디렉토리를 생성을 해주고 그 안에 test.js를 만들어 준다.
$ mkdir test    # test 디렉토리 생성
$ cd test       # test 디렉토리 이동
$ vim test.js   # test.js 파일 생성
:wq             # 저장 후 종료
$ npm install -g mocha     # mocha 모듈 설치
$ mocha                    # app.js 위치에서 실행을 할 경우 test디렉토리 안에 있는 코드들이 실행 된다.
// ./test/test.js 파일 중 일부 
var assert = require("assert"); 
describe('테스트', function() {
    it('값 비교', function () {
        assert(1 == 1);
    });
    it('값 비교2', function () {
        assert(1 != 1);
    });
});
테스트 코드는 기본적으로 위와 같은 예시처럼 진행을 하면 된다.
$ mocha
테스트
  √ 값 비교
  1) 값 비교2
1 passing (10ms)
1 failing
1) 테스트 값 비교2:
    AssertionError: false == true
    + expected - actual
    -false
    +true
    at Context. (test\test.js:8:3)
그러나 위 방식으로 진행을 하게 되면 문제가 발생하는 경우가 있다. 서버의 API는 테스트와 같이 비동기 처리를 하기 위해서는 콜백 응답이 올 때 까지 기다려야 한다. 이럴때는 done이라는 인자를 사용하면 된다.
// ./test/test.js 파일 중 일부 
describe('async code test',  ()=>{
    describe('setTimeout',  ()=> {
        it('2초가 넘어가면 실패!!',  (done)=> {
            setTimeout( ()=> {
                done();
            }, 5000);
        });
    });
});
서버에 요청을 하고 기다리는 것과 비슷하게 5초후에 실행되는 setTimeout을 걸어 테스트를 진행을 해보겠다.
위처럼 done을 넘겨준뒤 done을 호출해주면 된다. 이제 테스트 코드를 실행을 해보자.
$ mocha
테스트
  √ 값 비교
  1) 값 비교2
async code test
  setTimeout
    2) 2초가 넘어가면 실패!!
  
1 passing (2s)
2 failing
1) 테스트 값 비교2: 
    AssertionError: false == true
    + expected - actual
    -false
    +true
    at Context.it (test\test.js:8:3)
2) async code test setTimeout 2초가 넘어가면 실패!!:
   Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.
에러가 발생한다.
에러가 발생하는 이유는 mocha는 비동기 호출을 사용하여 테스트를 진행을 할 때 기본적으로 2000ms 이내에 done을 실행을 해야 정상적으로 실행이 된다. 위 코드에서 5000ms를 2000ms 이하로 바꾸면 정상적으로 테스트가 성공 할 것이다. 하지만 서버 API를 테스트 하다보면 네트워크에 문제 혹은 복잡한 로직으로 인하여 2000ms가 넘어갈 경우가 생기는데 이럴때는 mocha의 -t 옵션을 이용하여 테스트 코드를 실행을 하면 된다.
$ mocha -t 10000
위처럼 실행을 하면 정상적으로 테스트 코드가 성공적으로 끝날 것이다.
테스트를 진행을 하다보면 특정 작업을 반복적으로 하거나 정리 한 테스트 플로우에 대해서 데이터 생성 및 정리 코드를 작성할 경우가 있는데 이럴때에는 before, after와 같은 후크 함수를 사용하면 편하다.
// ./test/test.js 파일 중 일부 
describe('hooks', function() { 
    before(function() {
        //describe내에서 테스트를 시작하기 이전에 한번만 실행 
    });
    after(function() {
        //describe내에서 테스트가 끝났을때 한번만 실행 
    });
    beforeEach(function() {
        //describe내에서 각각의 it이 실행되기 이전에 실행 
    });
    afterEach(function() {
        //describe내에서 각각의 it이 실행된후 실행 
    });
    it('test1', ()=>{
    })
    it('test2', ()=>{ 
    })
});
위와 같은 구조로 테스트 코드를 작성을 할 수 있다.


https://github.com/pjt3591oo/node-express-frame
 소스 코드는 깃을 통해 제공됩니다.

댓글

이 블로그의 인기 게시물

[kali linux] sqlmap - post요청 injection 시도

아래 내용은 직접 테스트 서버를 구축하여 테스트 함을 알립니다.  실 서버에 사용하여 얻는 불이익에는 책임을 지지 않음을 알립니다. sqlmap을 이용하여 get요청이 아닌 post요청에 대해서 injection공격을 시도하자. 뚀한 다양한 플래그를 이용하여 DB 취약점 테스트를 진행을 해보려고 한다. 서버  OS : windows 7 64bit Web server : X Server engine : node.js Framework : expresss Use modules : mysql Address : 172.30.1.30 Open port : 6000번 공격자 OS : kali linux 64bit use tools : sqlmap Address : 172.30.1.57 우선 서버측 부터  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 var  express  =  require( 'express' ); var  app  =  express(); var  mysql  =  require( 'mysql' ); var  ccc  =  mysql.createConnection({     host: '127.0.0.1' ,     user: 'root' ,     pos...

[git] git log 확인하기

git log를 통해서 커밋 이력과 해당 커밋에서 어떤 작업이 있었는지에 대해 조회를 할 수 있다. 우선 git에서의 주요 명령어부터 알아보겠다. $ git push [branch name] $ git pull [branch name] 여기서 branch name은 로컬일 경우 해당 브런치 이름만 적으면 되지만 깃허브 원격 저장소로 연결을 원할 경우는 해당 브런치 이름 앞에 꼭 origin을 붙이도록 한다. $ git brnch [branch name] $ git checkout [branch name] branch일경우 해당 브런치를 생성을 한다. 여기서 현재의 브런치를 기준으로 브런치를 따는것이다. checkout은 브런치를 바꾸는 것이다.(HEAD~[숫자]를 이용하면 해당 커밋으로 움직일수 있다.. 아니면 해당 커밋 번호를 통해 직접 옮기는것도 가능하다.) -> 해당 커밋으로 옮기는 것일뿐 실질적으로 바뀌는 것은 없다. 해당 커밋으로 완전히 되돌리려면 reset이라는 명령어를 써야한다. 처음 checkout을 쓰면 매우 신기하게 느껴진다. 막 폴더가 생겼다가 지워졌다가 ㅋㅋㅋㅋㅋ  master 브런치에서는 ht.html파일이 존재하지만 a브런치에서는 존재하지않는다. checkout 으로 변경을 하면 D 로 명시를 해준다.  $ git log 해당 브런치의 커밋 내역을 보여준다. a 브런치의 커밋 내역들이다. (머지 테스트를 하느라 커밋 내용이 거의 비슷하다 ㅋㅋ) master 브런치의 커밋 내역들이다. 커밋 번호, 사용자, 날짜, 내용순으로 등장을 한다. 이건 단순히 지금까지의 내역을 훑어보기 좋다. 좀더 세밀한 내용을 봐보자. $ git log --stat --stat을 붙이면 기존의 로그에서 간략하게...

[git] pull을 하여 최신코드를 내려받자

보면 먼가 로고가 다르게 뜨는것을 확인을 할 수가있다. C:\Users\mung\Desktop\etc\study\python-gene>git checkout remotes/origin/master Note: checking out 'remotes/origin/master'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example:   git checkout -b HEAD is now at 29e282a... fetch test C:\Users\mung\Desktop\etc\study\python-gene>git branch * (HEAD detached at origin/master)   master   test1   test2 깃이 잘 쓰면 참 좋은놈인데 어지간히 쓰기가 까다롭다. 처음에 깃을 푸시 성공하는데만 한달정도 걸렸던걸로 기억이 난다.. ㅋㅋㅋ 여담으로  깃 프로필을 가면 아래사진 처럼 보인다. 기여도에 따라서 초록색으로 작은 박스가 채워지는데 저걸 잔디라고 표현을 한다고 합니다 ㅎ 저 사진은 제 깃 기여도 사진입니당 ㅋㅋㅋㅋ 다시 본론으로 돌아와서 ㅋㅋ pull을 하면...