ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [DB] 트랜잭션 사용하기 in node.js + Error해결
    TIL 2024. 3. 26. 18:31

     

     

    프로젝트 내에서 배너이미지 등록API를 개발하다가 트랜잭션이 필요해져서 구현하게되었다.

     

    배너이미지 등록 API는 [기존 배너이미지 삭제]와 [새로운 배너이미지 등록] 작업이 DB에서 둘다 이루어져야 한다.

     

    만약 [기존 배너이미지] 작업은 성공하고, [새로운 배너이미지 등록] 작업이 실패한다면, 등록된 배너이미지만 사라져 버리

     

    는 문제가 발생한다. 

     

    • 2가지 작업 모두 실패하여 기존 이미지만 남아있게 하기,
    • 2가지 작업 모두 성공하여 기존이미지 삭제후, 새로운 이미지로 대체하기

    경우의 수를 이 2가지로 만들기 위해서 트랜잭션을 이용해야했다.

     

     

     

    트랜잭션)

    pool.query('BEGIN'); //트랜잭션 시작지점설정
    pool.query('COMMIT'); //트랜잭션 끝나는지점설정
    pool.query('ROLLBACK'); //에러발생시 트랜잭션 롤백

     

     

     

     

     

    const { pool } = require('../config/postgres');
    
    //배너이미지 등록(수정) API
    router.post(
        '/game/:gameidx/banner',
        checkLogin,
        checkAdmin,
        uploadS3.array('images', 1),
        async (req, res, next) => {
            const gameIdx = req.params.gameidx;
    
            try {
                const location = req.files[0].location;
    
                await pool.query(`BEGIN`); // 트랜잭션시작지점
    
                //기존배너이미지 삭제
                await pool.query(
                    `UPDATE 
                        game_img_banner
                    SET 
                        deleted_at = now()
                    WHERE 
                        game_idx = $1
                    AND 
                        deleted_at IS NULL`,
                    [gameIdx]
                );
                //새로운배너이미지 추가
                await pool.query(
                    `INSERT INTO
                        game_img_banner(game_idx, img_path)
                    VALUES
                        ($1, $2)`,
                    [gameIdx, location]
                );
                await pool.query(`COMMIT`);// 트랜잭션 끝지점
    
                res.status(201).send();
            } catch (e) {
                await pool.query(`ROLLBACK`); // 에러발생시 트랜잭션 시작점으로 롤백시키기
                next(e);
            } 
        }
    );

     

     

     

    이와같이 트랜잭션을 적용해보았다

     

     

     

     

    하지만 트랜잭션이 적용된 이 API를 실행시켜보면 트랜잭션이 적용되지 않는 것을 알 수 있다.

     

    왜그럴까?

     

     

    기존에 DB를 이용할때 connection pool 방식을 이용했다.

     

    이는 DB연결이 필요할때마다 pool에서 커넥션을 하나 빌려와 사용하고 다시 반납하는 방식이다.

     

     

    그렇다면 순차적으로 코드가 실행될때, 1번 SQL이 실행되고 커넥션 반납하고

     

    다시 2번 SQL 실행이 되어 커넥션을 다시 빌려올 것이다.

     

    즉, 1번 SQL과 2번 SQL이 서로 다른 커넥션을 이용하게 되기때문에 트랜잭션이 적용되지않는다.

     

    그래서 하나의 커넥션을 계속 유지하여 트랜잭션 내의 SQL문들을 같은 커넥션에서 실행하게 하는 것이 필요하다.

     

     

    //하나의 커넥션을 변수에 저장하기
    const poolClient = pool.connect();
    
    poolClient.query('SQL');
    
    poolClient.release();

     

     

    pool에서 커넥션을 하나 가져와 변수에 저장해놓고 계속 사용하는 방법을 이용하였다.

     

     

     

    const { pool } = require('../config/postgres');
    
    //배너이미지 등록
    router.post(
        '/game/:gameidx/banner',
        checkLogin,
        checkAdmin,
        uploadS3.array('images', 1),
        async (req, res, next) => {
            const gameIdx = req.params.gameidx;
            let poolClient;
    
            try {
                const location = req.files[0].location;
    	// poolClient변수에 풀 커넥션을 가져와 저장하기
                poolClient = await pool.connect();
                await poolClient.query(`BEGIN`);
    
                //기존배너이미지 삭제
                await poolClient.query(
                    `UPDATE 
                        game_img_banner
                    SET 
                        deleted_at = now()
                    WHERE 
                        game_idx = $1
                    AND 
                        deleted_at IS NULL`,
                    [gameIdx]
                );
                //새로운배너이미지 추가
                await poolClient.query(
                    `INSERT INTO
                        game_img_banner(game_idx, img_path)
                    VALUES
                        ($1, $2)`,
                    [gameIdx, location]
                );
                await poolClient.query(`COMMIT`);
    
                res.status(201).send();
            } catch (e) {
                await if(poolClient) poolClient.query(`ROLLBACK`);
                next(e);
            } finally {
                if (poolClient) poolClient.release();
            }
        }
    );

     

     

    이렇게 poolClient = pool.connect()를 이용하니 트랜잭션이 잘 적용하는 것을 확인할 수 있었다.

     

     

     

     

    주의할점)

    1. pool.connect()를 이용하니, release()를 이용하여 커넥션을 반납해야한다.
    2. try 문안에서 const poolClient = pool.connect()를 사용할 경우, 지역변수로 생성되어  poolClient를 try문 밖에서 이용할 수 없다. try문 밖에서 선언하여 전역변수로 만들어줘야한다.
    3. finally{}에 poolClient.release()만 넣을 경우, poolClient = await pool.connect(); 이 연결 부분에서 에러가 생기면 poolClient가 널값이되고, finally{}에서는 널포인트엑셉션 에러가 발생해서 서버가 종료된다. 그래서 이러한 경우를 방지하기위해  finally {}, catch{} 에 if (poolClient) poolClient.release(); poolClient가 있을때만 release를 해줘야한다.

     

     

Designed by Tistory.