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와 비교하면 금방 알게 되겠지만 위 코드에서 요청 성공 또는 에러에 따른 COMMIT
과 ROLLBACK
명령이 따로 명시되어 있지 않지만 그럼에도 자체적으로 동작한다. 그래서 코드를 간결하게 작성할 수 있다는 장점이 있다.
하지만 에러 상황 외 롤백이 필요한 로직을 추가해야할 경우 어떻게 짜야 하는가?
에 대한 문제점이 있다. 가령 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}를 적용해야 한다는 점이다.
'백엔드 개발자(node.js)가 되는 과정' 카테고리의 다른 글
socket.io로 간단한 채팅 페이지 구현하기 (0) | 2023.07.12 |
---|---|
[sequelize] 데이터베이스의 테이블 ID를 UUID로 사용하기 (0) | 2023.07.11 |
트랜잭션의 기본 이해 (0) | 2023.07.10 |
node.js express 엑세스 토큰과 리프레시 토큰 (0) | 2023.07.05 |
Socket.io에 대해서 알아봅시다. (0) | 2023.07.04 |