ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TIL] 스파르타) Node.js 게임서버 주특기 플러스 3주차 강의 수강 (DB, connection pool, migration)
    TIL-sparta 2024. 6. 28. 18:47

     

     > 주특기 플러스 3주차 3-8과 3-9 강의에서 다뤄진 DB 연동 및 마이그레이션 관련 내용을 정리해보았다.

     

    학습 키워드: DB, MySQL, Node.js, mysql2, connection pool, migration

     

    1. DB Connection & Connection Pooling

    1) What is it?:

     커넥션 풀링(Connection Pooling)은 이전에 학습했던 쓰레드 풀링과 같은 개념으로 데이터베이스와의 연결 여러 개를 미리 생성하여 풀에 저장해두고 필요할 때 마다 꺼내서 쓰는 방식이다. 연결을 캐싱해둔다고 이해하면 되겠다. 미리 생성해둔 연결을 사용하기 때문에 응답 시간이 감소하는 효과가 있다고 한다.

     

     

    2) How does it work?:

    // db/database.js
    
    import mysql from 'mysql2/promise';
    import { config } from './config.js';
    import { formatDate } from '../utils/dateFormatter.js';
    
    const { databases } = config;
    
    // 데이터베이스 커넥션 풀 생성 함수
    const createPool = (dbConfig) => {
      const pool = mysql.createPool({
        host: dbConfig.host,
        port: dbConfig.port,
        user: dbConfig.user,
        password: dbConfig.password,
        database: dbConfig.name,
        waitForConnections: true, // 연결 대기 여부
        connectionLimit: 10, // 커넥션 풀에서 최대 연결 수
        queueLimit: 0, // 0일 경우 무제한 대기열
      });
      
      // 여기에서 pool.query override
    
      return pool;
    };
    
    // 여러 데이터베이스 커넥션 풀 생성
    const pools = {
      GAME_DB: createPool(databases.GAME_DB),
      USER_DB: createPool(databases.USER_DB),
    };
    
    export default pools;

     위는 강의에서 작성된 코드의 일부다. 맨 위에서 mysql2의 promise를 'mysql2/promise'의 형태로 import하여 사용하는 것을 볼 수 있는데, 이렇게 하면 mysql2 라이브러리에서 지원하는 promise 기반의 API 모음을 가져오게 된다. mysql.createPool() 함수가 pool의 생성을 담당하며, 객체 인자로 여러가지 옵션을 key-value 페어로 정의해줄 수 있다.

     

    ...
    
      const originalQuery = pool.query;
    
      pool.query = (sql, params) => {
        const date = new Date();
        // 쿼리 실행시 로그
        console.log(
          `[${formatDate(date)}] Executing query: ${sql} ${
            params ? `, ${JSON.stringify(params)}` : ``
          }`,
        );
        return originalQuery.call(pool, sql, params);
      };
      
    ...

     Pool 생성을 좀 더 직관적으로 보기 위해 위 코드를 제거했는데, 원본에는 이처럼 pool 객체의 query를 override하여 logging 기능을 추가했다.

     

     

    3) Why use it?:

     DB 커넥션 풀링의 장점은 많은 수의 연결이 동시에 더 많은 처리를 할 수 있어서 동시 처리 능력이 향상되므로 동시 접속자가 많은 환경에서 유리하다. 또한 연결 풀이 많으면 갑작스러운 트래픽 증가에서도 부담이 줄기 때문에 가용성이 뛰어나다.

     

     반면에 단점 또한 존재하는데, 유저가 적을 때도 연결이 유지되기 때문에 사용하지 않는 연결 만큼 자원이 낭비되고, 데이터베이스 서버의 부하가 늘어나기 때문에 성능 저하가 발생할 수 있으며, 네트워크 대역폭 사용량이 증가하기 때문에 네트워크 성능에도 영향을 끼친다.

     

     

    2. DB Migration

    1) What is it?:

     DB Migration은 RDB에서 데이터의 구조를 변경하기 위한 작업을 총칭한다. 예를 들어 테이블을 추가하거나, 컬럼의 추가 제거, 필드의 분리(정규화?), 그리고 필드의 타입이나 constraints를 바꾸는 것 등을 말한다. 강의에서는 테이블을 추가하는 쿼리와 해당 쿼리를 실행하는 JS 파일을 작성했다.

     

    2) How does it work?:

    CREATE TABLE IF NOT EXISTS user
    (
        id         VARCHAR(36) PRIMARY KEY,
        device_id  VARCHAR(255) UNIQUE NOT NULL,
        last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    
    CREATE TABLE IF NOT EXISTS game_end
    (
        id    VARCHAR(36) PRIMARY KEY,
        user_id    VARCHAR(36) NOT NULL,
        start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        end_time   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        score      INT       DEFAULT 0,
        FOREIGN KEY (user_id) REFERENCES user (id)
    );

     위 쿼리들을 통해 테이블을 생성하는 것이 목표인데, 한 가지 알아둬야 할 점은 MySQL은 테이블 생성 쿼리를 동시에 실행할 수 없기 때문에 쿼리를 세미 콜론으로 구분하고 런타임에 split 하여 하나씩 실행되도록 작성했다.

     

    import fs from 'fs';
    import path from 'path';
    import pools from '../database.js';
    import { fileURLToPath } from 'url';
    
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = path.dirname(__filename);
    
    const executeSqlFile = async (pool, filePath) => {
      const sql = fs.readFileSync(filePath, 'utf8');
      const queries = sql
        .split(';')
        .map((query) => query.trim())
        .filter((query) => query.length > 0);
    
      for (const query of queries) {
        await pool.query(query);
      }
    };
    
    const createSchemas = async () => {
      const sqlDir = path.join(__dirname, '../sql');
      try {
        // USER_DB SQL 파일 실행
        await executeSqlFile(pools.USER_DB, path.join(sqlDir, 'user_db.sql'));
    
        console.log('데이터베이스 테이블이 성공적으로 생성되었습니다.');
      } catch (error) {
        console.error('데이터베이스 테이블 생성 중 오류가 발생했습니다:', error);
      }
    };
    
    createSchemas()
      .then(() => {
        console.log('마이그레이션이 완료되었습니다.');
        process.exit(0); // 마이그레이션 완료 후 프로세스 종료
      })
      .catch((error) => {
        console.error('마이그레이션 중 오류가 발생했습니다:', error);
        process.exit(1); // 오류 발생 시 프로세스 종료
      });

     앞서 설명한 커넥션 풀을 import 하고, 기존에 사용하던 파일을 읽어들이는 코드를 가져와 쿼리 파일을 읽어오도록 수정하고, 불러온 파일의 내용물을 세미 콜론을 delimeter로 split하여 뽑아낸 쿼리를 하나씩 실행해주는 방식이다. 자세히 보면 export를 사용하지 않고 있는데, 이 코드는 어디에 추가하지 않고 package.json에 따로 실행 script를 추가하여 사전에 한 번 실행해두는 식으로 사용한다. 서버를 실행할 때 마다 migration을 할 이유는 없기 때문이다.

     

     

    3) Why use it?:

     데이터베이스의 구조를 파일로 관리할 수 있어서 변화를 추적하거나 관리하는 것이 쉬워진다는 장점이 있다. 위에서 작성한 쿼리는 테이블이 없을 때 생성만 해주지만 실제로 컬럼의 추가나 element 제거 같은 기타 migration 작업을 하게되면 데이터가 유실될 위험이 있다고 하니 사용 시 항상 주의해야한다는 것이 단점이라면 단점이다. Prisma 같은 ORM을 사용할 때 편리하게 사용하던 기능이지만 이번 강의에서는 ORM을 사용하지 않았을 때 어떤 식으로 작성하는지 예시를 보여주기 위함이었던 것 같다.

     

     

    --

     

    REFERENCES:

     

    https://teamsparta.notion.site/3-DB-18f3cb1dfb5149998ab64809bf136c26

     > 강의 노트, "[게임서버개발 플러스] 실전 게임 서버만들기 (3) - DB" by 조호영 튜터님, accessed: 2024-06-28

    https://sidorares.github.io/node-mysql2/docs/documentation/promise-wrapper

     > MySQL2, "Promise Wrappers"

    https://en.wikipedia.org/wiki/Connection_pool

     > Wikipedia, "Connection Pool", accessed: 2024-06-28

    https://www.prisma.io/dataguide/types/relational/what-are-database-migrations

     > Prisma, "Database Migrations", accessed: 2024-06-28

    728x90
Designed by Tistory.