개요 🙋

보통 프로젝트를 진행하다보면,
설계된 API 를 호출하기 위해 로직을 작성하는데
그 로직 안의 내용은 보통 아래와 같을것이다.

const db = require('../loaders/db');

module.exports = {
  getBrandData: async () => {
    const connection = await db();
    const [rows] = await connection.query(
      'SELECT brand_name, name, caffeine FROM brand'
    );
    connection.end();
    return rows;
  }
};

로직을 살펴보면,
외부에 정의된 데이터베이스초기화 로직을 불러와 connection 변수에 비동기로 담아 사용한다.

DB 에 접근이 성공하게되면,
쿼리문을 날려 명령을 수행한 뒤 불필요한 리소스 낭비를 방지하기 위해 DB 의 연결을 해제한다.

이와 같은 방식을 단일 커넥션 이라고 하며,
각 클라이언트 또는 사용자에 대해
하나의 데이터베이스 연결(createConnection) 만을 유지하는 방식의 개념을 갖는다.

그럼 이와 반대되는 개념인 커넥션 풀 에 대해
장, 단점을 알아보고 이를 로직에 적용해 보고자 해당 포스팅을 시작한다.


단일 커넥션 (Single Connection) 👼

위에서 간략하게 설명했듯,
단일 커넥션의 경우 작은 규모의 프로젝트에서 주로 채택되는 방식이다.

장점 👍

  • 간단한 구현
    👉 작은 규모의 애플리케이션에서는 그에 상응하는 간단한 구현이 가능하다.
  • 적은 리소스
    👉 연결을 생성하고 필요한 만큼만 사용 후 즉시 닫을 수 있음
  • 간단한 코드
    👉 커넥션 관리가 간단하여 코드가 간결해짐

단점 👎

  • 동시 접속
    👉 동시에 많은 접속이 발생 할 경우 유연한 대체가 불편하다.
  • 생명주기 관리
    👉 트랜잭션의 생명주기가 다양하고 길어질 경우 커넥션을 효과적으로 관리하기 어려움.

커넥션 풀 (Connection Pool) 💫

장점 👍

  • 동시 접속
    👉 여러 사용자의 동시 접속을 효과적으로 처리 가능.
  • 리소스
    👉 미리 생성된 커넥션을 재 사용하여 발생하는 오버헤드를 감소시키고 리소스에도 유의미한 영향을 끼침
  • 연결 지속
    👉 장기적인 연결을 통해 트랜잭션 간 효율적인 데이터 전송 가능

단점 👎

  • 복잡성
    👉 커넥션 풀의 구현 및 관리는 상대적으로 복잡할 수 있다.
  • 리소스
    👉 미리 생성된 커넥션들이 항상 메모리를 차지하고 있음
  • 오버헤드
    👉 일부 상황에서는 커넥션 풀 자체의 오버헤드가 발생할 수 있음.

오버헤드란 ❓
Task 를 처리하기 위해 사용되는 추가적인 비용이나 리소스를 말한다.
기본 작업의 수행을 위한것이 아닌,
시스템 & 프로세스를 관리하고 유지하기 위한 부가적인 요소에서 발생하는 것을 말한다.

장 단점을 확인 해 봤으니,
두 방법을 코드로 구현 해 보며 확인 해 보도록 하자 ✅


Example 💌

createConnection 🔌

require('dotenv').config();
const mysql = require('mysql');
const {
  DB_HOST,
  DB_PORT,
  DB_USER,
  DB_PASSWORD,
  DB_SCHEMA
} = require('../config/index');

const conn = async () => {
    mysql
        .createConnection({
          host: DB_HOST,
          port: Number(DB_PORT),
          user: DB_USER,
          password: DB_PASSWORD,
          database: DB_SCHEMA
        })
        .connect(err => {
          err ? console.log(err) : console.log('DB_CONNECTED');
        });
};

module.exports = conn;

가장 기본적인 방법으로 구현된 DB 와 연결하는 로직이다.
Callback 함수를 사용하기 위해 MySQL2 모듈을 사용하였고,
createConnection 메서드를 호출하여 MySQL 에 대한 커넥션을 생성 해 주었다.

const db = require('../loaders/db');

module.exports = {
  getBrandData: async () => {
    const connection = await db();
    const [rows, fields] = await connection.query(
      'SELECT brand_name, name, caffeine FROM brand'
    );
    connection.end();
    return rows;
  }
};

분리된 코드를 불러와
생성된 DB 에 연결 및 사용 후 end, destory 등으로 커넥션을 제거하여 사용한다.

이와같이 단일 커넥션을 채택하여 사용했을 경우,
위의 단점에서도 언급했듯
커넥션을 삭제하고 재 생성됨에 따라 발생하는 handshake 비용,
프로세스 혹은 스레드 비용이 증가한다는 단점이 존재한다.


createPool 💫

커넥션 풀은 미리 필요한 수 만큼 커넥션을 생성 해 놓고,
사용 후 반납하는 개념이다.

이를 통해 단일 커넥션과는 다르게
커넥션을 다시 생성하는데 사용되는 비용이 소비되지 않는다는 장점이 있다.

const conn = async () => {
  const connection = await mysql.createPool({
    host: DB_HOST,
    port: Number(DB_PORT),
    user: DB_USER,
    password: DB_PASSWORD,
    database: DB_SCHEMA,
    connectionLimit: 10
  });
  return connection;
};

module.exports = conn;

async/await 을 사용하여 MySQL2 모듈의 Promise 기능을 활용했다.

위 로직과 차이점은,
connectionLimit 옵션을 추가함으로 서버에 가해지는 부하를 줄일 수 있는 점 이다.

connectionLimit: 10 은,
동시에 최대 10 개의 연결만 허용하겠다는 의미이며
11 번 째 요청은 Pending 되어 동시에 접근하는 연결을 분산시킬 수 있다.

하지만,
Pool 을 너무 적게 생성 해 놓으면 대기시간 (Pending) 이 발생할 수 있고,
너무 많이 생성 해 놓으면 메모리(리소스) 낭비가 크다는 단점이 있다.

exports.getCoffeeInfoSum = async ({ getReq }) => {
  const conn = await db();
  const getConn = await conn.getConnection();
  const params = [getReq];
  const sql = `
  SELECT SUM(caffeine)
  FROM post
  WHERE user_id = ?
  `;
  const [row] = await getConn.query(sql, params);
  getConn.release();
  return row;
};

마찬가지로 분리된 코드를 불러와
생성된 DBgetConnection 메서드를 사용하여
가져온 커넥션에 쿼리문을 날리고 release 메서드를 통해 연결된 커넥션을 풀 에 반납하여 사용한다.

getConnection 는,
MySQL 커넥션 풀 에서 새로운 연결을 가져오는 메서드 이다.


결론 🤹‍♂️

Node.js 환경에서 MySQL 모듈을 사용하여 DB 에 접근하는 두 가지의 방법을 살펴봤는데,

작은 규모의 애플리케이션이나
간단한 작업에서는 단일 커넥션으로 구현하는 것이 보다 효율적인 부분이 있으며

대규모 애플리케이션이나
웹 서버, 데이터베이스에 잦은 요청이 요구된다면,
커넥션 풀 을 사용하여 성능을 최적화 할 수 있을 것 같다.

단,
커넥션 풀은 대부분의 상황에서 권장되는 방법이다.

클라이언트가 동시에 서비스에 접근하거나 웹 앱의 경우와 같이 많은 수의 요청이 들어오는 상황에서
자원을 효율적으로 관리하고 성능을 최적화시킬 수 있기 때문이다.

이 글을 정리하며,
실제 단일 커넥션으로 구현되어있던 로직을 👉 커넥션 풀로 리펙토링하여 사용중 이다.


Reference 🌊

https://velog.io/@wngud4950/MySQL-%EB%8B%A8%EC%9D%BC-Connection-VS-Connection-Pool-%EB%B0%A9%EC%8B%9D%EC%9D%98-%EC%B0%A8%EC%9D%B4
https://velog.io/@gwon713/Nodejs-MySQL-DB-connection-pool
https://techbless.github.io/2020/01/17/Node-js%EC%97%90%EC%84%9C-Mysql-Connection-Pool-%EC%9D%B4%EC%9A%A9%ED%95%98%EA%B8%B0/
https://m.blog.naver.com/collcr/222869575871