백엔드 개발자(node.js)가 되는 과정

Sequelize로 MySQL 트랜잭션 적용하기

soopy 2023. 7. 11. 11:21
728x90

managed와 Unmanaged 트랜잭션

sequelize에서 Transaction는 어떻게 사용할 수 있는지 알아보자

sequelize에서 트랜잭션을 적용하는 방법은 크기 Managed 방식과 Unmanaged 방식으로 나뉜다.

// Managed
const { sequelize, User } = require('../models');

// 트랜잭션의 콜백으로 유저 생성 로직을 처리합니다
const result = await sequelize.transaction(async (t) => {
  const user = await User.create(
    {
      nickname: 'apple',
      age: 18,
    },
    { transaction: t } // 해당 로직이 트랜잭션으로 묶여있음을 명시함
  );

  return user;
});

위 코드에서 보는 것과 같이 단순히 models/index.js에서 sequelize를 import한 뒤 기존에 적용하던 유저 생성 로직을 콜백 함수로 감싸주는 것을 확인할 수 있다.
unmanaged와 비교하면 금방 알게 되겠지만 위 코드에서 요청 성공 또는 에러에 따른 COMMITROLLBACK 명령이 따로 명시되어 있지 않지만 그럼에도 자체적으로 동작한다. 그래서 코드를 간결하게 작성할 수 있다는 장점이 있다.
하지만 에러 상황 외 롤백이 필요한 로직을 추가해야할 경우 어떻게 짜야 하는가? 에 대한 문제점이 있다. 가령 User 생성 후 생성이 잘 되었는가에 대한 검증을 거친다고 가정했을 때 문제가 있다면 다시 롤백처리 되도록 로직을 구성할 수도 있을 것인데 managed에서는 이러한 추가 로직 구성에 있어 제한이 있다.

// Unmanaged
const { sequelize, User } = require('../models');
const { Transaction } = require('sequelize');

// 트랜잭션의 콜백으로 유저 생성 로직을 처리합니다
const t = await sequelize.transaction({
  isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED,
});

try {
  const user = await User.create(
    {
      nickname: 'apple',
      age: 18,
    },
    { transaction: t } // 해당 로직이 트랜잭션으로 묶여있음을 명시함
  );

  await t.commit();
} catch (transactionError) {
  await t.rollback();
}

Unmanaged 방식은 트랜잭션의 t를 변수로 따로 할당하면서 IsolationLevel(격리 수준)을 설정할 수 있다.
또한 commit과 rollback을 명확하게 명시하고 있다는 점이 managed와의 차이점이라 할 수 있다. 이는 managed 방식과 동일한 작동방식을 보이나 요청에 대한 성공과 에러 발생에 대한 명확한 구분을 보이고 있으며 특히 메인 task 이외 추가적인 task가 필요할 경우에도 롤백을 적용할 수 있다는 점에서 managed와의 차별점을 보인다.
개인적으로는 항상 협업의 상황을 가정한다고 했을 떄 코드 공유를 위해 명확하게 명시되는 것이 좋다고 생각하며 또한 확장성을 고려했을 때 Unmanaged 방식이 더 선호되어야 한다고 생각한다.

Unmanaged 트랜잭션 예시

이제 unmanaged 방식을 기준으로 실제 예시를 살펴보자
회원가입 task를 수행하는 과정에서 유저 로그인 시 필요한 이메일과 패스워드정보는 User 테이블에 저장하고, 그 외 유저에 대한 정보는 UserInfo 테이블에 따로 저장한다고 했을 때 아래와 같은 트랜잭션을 적용할 수 있을 것이다.

// Unmanaged
const { sequelize, User, UserInfo } = require('../models');
const { Transaction } = require('sequelize');
const express = require('express');
const router = express.Router();

router.post('/users', async (req, res) => {
  const { email, password, nickname, name, age, gender } = req.body;

  // 트랜잭션을 정의합니다.
  const t = await sequelize.transaction({
    isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED,
  });

  try {
    // 유저를 생성합니다.
    const user = await User.create({ email, password }, { transaction: t }); // 트랜잭션 등록

    // 유저 정보를 생성합니다.
    const userInfo = await UserInfo.create(
      {
        nickname,
        name,
        age,
        gender,
      },
      { transaction: t } // 트랜잭션 등록
    );

    await t.commit(); // 성공 시 커밋
  } catch (transactionError) {
    await t.rollback(); // 실패 시 롤백
    return res.status(400).send({ message: '유저 생성 실패' });
  }

  return res.status(201).send({ message: '유저 생성 성공' });
});

위 코드와 같이 두 가지 다른 모델에 데이터를 생성하는 경우 하나의 트랜잭션으로 묶어서 처리하도록 함을 알 수 있다.
기억해야할 점은 하나의 트랜잭션으로 묶일 수 있도록 반드시 각 모델 접근 task에 {transaction: t}를 적용해야 한다는 점이다.

728x90
728x90