[MySql] mysql2 사용하여 transaction 처리하기

해달·2023년 5월 14일
2
post-thumbnail

서두

필요할 때마다 수동으로 데이터베이스에 직접 쿼리를 실행시키는 케이스가 있었는데,
이를 데일리로 동작되게 끔 만들어야 했다.

쿼리 내용은
1. 테이블 전체의 값을 DELETE 한 뒤,
2. 업데이트 된 값으로 INSERT INTO를 해주어야 했는데

delete 구문이 테이블의 값을 전부 삭제하는 중요한 작업이여서
삭제 -> 삽입으로 쿼리가 진행되는 중에
에러가 발생 할 경우 실행했던 쿼리를 롤백시키는 작업이 필요했다.

이를 위한 작업을 한 내용중에서 알게 된 내용들이 많아 정리해놓으려고 한다.


수동으로 실행시켰던 sql query

START TRANSACTION;
1. 실행쿼리
2. 실행쿼리
COMMIT; // 성공 후 커밋처리

1. START TRANSACTION
SQL 트랜잭션을 시작한다.

  • 트랜잭션 : DB에서 실행되는 작업들을 그룹화 하는 작업단위

2. 실행쿼리
실제로 작업이 이루어지는 쿼리
delete, insert into 쿼리 문이 들어간다.

3. COMMIT
트랜잭션 내의 모든 작업이 성공적 이루어졌다면,
이 명령어를 실행하여 변경사항을 DB에 반영한다.


코드에서 동작하는 트랜잭션 처리!

위와 같은 로직을 지키면서 팀장님이 Mysql2 패키지에서는 어떻게 동작하는 확인하고 작업해야 한다해서 mysql2 라이브러리에서 사용 되는 방식을 찾아 적용시켰다.

우선 db와 연결해주기 위해 createPool로 연결시켜준다

둘의 차이점

  • createPool : 연결 풀을 생성하여 연결을 관리하고 재사용 함, 동시 요청 및 트랜잭션 관리가 필요한경우에는 pool을 사용한다.
    • pool : 여러개의 커넥션을 가지고 있는 것으로 이해할 수 있다.
      커넥션은 DB에 연결되어 있는 상태를 나타내고, 풀은 이러한 커넥션들을 관리하는 역할도 한다.
    • 커넥션(chatGPT) : 커넥션은 애플리케이션과 데이터베이스 간의 통신 통로로서, 애플리케이션이 데이터베이스에 쿼리를 보내고 결과를 받을 수 있도록 도와줍니다. 커넥션은 데이터베이스에 접속하는 과정을 거친 후, 애플리케이션과 데이터베이스 간의 실시간 연결을 유지합니다
  • createConnection : 데이터베이스와 연결된 커녁센 객체를 하나를 만든다. 단일 연결을 생성

커넥션을 하나만 가지고 있게 될 경우 트랜잭션 처리에 대한 이슈가 발생할 수 있다.
하나의 커넥션만을 가지고 여러 요청을 보내게 된다면 동시에 트랜잭션을 시작하거나 커밋/롤백을 독립 수행할 수 있게 된다.
가령, A와 B라는 두 개의 요청이 동시에 트랜잭션을 시작했을 때,
A가 커밋되고 B만 롤백된다면 이는 데이터의 일관성이 깨질 수 있다.
따라서 요청 단위로 커넥션을 만들어 독립적으로 관리하는 것이 중요하다.

그러하기 때문에 풀을 사용하여 여러 커넥션을 관리하는것이 일반적으로 권장되는 방법이다.

createPool로 연결 풀을 생성하고 연결 된 풀에서 커넥션을 꺼내어 사용해야 한다.

import mysql from 'mysql2/promise';
import { DBHostType, dbConfig } from '../../utils/db';

export const updateData = async (dbHost: DBHostType) => {
  const pool = mysql.createPool(dbConfig(dbHost));
  const conn = await pool.getConnection();
  await conn.beginTransaction();
  
  const UPDATE_TEST_QUERY = 
        `update test  
 	 	 set t1 = 123
         where t2 = 'test' `;
  
// 에러가 발생하는 쿼리
  const UPDATE_TEST_QUERY2 = ''
  
  if (conn) {
    try {
      await conn.query(UPDATE_TEST_QUERY);
      await conn.query(UPDATE_TEST_QUERY2); // 에러발생
      await conn.commit();

      
    } catch (err) {
      console.error(`Transaction error: ${err.message}`);
      await conn.rollback();
    } finally {
      conn.release();
    }
  }
};

pool을 생성하고 커넥션을 가져와 트랜잭션을 시작한다.

연결 된 커넥션이 있으면
쿼리를 실행시키고 완료하면 커밋되도록 작성하였다.
중간에 에러가 발생한다면 rollback 처리 되도록 하였다.

만약 첫번째 쿼리가 실행되고 두번째쿼리에서 에러가 났다면 첫번째 성공한 쿼리의 결과는 디비에 저장되지 않는다!

로직이 실행된 뒤 마지막에는 finally 로 relases로 db 연결을 해제해주었다.

검증해본 test case

에러를 발생시키는 쿼리를 고의로 작성해서 아래 케이스로 검증해보고 작업을 마무리 했다!

  • 참조가능한 fk 없는 경우 update 하려했을 때 트랜잭션 에러 후, 정상 쿼리인 update 구문 롤백 확인

  • 참조가능한 fk 없는 경우 insert into 하려했을 때 insert into 에러 후 , 정상 쿼리인 update 구문 롤백 확인


그 외

처음에는 아래와 같이 각 행위들을 함수들을 다 쪼개놨었다
만약 에러가 발생한다면 어디서 발생하는지 명확하게 보고싶어 나뉘어놨는데 지금과 같은 코드로 작성해놓아도
에러가나면 어디에서 에러가났는지 확인이 쉬운 케이스였기때문에 팀장님이 필요없이 너무 높은 추상화가 되어있는거 같다 말씀하셨다.
코드의 가독성도 고려했을 때 모든 함수를 추상화 할 필요는 없다고 생각되어 현재의 코드로 변경되었다.

수정 전 코드


import mysql from 'mysql2/promise';
import { DBHostType, dbConfig } from '../../utils/db';

const startTransaction = async (dbHost: DBHostType) => {
  try {
    const pool = mysql.createPool(dbConfig(dbHost));
    const conn = await pool.getConnection();

    // 트랜잭션 시작
    await conn.beginTransaction();

    return conn;
  } catch (error) {
    console.error(`Transaction start error: ${error.message}`);
    return null;
  }
};

const commitTransaction = async (conn) => {
  try {
    // 커밋
    await conn.commit();
  } catch (error) {
    console.error(`Transaction commit error: ${error.message}`);
    await rollbackTransaction(conn);
  } finally {
    releaseConnection(conn);
  }
};

const rollbackTransaction = async (conn) => {
  try {
    // 롤백
    await conn.rollback();
  } catch (error) {
    console.error(`Transaction rollback error: ${error.message}`);
  } finally {
    releaseConnection(conn);
  }
};

const releaseConnection = async (conn) => {
  try {
    // 연결 해제
    await conn.release();
  } catch (error) {
    console.error(`Connection release error: ${error.message}`);
  }
};

export const updateSkuOnSalesTable = async (dbHost: DBHostType) => {
  const conn = await startTransaction(dbHost);

  if (conn) {
    try {
      await conn.query(``);
      await conn.query(``);
      await commitTransaction(conn);
 
    } catch (err) {
      console.error(`Transaction error: ${err.message}`);
      await rollbackTransaction(conn);
    }
  }
};

마치며,

db 작업을 하면서 트랜잭션처리를 하는 일이 없어 이번 기능을 구현하며,
pool 사용을 처음해보게되며 두개의 차이점을 알게 되었고 필요에 따라 사용할 수 있을 거 같다! 모르던 내용을 하나 더 알게 된거 같아서 기뻤다.

reference

0개의 댓글