Node.js로 데이터베이스 커넥션 풀을 구현하면서 사용하게 된 라이브러리를 정리하기 위해 작성합니다.

[generic-pool]

- Node.js에서 일반적인 자원 풀링을 구현할 수 있도록 도와주는 라이브러리임.
- 이 라이브러리를 사용하면 자원( DB connection, HTTP 요청 등) 을 관리하고 재사용할 수 있음.
- 특히, 데이터베이스 연결과 같은 생성 비용이 비싼 자원을 사용할 때 효과적임.

사용법
 1. 풀 생성 및 구성 : "generic-pool" 을 사용하여 풀을 생성하고 필요한 옵션을 구성함.
 2. 자원 생성 및 반환 로직 정의 : 풀에서 관리할 자원을 생성하고 반환하는 로직을 정의함. ( 이는 "create" 와 "destroy" 옵션에 해당함)
 3. 자원 획득 및 반환 : "acquire" 함수를 사용하여 자원을 획득하고, 작업이 끝난 후에 "release" 함수를 사용하여 자원을 풀에 반환함.

사용이유
 1. 성능향상 : 자원을 반복적이고 생성하고 소멸하는 것은 비용이 많이 들며 시스템 성능을 저하시킬 수 있음.
대신 자원을 풀에 보관하고 필요할 때 재사용함으로써 성능을 향상시킬 수 있음.
 2. 자원관리 : 풀을 사용하면 동시에 여러 작업에서 자원을 공유하고 관리할 수 있음. 이는 메모리 누수나 자원 고갈과 같은 문제를 방지할 수 있음.
 3. 서버 부하 감소 : 풀을 사용하면 서버에 대한 부하를 분산할 수 있음. 
예를 들어, 데이터베이스 연결 풀을 사용하면 동시에 많은 클라이언트 요청을 처리할 수 있고, 데이터베이스 서버에 대한 과도한 부하를 방지할 수 있음.
 4. 애플리케이션 확장성 : 풀을 사용하면 애플리케이션의 확장성을 향상시킬 수 있음.
풀을 관리하는 컴포넌트를 쉽게 확장하거나 교체하여 시스템의 요구 사항에 맞게 조정할 수 있음.
 5. 안정성 : 풀을 사용하면 시스템이 과부하 상태일 때도 자원을 효율적으로 관리할 수 있음. 이는 시스템의 안정성을 높일 수 있음.

자원 풀링
- 다수의 요청이 동시에 발생하는 경우, 요청마다 자원을 새로 생성하는 것이 아니라 풀에 미리 생선된 자원을 할당하여 사용하고,
작업이 끝나면 자원을 풀에 반환하는 방식으로 동작함. 
=> 이를 통해 성능을 향상시킬 수 있음.

 

 실무를 하다보니.. 새로운 것을 만들기보다는 기존에 있던 것을 유지보수 하거나 기능을 추가하는 일이 많습니다.
아무래도 새로운 모듈을 추가하거나 새로운 것을 처음부터 개발하는 경우는 많이 없었습니다.
 그래서 잊고지내던 친구들을 다시 보고싶어 정리를 결심하게 되었습니다.

1. Node 프로젝트의 'devDependencies' 와 'dependencies' 의 차이.

   1) dependencies (의존성) 
     : 이 카테고리에 있는 패키지는 프로덕션 환경에서 실행될 때 필요한 패키지입니다.
     즉, 실제로 애플리케이션이 동작할 때 사용됩니다. 이러한 패키지들은 애플리케이션의 핵심 로직을 지원하거나 실행에 필수적인 라이브러리임.

    -> 이게 뭔말이냐.. npm install express 를 하면 express 모듈을 사용해서 서버를 구축하잖아요?
그럼 이건 프로덕션 환경(실제 서비스) 에서 사용되는 것이기 때문에 dependencies 에 속하게 되는 거죠..

  2) devDependencies (개발 의존성)
    : 이 카테고리에 있는 패키지는 주로 개발 환경에서 개발자의 작업을 돕거나 테스트, 빌드, 디버깅 등을 지원하는 도구나 라이브러리임. 
    즉, 이러한 패키지들은 실제 프로덕션 환경에서 필요하지 않으며, 주로 개발자가 코드를 작성하고 테스트하는 동안 사용됨.

  -> 이건... 많이들 사용하시는 npm install rand-token  nodemon --save-dev 를 사용하는 경우죠.
그럼 대개 뭐가 있냐.. 위에서 install 한 라이브러리들이 대개 개발자의 작업을 돕는 도구나 라이브러리가 되는 것이죠.

'개발일지 > NODE' 카테고리의 다른 글

[Node.js] generic-pool  (0) 2024.02.11
[Node.js] 에러핸들러 만들어보기  (0) 2023.04.21
[Node.js] 비밀번호 암호화 하기  (0) 2023.04.20
[Node.js] JWT 에러 핸들링  (0) 2023.04.19

 개발을 진행하면서 CRUD만 작성을 하다보니깐 에러를 처리하는 부분이 마음에 들지 않아서!!
특히, catch부분에 ctrl + c , ctrl + v 로 500에러만 나타내주기 보다는 각각의 상황에 맞춰 에러를 핸들링 해주고 싶었습니다.
 구글링을 통해서 best practice를 찾아보고 적용하면서 부족한 부분이 무엇인지에 대해서도 알 수 있었습니다.
next()의 동작과 app.js의 플로우와 app.use를 세밀하게 더 살펴볼 수 있었고 제 코드에 적용할 수 있었던 기회였습니다!

controlloer/user.controller.js

const userService = require('../services/user.service');
const HttpStatusCode = require('../config/httpStatusCode');

module.exports = {
    findAllUser: async (req, res) => {
        try {
            const user = await userService.read();
            if (user.length !== 0) return res.status(400).send({ statusCode: 400, message: "Bad Request" });
            return res.status(200).send({ data: user, statusCode: 200 });
        } catch (error) {
            res.status(500).send({ statusCode: 500, message: "Server Error" });
        }
    }
}

 

결과  

위의 controller의 에러 처리

구글링을 통해 Error Best Practice 찾아봐서 내 코드에 적용해보기

config/httpStatusCode.js

const HttpStatusCode = {
    OK: 200,
    BAD_REQUEST: 400,
    NOT_FOUND: 404,
    INTERNAL_SERVER: 500,
}

module.exports = HttpStatusCode;

error/error.js

const HttpStatusCode = require('../config/httpStatusCode');

class BaseError extends Error {

    constructor(name, httpCode, description, isOperational) {
        super(description);
        Object.setPrototypeOf(this, new.target.prototype);

        this.name = name;
        this.httpCode = httpCode;
        this.isOperational = isOperational;

        Error.captureStackTrace(this);
    }
}
//free to extend the BaseError
class APIError extends BaseError {
    constructor(name, httpCode = HttpStatusCode.INTERNAL_SERVER, isOperational = true, description = 'internal server error') {
        super(name, httpCode, isOperational, description);
    }
}

module.exports = { BaseError, APIError };

 

 

 

middlewares/errorHandler.js

function logError(err) {
    console.error(err)
}

function logErrorMiddleware(err, req, res, next) {
    logError(err)
    next(err)
}

function returnError(err, req, res, next) {
    res.status(err.statusCode || 500).send(err.message)
}

function isOperationalError(error) {
    if (error instanceof BaseError) {
        return error.isOperational
    }
    return false
}

module.exports = {
    logError,
    logErrorMiddleware,
    returnError,
    isOperationalError
}

자 그러면, 기존의 Controller에 적용을 하기 전에 app.js에서 middleware를 app.use()를 통해 등록해줍니다.

const express = require('express');
const userRouter = require('./routes/userRouter');
const logger = require('./logger/logger');
const { APIError } = require('./errors/error');
const { logError, returnError, logErrorMiddleware } = require('./middlewares/error1');


const app = express();

app.use(express.json());

app.use('/user', userRouter);
app.use(APIError);
app.use(logError);
app.use(logErrorMiddleware)
app.use(returnError);
app.use(logHandler);

app.listen(3030, () => {
    logger.info('server is running!');
});

Controller에 적용해보기

const userService = require('../services/user.service');
const HttpStatusCode = require('../config/httpStatusCode');
const { APIError } = require('../errors/error');

module.exports = {
    findAllUser: async (req, res, next) => {
        try {
            const user = await userService.read();
            if (user.length !== 0) throw new APIError("Bad Request", HttpStatusCode.BAD_REQUEST, true, "Bad Request");
            return res.status(200).send({ data: user, statusCode: 200 });
        } catch (error) {
            next(error);
        }
    }
}

 

결과 

예제 적용한 후의 결과
log

참고 사이트 : https://sematext.com/blog/node-js-error-handling/

자료구조 Hash에서 정리했듯이 crypto를 활용하여 비밀번호를 암호화 하는 방식에 대해서  왜 사용을 하였는지에 초점을 두고 정리를 하고자합니다.

솔트(Salt)란?

암호학에서 솔트는 데이터, 비밀번호, 통과암호를 해시 처리하는 단방향 함수의 추가 입력으로 사용되는 랜덤 데이터입니다.

솔트(Salt)는 왜 사용한 것인가요?

예를 들어, 'jeong-park' 이라는 스트링을 sha512로 해시 해보겠습니다.
"2C1E444BE95BCB0236B7C82FDA667EC441B7A7E087898EA3299512675FADB1D65106905CEF0F757CB0D96A1AB82F97B83D3C64A8F30052893E6A8EC46D5935AA" 
위처럼 나옵니다. 제가 아닌 다른 사람들이 'jeong-park'이라는 스트링을 입력하여 sha512로 해시하면 똑같이 나옵니다.

항상 똑같은 결과값을 반환한다면?
해시 함수를 사용하여 변환 가능한 모든 해시 값을 저장시켜 놓고 저장시켜 놓으면 원래의 비밀번호를 추출(레인보우 테이블)해낼 수 있지 않을까요?
이러한 문제 때문에 Salt을 사용하며, 또한 Key Stretching 이라는 친구를 사용합니다.
 
* Key Stretching은 해시된 값을 또 다시 해시 함수의 입력 값으로 넣어서 해싱을 반복하는 것입니다. 이런 방식으로 무작위 대입을 통해 해시 값을 구하는 시간을 늘려서 레인보우 테이블이나 무차별 대입 공격을 어렵게 하기 위함이라고 합니다.

1. createSalt : randomBytes 메소드로 64바이트 길이의 salt를 생성합니다. new_salt는 버퍼형식으로 나오기 때문에 toString("base64")을 사용하여 리턴해줍니다.
2. createHashedPassword : pbkdf2(유저가 입력한 비밀번호, 만들어준 솔트값, 키스트래칭, 비밀번호의 길이, 해시함수) 입니다. 

(유저가 입력한 비밀번호 + 솔트 값) => Hash 값을 * Key Stretching 만큼 반복 하여 = HashedPassword를 만들어 줍니다.

const crypto = require('crypto');
const util = require('util');

const pbkdf2Promise = util.promisify(crypto.pbkdf2);
const randomBytesPromise = util.promisify(crypto.randomBytes);

module.exports = {

    /**
     * Salt Create 
     * @returns String
     */
    createSalt: async () => {
        const new_salt = await randomBytesPromise(64);
        return new_salt.toString("base64");
    },

    /**
     * Create HashedPassword
     * @param {String} password 
     * @returns String, String
     */
    createHashedPassword: async (password, currnet_salt) => {
        const salt = currnet_salt;
        const key = await pbkdf2Promise(password, salt, Number(process.env.KEY_STRETCHING), 64, "sha512");
        const hashedPassword = key.toString("base64");
        return { hashedPassword, salt };
    },

    /**
    * verifyPassword
    * @param {String} password 
    * @param {String} userSalt 
    * @param {String} userPassword 
    * @returns boolean
    */
    verifyPassword: async (password, userSalt, userPassword) => {
        const key = await pbkdf2Promise(password, userSalt, Number(process.env.KEY_STRETCHING), 64, 'sha512');
        const hashedPassword = key.toString('base64');
        if (hashedPassword === userPassword) return true;
        return false;
    }
}

 

참고 사이트 : https://ko.wikipedia.org/wiki/%EC%86%94%ED%8A%B8_(%EC%95%94%ED%98%B8%ED%95%99)


  토큰을 발행해준 후, verify 과정에서 Invalid, expired를 처리해줘야 할까 고민이 들었습니다. 그래서 상황에 맞는 시나리오를 그리고 어떻게 해결해 나갈 것인지 코드를 작성하였습니다.

시나리오

1. 로그인을 하면 AccessToken과 RefreshToken을 발행합니다.
 - AccessToken은 Bearer, RefreshToken은 Redis에 저장을 해줍니다.

2. 사용자가 인증일 필요한 API에 접근하고자 할 때, 검증 미들웨어(authMiddleware)를 통해 검사함.
 

  • Case 1 : AccessToken 만료, RefreshToken 만료  : Error
  • Case 2 : AccessToken 만료, RefreshToken 유효 : AccessToken 재발급
  • Case 3 : AccessToken 유효, RefreshToken 만료 : 로그인 재요청
  • Case 4 : AccessToken 유효, RefreshToken 유효 : next

3. 로그아웃

  • AccessToken, RefreshToken 삭제시키기.

🛠Advanced Feacture🛠

const authMiddleware = {

    checkToken: async (req, res, next) => {

        try {
            // accessToken이 만료된 경우에는 새로 발급 받은 accessToken을 받아옵니다. 그 외에는 null로 넘겨줬습니다.
            const { accessToken, result } = await authService.verifyToken(req);
			
            //result를 통해서 ( "(AccessToken||RefreshToken)_(INVALID||EXPIRED)" 를 받아옵니다.
            
            //AccessToken이 변조된 경우
            if (result === ACCESSTOKEN_INVALID) {
                return res.status(403).send({ err: "accessToken is invalid", statusCode: 403, msg: 'Forbidden' });
            }
            //RefreshToken이 변조된 경우
            else if (result === REFRESHTOKEN_INVALID) {
                return res.status(401).send({ err: "refreshToken is invalid", statusCode: 401, msg: '재로그인 해주세요' });
            }
            //AccessToken이 만료된 경우            
            else if (result === ACCESSTOKEN_EXPIRED) {
            	//새로 받아온 accessToken을 Bearer + 새로 발급받은 accessToekn을 넣어줍니다.
                res.header('AccessToken', "Bearer " + accessToken);
                return next();
            }
            //RefreshToken이 만료된 경우
            else if (result === REFRESHTOKEN_EXPIRED) {
                return res.status(401).send({ statusCode: 401, msg: '재로그인 해주세요' });
            }
            //둘다 유효한 경우에는 기존의 accessToken을 넘겨줍니다.
            res.header('AccessToken', "Bearer " + accessToken);

            return next();
        } catch (error) {
        	//둘다 만료인 경우에는 에러
            error.status = 500;
            error.msg = 'token is not found';
            next(error);
        }
    }
}

 

 

 

플로우

코드를 작성해 나가면서 위의 플로우처럼 모든 시나리오를 수행하는 방법도 있을 수 있지만, 다른 방법이 있지 않을까라는 생각이 들었습니다.  

예를 들어, 토큰이 만료되었다고 클라이언트 측에서 재발급 요청을 한다거나 하는 그런 플로우도 가능하지 않을까 생각을 했습니다.
하지만, 그렇게되면 불필요한 요청이 많아지는 것을 확인할 수 있었습니다.

+ Recent posts