포시코딩

[Sequelize] 트랜잭션(Transaction) 본문

Node.js

[Sequelize] 트랜잭션(Transaction)

포시 2023. 1. 15. 11:09
728x90

개요

A의 포인트를 차감하고 B의 포인트를 올려주는 로직이 있을 때

A의 포인트를 차감했는데 어떤 이유로 인해 B의 포인트를 올려주지 못하는 에러가 발생했을 때

수습하기 힘든 문제가 발생할 것이다. 

 

하지만 만약 두 행동에 대해 트랜잭션으로 묶여 있었다면

A의 데이터가 변화한 뒤에 문제가 생겨도 트랜잭션의 롤백 처리로 인해 

트랜잭션으로 묶인 모든 행동들을 없던 일로 만들 수 있다.

 

때문에 위와 같은 결제 처리 관련 로직에서 많이 사용하며 

한번에 연속적으로 DB에서 수행되야 하는 일들에도 트랜잭션을 사용한다.

 

나는 프로젝트에서 order를 등록할 때 포인트가 차감되야 하는 상황을 트랜잭션으로 묶어볼 것이다.

 

사용 방법

기존의 코드를 보면 다음과 같다.

변경 전

service

// ...생략
await this.ordersRepository.createOrder(ownerId, kinds, details, pickup, imageUrl);
await this.ordersRepository.pointDeduct(ownerId, 10000);
// ...생략

repository

createOrder = async (ownerId, kinds, details, pickup, imageUrl) => {
  await this.ordersModel.create({ ownerId, kinds, details, pickup, imageUrl });
};

pointDeduct = async (id, point) => {
  await this.usersModel.decrement({ point }, { where: { id } });
};

 

repository에 order 저장 메소드와 포인트 차감 메소드가 각각 존재하고

service에서 하나씩 불러 처리하고 있다.

이 경우 order는 등록됐는데 서버에 문제가 발생하면 포인트는 차감되지 않았는데 order만 등록되어 있을 것이다.

 

해야할 일을 정리해보았다.

  1. service에서 createOrder를 호출할 때 repository에서 두가지 로직을 모두 수행하게 변경
  2. sequelize의 decrement보다는 먼저 해당 유저가 있는지 확인한다음 point 값을 변경해서 저장하는 save() 함수 사용
  3. repository의 메소드를 호출할 때 orderInfo 객체를 만들어 데이터 전달

위 사항을 적용한 결과는 다음과 같다.

 

변경 후

service

createOrder = async (ownerId, kinds, details, pickup, imageUrl) => {
    try {
      const orderInfo = {
        ownerId,
        kinds,
        details,
        pickup,
        imageUrl,
      };
      await this.ordersRepository.createOrder(orderInfo);

      return { code: 201, message: '주문에 성공하였습니다.' };
    } catch (err) {
      return { code: 403, message: err.message };
    }
};

repository

const { sequelize } = require('../sequelize/models/index');

// ...생략
createOrder = async ({ ownerId, kinds, details, pickup, imageUrl }) => {
    const transferPoint = 10000;

    const transaction = await sequelize.transaction();
    try {
      const userInfo = await this.usersModel.findOne(
        {
          attributes: ['id', 'point'],
          where: {
            id: ownerId,
          },
        },
        { transaction }
      );

      if (!userInfo) {
        throw new Error('유저가 존재하지 않습니다.');
      }

      await this.ordersModel.create(
        {
          ownerId,
          kinds,
          details,
          pickup,
          imageUrl,
        },
        { transaction }
      );

      userInfo.point -= transferPoint;
      if (userInfo.point < 0) {
        throw new Error('유저의 포인트가 부족합니다.');
      }
      await userInfo.save({ transaction });

      await transaction.commit();
    } catch (err) {
      await transaction.rollback();
      throw new Error(err.message);
    }
};

 

각각의 sequelize 행동들에서 transaction을 요소로 전달하고 

잘못된 상황일 시 throw를 통해 catch로 넘겨 rollback 시킨 다음

다시 service의 catch에서 받을 수 있게 throw 하는 모습을 볼 수 있다.

 

정리하며

sequelize에는 managed transaction과 unmanaged transaction 두가지 종류가 있는데

managed는 try문에서 정상적으로 끝나면 자동으로 commit 해주고 catch문으로 넘어가게 되면 자동으로 rollback 해준다.

unmanaged는 반대로 수동으로 commit과 rollback을 해줘야 하는데 

 

처음 써보는 기능이기도 하고 이런 민감한건 수동으로 직접 원하는 위치에서 처리해줘야 안전할 것 같아 

unmanaged 하는 방법을 사용했다.

 

트랜잭션이라는 개념은 잘 알고 있었지만 직접 사용해본건 처음인데

생각보다 구현이 간단해서 놀랐다. 왜 이제야 사용했지..

 

앞으로도 쓰일 일이 많을 것 같은데 이제라도 구현해봐서 다행이다.

 

 

 

 

도움받은 곳

https://velog.io/@leesilverash/데이터베이스-트랜잭션-사용법-sequelize

 

[데이터베이스] 트랜잭션 사용법 node.js

Unmanaged transactionsUnmanaged transactions는 사용자가 커밋과 롤백을 직접 작성해야 한다.Managed transactionsManaged transactions는 Sequelize가 에러가 발생했을 때 자동으로 트랜잭션을 롤백해주며

velog.io

728x90