트랜잭션이란, 작업의 단위를 뜻한다. 하나의 트랜잭션이 하나의 작업이 되는 것이다. 일반적으로 하나의 작업은 여러개의 질의어로 이루어져 있다.
예를 들어, 은행에서 계좌이체를 하는 상황을 생각해보자. A가 B에게 1000원을 보내는 상황을 아주 간소화하면
위와 같이 두개로 나누어 볼 수 있다. 1과 2는 각각 하나의 SQL문으로 수행될 것이다.
그런데, 만약 서버에 문제가 생겨서 1이 완료된 상황에서 2가 수행되지 않았다면?
1000원은 그냥 증발해버리는 것이다. 보낸 사람은 있지만, 받는 사람은 없는 상황이다.
따라서 이런 경우, 2번을 수행하다가 오류가 생긴다면 1번도 이전 상태로 돌려놓아야 한다. 이것이 트랜잭션의 특징인 원자성이다. 이외에도 일관성, 독립성, 지속성이 있지만 트랜잭션에 대한 설명을 하려던 것은 아니므로... 일단 넘어가 보겠다.
Sequelize에서 트랜잭션은 다음과 같이 선언한다.
const t = await sequelize.transaction();
이걸 사용하는 방법이 두 가지 있는데, 하나는 수동(unmanaged transaction)이고, 다른 하나는 자동(managed transaction)이다.
수동도 그렇게 복잡하지는 않다.
수행이 완료된 후 데이터베이스에 반영 될 수 있는 지점에서 커밋을 하면 된다.
await t.commit();
그리고 만일 에러가 났을 때 도달할 수 있는 지점에서 롤백을 해주면 된다.
await t.rollback();
예시를 보면 더 어렵지 않다.
const t = await sequelize.transaction();
try {
// a 계좌에서 1000원 출금
await Account.decrement("balance",{
transaction: t,
where: {
// id = a
},
by: 1000,
});
// b 계좌에 1000원 입금
await Account.increment("balance",{
transaction: t,
where: {
// id = b
},
by: 1000,
});
// 에러가 나지 않았으므로 커밋
await t.commit();
} catch (error) {
// 중간에 에러가 발생, 롤백
await t.rollback();
}
자동은 위의 프로세스를 자동으로 해주는 것이다. 따라서 커밋과 롤백을 적어주지 않아도 된다. 대신, 작업을 콜백 함수로 sequelize.transaction에 전달해주어야 한다.
try {
await sequelize.transaction(async t => {
// a 계좌에서 1000원 출금
await Account.decrement("balance",{
transaction: t,
where: {
// id = a
},
by: 1000,
});
// b 계좌에 1000원 입금
await Account.increment("balance",{
transaction: t,
where: {
// id = b
},
by: 1000,
});
});
} catch (error) {
console.log(error);
}
그러나 이 경우에도 try-catch문은 써주어야 한다. 만약 catch문으로 에러가 전달된다면 롤백이 진행되고, 에러가 발생하지 않는다면 커밋이 자동으로 이루어진다.
두가지 모두, try-catch문의 위치는 크게 중요하지 않다. 에러가 발생했을 때 catch문에 전달되도록 짜면 된다.
트랜잭션을 선언할 때 "s"equelize.transaction()
이 소문자임에 주의하자.
데이터베이스와 동기화(sync()
) 된 인스턴스를 불러와야 한다!!
const Sequelize = require("sequelize"); // 이게 아니라
const { sequelize } = require("../path"); // 코드에 따라 위치는 다를 수 있음
만약 catch에 에러가 잡히는데도 롤백이 제대로 이루어지 않는 다면, 트랜잭션 인스턴스를 제대로 전달하고 있는지 확인할 것!! 각 함수의 Option 객체가 어떻게 이루어졌는지 확인해야 한다.
예를 들어, create의 경우 두번째 인자 옵션에 트랜잭션을 넣어서 전달해야 하지만
await User.create({user_id: ... },{ transaction: t });
destroy문의 경우 첫번째 인자가 Option이므로 첫번째 인자에 트랜잭션을 넣어서 전달해야 한다.
await models.userCalculetLike.destroy({
where: { ... },
transaction: t,
});
이걸 못찾아서 한참 삽질함