Sequelize 튜토리얼(14)-트랜잭션

차분한열정·2021년 4월 29일
1

Sequelize 튜토리얼

목록 보기
14/14

Sequelize에서 트랜잭션에는 크게 두 가지 종류가 있다.

  1. Unmanaged transactions: 커밋과 롤백이 별도의 메소드에 의해 직접 호출되어야 하는 트랜잭션
  2. Managed transactions: 별 문제가 없으면 Sequelize가 자동으로 커밋해주고, 만약 중간에 에러가 발생하면 롤백시키는 트랜잭션

1. 트랜잭션의 종류

(1) Unmanaged transaction

// First, we start a transaction and save it into a variable
const t = await sequelize.transaction();

try {

  // Then, we do some calls passing this transaction as an option:

  const user = await User.create({
    firstName: 'Bart',
    lastName: 'Simpson'
  }, { transaction: t });

  await user.addSibling({
    firstName: 'Lisa',
    lastName: 'Simpson'
  }, { transaction: t });

  // If the execution reaches this line, no errors were thrown.
  // We commit the transaction.
  await t.commit();

} catch (error) {

  // If the execution reaches this line, an error was thrown.
  // We rollback the transaction.
  await t.rollback();

}

직접 sequelize.transaction() / t.commit() / t.rollback() 과 같은 함수를 호출하고 있는 것을 알 수 있다.

(2) Managed transaction

try {

  const result = await sequelize.transaction(async (t) => {

    const user = await User.create({
      firstName: 'Abraham',
      lastName: 'Lincoln'
    }, { transaction: t });

    await user.setShooter({
      firstName: 'John',
      lastName: 'Boothe'
    }, { transaction: t });

    return user;

  });

  // If the execution reaches this line, the transaction has been committed successfully
  // `result` is whatever was returned from the transaction callback (the `user`, in this case)

} catch (error) {

  // If the execution reaches this line, an error occurred.
  // The transaction has already been rolled back automatically by Sequelize!

}

seuqelize 객체의 transaction 메소드에 async 콜백을 넘겨주고 실행했다.
이 경우에는
1. Sequelize는 자동으로 트랜잭션을 시작하고 트랜잭션 객체 t를 얻는다.
2. 콜백을 트랜잭션 t를 넘겨서 실행한다.
3. 만약 콜백 실행 도중 에러가 발생하면 자동으로 트랜잭션이 롤백되고
4. 별 일 없이 콜백이 다 잘 실행되면 자동으로 트랜잭션이 커밋된다.

만약 SQL 작업 관련해서는 별다른 오류가 없었지만 개발자 판단 하에 트랜잭션을 롤백해야하는 상황이라면

await sequelize.transaction(async t => {
  const user = await User.create({
    firstName: 'Abraham',
    lastName: 'Lincoln'
  }, { transaction: t });

  // Woops, the query was successful but we still want to roll back!
  // We throw an error manually, so that Sequelize handles everything automatically.
  throw new Error();
});

이렇게 콜백 안에서 에러를 throw 하면 된다.

그런데 Managed Transaction 코드에서도 자세히 보면 마찬가지로 각 메소드에 { transaction: t} 라는 두 번째 인자를 넣어서 트랜잭션 객체를 넣어주고 있음을 알 수 있다.

모든 트랜잭션에서 자동으로 각각의 트랜잭션 객체를 자동으로 넘겨주려면 cls-hooked(CLS, Continuation Local Storage) 모듈을 설치하고 이런 식의 코드를 써주면 된다.

const cls = require('cls-hooked');
const namespace = cls.createNamespace('my-very-own-namespace');

const Sequelize = require('sequelize');
Sequelize.useCLS(namespace);

new Sequelize(...);

이렇게 하고나면

sequelize.transaction((t1) => {
  // With CLS enabled, the user will be created inside the transaction
  return User.create({ name: 'Alice' });
});

이런 식으로 이제 해당 트랜잭션 객체를 넘겨주지 않아도 된다.

2. Concurrent 트랜잭션

배치 작업 등을 수행할 때는 Concurrent 트랜잭션들이 필요하다. 만약 CLS enabled 상태라고 가정했을 때 다음 코드는 주석에 그 의미들이 나와있다.

sequelize.transaction((t1) => {
  return sequelize.transaction((t2) => {
    // With CLS enabled, queries here will by default use t2.
    // Pass in the `transaction` option to define/alter the transaction they belong to.
    return Promise.all([
        User.create({ name: 'Bob' }, { transaction: null }),
        User.create({ name: 'Mallory' }, { transaction: t1 }),
        User.create({ name: 'John' }) // this would default to t2
    ]);
  });
});

각 작업이 트랜잭션에 속해있지 않거나, 어느 트랜잭션에 속하는지가 옵션으로 표시되어 있거나,
아니면 CLS에 의해 자동으로 기본 트랜잭션에 속하거나 하고 있다. 이런 식으로 Concurrent 트랜잭션을 구현할 수 있다.

3. 옵션 넘기기

Unmanaged transaction의 경우에는

sequelize.transaction(options);

Managed transaction의 경우에는

sequelize.transaction(options, callback);

이렇게 필요한 옵션을 넘거주면 된다.

예를 들어 Isolation Level에는 다음과 같은 값들이 있는데

const { Transaction } = require('sequelize');

// The following are valid isolation levels:
Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED // "READ UNCOMMITTED"
Transaction.ISOLATION_LEVELS.READ_COMMITTED // "READ COMMITTED"
Transaction.ISOLATION_LEVELS.REPEATABLE_READ  // "REPEATABLE READ"
Transaction.ISOLATION_LEVELS.SERIALIZABLE // "SERIALIZABLE"

이런 식으로 특정 트랜잭션의 isolation level을 설정할 수 있다.

const { Transaction } = require('sequelize');

await sequelize.transaction({
  isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE
}, async (t) => {
  // Your code
});

만약 모든 트랜잭션에 대해서 같은 isolation level을 주고 싶다면 이렇게 해주면 된다.

const { Sequelize, Transaction } = require('sequelize');

const sequelize = new Sequelize('sqlite::memory:', {
  isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE
});

** 참고로 Servcie, Repository layer의 함수 하나당 트랜잭션 하나만 할당되도록 하는 것이 깔끔하다!

profile
성장의 기쁨

2개의 댓글

comment-user-thumbnail
2021년 7월 7일

정리 굉장히 잘되어있네요! 잘봤습니다 :)

답글 달기
comment-user-thumbnail
2022년 2월 18일

제가 보았던 sequelize 게시물 중 가장 유용한 게시물들이었습니다. 첫번째 게시글부터 끝까지 한번에 다 봤네요, 많은 도움 얻고 갑니다! 감사합니다.

답글 달기