본문 바로가기
Network/Server

[Server] Rest API config 파일 설명

by 며루치꽃 2021. 1. 22.

1. 전반적인 개요

node template은 node express framework를 사용해서 mvc 패턴으로 구성되어있습니다. 

 

config 폴더에는 데이터베이스를 설정, express 설정 구성파일이 들어있습니다

winsten 파일이라는 라이브러리를 만들어서 실행할 때마다 로그 폴더에 로그를 쌓아둘 수 있습니다. 

node_module 폴더는 npm install 해서 설정되는 폴더입니다.

소스를 구성할 부분은 src 폴더입니다. 앱 구성은 app에서 구성을 하고, 웹 API를 구성할 때는 web에서, 웹 Admin API 구성은 webAdmin에서 구성을 하게됩니다.

gitignore은 git을 푸쉬할 때 제외할 폴더를 구성해주고 

package.json의 dependcies 부분은 npm install 구성시 환경설정을 구성해줍니다. 

 

2. 세부파일 설명

세부파일 설명

1) database.js 파일 

const mysql = require('mysql2/promise'); // mysql2 promise라는 모듈을 사용해서 mysql을 잡아오고

default 상태일 때는 아래와 같이 생겼는데 데이터베이스 구성을 이렇게 생겼고

const pool = mysql.createPool({ // 기본적인 데이터베이스의 정보 
    host: '',
    user: '',
    port: ,
    password: '',
    database: ''
});

작성을 한 이후에는, 아래를 바탕으로 createpool 을 해주고 데이터베이스를 생성합니다.  

const pool = mysql.createPool({ // 기본적인 데이터베이스의 정보 
    host: 'localhost',
    user: 'root',
    // port: ,
    password: '1234',
    database: 'soft-server'
});

만들어진 데이터베이스 pool을 이용해 다른곳에서 이용할 수 있게 export를 해줍니다.

module.exports = { // 만들어진 pool을 
    pool: pool
};

connection pool에 대한 개념은 해당 connection pool과 같습니다.

 

트랜잭션의 간단한 예 데이터베이스에 회원정보(usertable)에 정보를 저장했다가 user에 연관된 정보인 배터리정보(batterytable) 같은 곳에 한 번 더 컬럼을 정보를 넣어줘야합니다. 이때, 똑같이 API를 실행했을 때, 회원정보는 들어가고 그리고 배터리 table에서도 배터리정보가 들어가야하는데 회원정보에 들어가고 나서, error가 났으면 배터리 정보는 들어가있지 않을 때, 이 경우 트랜잭션 처리를 해줘야하는 건데, 회원정보에도 들어가고 배터리정보에 들어가야만 모든 것이 완벽하게 되어서, 저장이 된다. 만약에 회원정보만 들어가게 되었다면, 다시 모든 것을 복귀 시켜서 처음 상태로 만들어준다라는 개념입니다.

아래 코드는 트랜잭션을 하는것과 안하는 것의 예시코드를 넣어놓은 것인데 논트랜잭션과 트랜잭션으로 구성됩니다. 

 

논트랜잭션일 경우

 

database.js

// pool에 저장되어있는 계정정보를 가지고있는 것에 getconnection을 통해 
const exampleNonTransaction = async (sql, params) => {
    try {
        const connection = await pool.getConnection(async conn => conn);
        try {
            const [rows] = await connection.query(sql, params); // connection해준 것의 쿼리를 sql과 파라미터에 넣어준다 -> 끝난정보는 rows에 들어가게 되고
            connection.release(); // connection 해온 것에 대한 결과값이 나오게 된다면 release를 꼭 해줘서 connection을 풀어줘야합니다. 
            return rows;
        } catch(err) { // 만약 try에서 오류가 발생했다면 try-catch를 통해 에러가 발생했다고 알려준 후  
            logger.error(`example non transaction Query error\n: ${JSON.stringify(err)}`);
            connection.release(); //connection release를 통해 false를 반환해준다 
            return false;
        }
    } catch(err) {
        logger.error(`example non transaction DB Connection error\n: ${JSON.stringify(err)}`);
        return false;
    }
};

 

1) pool에 저장되어있는 계정정보를 가지고있는 것에 getconnection을 통해 연결을 합니다.

2) connection해준 것의 쿼리를 sql과 파라미터에 넣어줍니다. sql에는 쿼리문이 파라미터에는 쿼리 where문 뒤에 오는 매개변수가 들어갑니다  -> 끝난정보는 rows에 들어가게 됩니다

3) 만약 try에서 오류가 발생했다면 try-catch를 통해 에러가 발생했다고 알려준 후

4) connection release를 통해 false를 반환해줍니다. 

 

트랜잭션일 경우

 

database.js

// 트랜잭션인 경우 
const exampleTransaction = async (sql, params) => {
    try {
        const connection = await pool.getConnection(async conn => conn); // pool을 바탕으로 getconnection을 진행해주고
        try {
            await connection.beginTransaction(); // START TRANSACTION : connection 가져온 것을 start해줍니다. 
            const [rows] = await connection.query(sql, params); // 연결된 connection을 가지고서 쿼리를 진행해준다 sql에는 실질적인 sql이 들어가고, params에는 sql의 where문 뒤에 들어갈 parameter들을 가져오게 됩니다.
            await connection.commit(); // COMMIT : 진행된 쿼리문에 대해서 완벽히 됐다고 저장하는 단계
            connection.release(); // connection을 release 시킵니다
            return rows;
        } catch(err) { // 만약 에러가 났을 경우 
            await connection.rollback(); // ROLLBACK : connection을 전부 rollback 시켜 처음의 상태로 만들어줍니다 
            connection.release(); // connection을 release 시킵니다
            logger.error(`example transaction Query error\n: ${JSON.stringify(err)}`);
            return false;
        }
    } catch(err) {
        logger.error(`example transaction DB Connection error\n: ${JSON.stringify(err)}`);
        return false;
    }
};

1) pool을 바탕으로 getconnection을 진행해줍니다

2) START TRANSACTION : connection 가져온 것을 start해줍니다. 

3) 연결된 connection을 가지고서 쿼리를 진행해준다 sql에는 실질적인 sql이 들어가고, params에는 sql의 where문 뒤에 들어갈 parameter들을 가져오게 됩니다.

4) COMMIT : 진행된 쿼리문에 대해서 완벽히 됐다고 저장하는 단계

5) connection을 release 시킵니다

6) 만약 에러가 났을 경우, ROLLBACK : connection을 전부 rollback 시켜 처음의 상태로 만들어주고 connection을 release 시킵니다.

 

2) express.js 

node가 실행될 때 실행구조는 터미널에 index.js로 index.js 파일을 node로 실행시켜주면 node 가 실행이 되게됩니다. 이 명령의 의미는 상위 폴더에 있는 index.js 파일에서 express가 실행이 되면서 3000번 포트로 실행한다는 의미입니다

 

index.js

const express = require('./config/express');
const {logger} = require('./config/winston');

const port = 3000;
express().listen(port);
logger.info(`${process.env.NODE_ENV} - API Server Start At Port ${port}`);

express.js

const express = require('express');
const compression = require('compression');
const methodOverride = require('method-override');
var cors = require('cors');
module.exports = function () {
    const app = express();

    app.use(compression());

    app.use(express.json());

    app.use(express.urlencoded({extended: true}));

    app.use(methodOverride());

    app.use(cors());
    // app.use(express.static(process.cwd() + '/public'));

    /* App (Android, iOS) */
    require('../src/app/routes/indexRoute')(app);
    require('../src/app/routes/userRoute')(app);

    /* Web */
    // require('../src/web/routes/indexRoute')(app);

    /* Web Admin*/
    // require('../src/web-admin/routes/indexRoute')(app);
    return app;
};

기본적으로 express를 사용해도 되지만, 위와 같이 custom을 해서 사용할 수 있습니다. 

여기서 기억해야할 점은 API를 하나 만들 때 src에 작업을 하게 되는데, route에 indexRoute 나 userRoute를 구성하고 있습니다. 만약 boardRoute를 추가하게된다면 express.js에도 boardRoute가 추가된 것을 알아야하기 때문에 아래와 같이 구성하면 됩니다.

/* App (Android, iOS) */
    require('../src/app/routes/indexRoute')(app);
    require('../src/app/routes/userRoute')(app);
    require('../src/app/routes/boardRoute')(app);

 

3) jwtMiddleware.js 

jwtMiddleware.js 에는 토큰 값이 계속 가져오는게 가능한지 확인하는 파일입니다. 항상 로그인을 하고나서 유저가 가지고 있는 토큰 값을 확인을 해야하는 절차가 매번 있기 때문에 그것을 API에서 계속 해주기에는 번거롭습니다. 그래서 미들웨어로 모듈화를 시켜서 진행합니다. 

 

const jwt = require('jsonwebtoken');
const secret_config = require('./secret');
const jwtMiddleware = (req, res, next) => {
    // read the token from header or url 
    const token = req.headers['x-access-token'] || req.query.token;
    // token does not exist
    if(!token) {
        return res.status(403).json({ 
            isSuccess:false,
            code: 403,
            message: '로그인이 되어 있지 않습니다.'
        });
    }

    // create a promise that decodes the token
    const p = new Promise(
        (resolve, reject) => {
            jwt.verify(token, secret_config.jwtsecret , (err, verifiedToken) => {
                if(err) reject(err);
                resolve(verifiedToken)
            })
        }
    );

    // if it has failed to verify, it will return an error message
    const onError = (error) => {
        res.status(403).json({
            isSuccess:false,
            code: 403,
            message:"검증 실패"
        });
    };

    // process the promise
    p.then((verifiedToken)=>{
        //비밀 번호 바꼇을 때 검증 부분 추가 할 곳
        req.verifiedToken = verifiedToken;
        next();
    }).catch(onError)
};

module.exports = jwtMiddleware;

1) read the token from header or url : 클라이언트 측에서 request를 받을 때 클라이언트 측에서 x-aceess헤더에 x-access라는 토큰을 보내주게 되거나 token이라는 값을 보내주게 된다면

token이 없을 때는 로그인이 되어있지 않습니다 라는 response를 보내주고

token이 값이 있을 때는 jwt 토큰을 만들 때 방식으로 다시 풀어서 볼 수 있도록 합니다.

만약 토큰을 푸는 과정에서 에러가 일어났다면, 토큰 검증 실패라는 메세지를 띄어줍니다. 

 

4) secret.js

헤더에 들어가는 key입니다.

 

5) winston.js

log를 생성할 수 있는 라이브러리 winston을 사용하였습니다.

log 파일이 형성되는 형식을 해놨습니다.

const { createLogger, format, transports } = require('winston');
require('winston-daily-rotate-file');
const fs = require('fs');

const env = process.env.NODE_ENV || 'development';
const logDir = 'log';

// https://lovemewithoutall.github.io/it/winston-example/
// Create the log directory if it does not exist
if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir)
}

const dailyRotateFileTransport = new transports.DailyRotateFile({
    level: 'debug',
    filename: `${logDir}/%DATE%-smart-push.log`,
    datePattern: 'YYYY-MM-DD',
    zippedArchive: true,
    maxSize: '20m',
    maxFiles: '14d'
});

const logger = createLogger({
    level: env === 'development' ? 'debug' : 'info',
    format: format.combine(
        format.timestamp({
            format: 'YYYY-MM-DD HH:mm:ss'
        }),
        format.json()
    ),
    transports: [
        new transports.Console({
            level: 'info',
            format: format.combine(
                format.colorize(),
                format.printf(
                    info => `${info.timestamp} ${info.level}: ${info.message}`
                )
            )
        }),
        dailyRotateFileTransport
    ]
});

module.exports = {
    logger: logger
};

 

댓글