'sender'라는 사용자가 'receiver'라는 사용자에게 500원을 입금하는 api 요청이 있다. 해당 기능을 수행하기 위한 로직은 다음과 같다.
sender와 receiver가 유효한 사용자인지 확인해야 한다. (지금 상황에서는 sender와 receiver를 조회하는 부분은 transaction을 걸지 않아도 되지만 중복 조회를 막아야 하는 상황이라고 가정하여 transaction을 걸어준다.)
유효한 사용자인 경우는 그대로 진행하지만 그렇지 않은 경우 에러를 발생시킨다.
그 다음에는 sender의 money에서 500원을 빼고 저장하고, receiver의 money에 500원을 더해준 후 저장한다.
마지막으로는 해당 기능을 했다는 증거를 남기기 위해 Transfer라는 테이블에 값을 저장해준다.
코드는 다음과 같다.
router.post("/transferMoney", async (req, res) => {
const {senderId, receiverId} = req
const transferMoney = 500
const transaction = await sequelize.transaction()
try{
const sender= await User.findOne({
attributes:['id', 'money'],
where:{
id: senderId,
},
},
{transaction}
)
if(!sender){
throw new Error('sender not found')
}
sender.money -= transferMoney;
await sender.save({transaction})
const receiver = await User.findOne({
attributes:['id', 'money'],
where:{
id: receiverId,
}
},
{transaction}
)
if(!receiver){
throw new Error('receiver not found')
}
receiver.money += transferMoney;
await receiver.save({transaction})
await Transfer.create(
{
sender: sender.id,
receiver: receiver.id,
transferMoney,
},
{transaction}
)
await transaction.commit()
} catch (error) {
await transaction.rollback()
console.log(error)
}
}
unmanaged transaction은 위에 보이는 코드와 같이 개발자가 commit 과 rollback의 위치를 지정해놓는 방법이다. 코드가 문제 없이 실행이 되었다면 try의 마지막 부분에서 transaction을 커밋한다. 만약 코드 중간에 에러가 발생하게 되면 catch 부분에서 transaction을 롤백한 후 에러 로그를 발생시킨다.
다음은 managed transactions 방법 예시이다.
router.post("/transferMoney", async (req, res) => {
const {senderId, receiverId} = req
const transferMoney = 500
try{
await sequelize.transaction(async (transaction) => {
const sender= await User.findOne({
attributes:['id', 'money'],
where:{
id: senderId,
},
},
{transaction}
)
if(!sender){
throw new Error('sender not found')
}
sender.money -= transferMoney;
await sender.save({transaction})
const receiver = await User.findOne({
attributes:['id', 'money'],
where:{
id: receiverId,
}
},
{transaction}
)
if(!receiver){
throw new Error('receiver not found')
}
receiver.money += transferMoney;
await receiver.save({transaction})
await Transfer.create(
{
sender: sender.id,
receiver: receiver.id,
transferMoney,
},
{transaction}
)
})
} catch (error) {
console.log(error)
}
}
Managed trasaction은 Unamanaged Transaction과 유사하지만 개발자가 commit과 rollback을 따로 명시하지 않아도 된다는 차이점이 있다. 기능이 문제 없이 진행이 된다면 api의 마지막 부분에서 commit을 자동으로 실행하며, error가 발생하면 자동으로 rollback을 해준다.
내 생각에 transaction을 사용할 때 유의해야 할 점은 try catch의 범위를 잘 살펴보는 것이다. 만약 managed transaction을 사용하면서 try 범위 밖에서 throw Error를 던진다면, catch에서 에러를 잡지 못해 롤백이 실행되지 않을 수 있다. 그러므로 항상 상황에 맞춰서 try catch의 범위를 잘 선택해야 하고, 거기에 맞는 transaction 방법을 사용해야 한다. 개인적으론 코드 로직이 복잡한 경우에는 어디서 커밋과 롤백이 일어나는 지 쉽게 알기 위해 unmanaged transaction을 사용하여 가독성을 높인다.
^^