Mongoose-Transaction
BACKEND
NESTJS
Mongoose에서 Transaction(트랜잭션) 구현 방법
MongoDB는 기본적으로 NoSQL이지만, ACID 트랜잭션을 지원하며, Mongoose에서도 이를 활용할 수 있습니다.
트랜잭션을 사용하면 여러 개의 데이터 변경 작업을 하나의 단위로 묶어 실행하고, 중간에 실패 시 전체 롤백할 수 있습니다.
기본적인 Mongoose 트랜잭션 예제
예제: 유저(User)가 그룹(Group)에 가입하는 과정에서 트랜잭션 사용
User와 Group 컬렉션이 존재한다고 가정
user.groups에 그룹 추가
group.users에도 유저 추가
중간에 에러가 발생하면 모든 변경 사항을 롤백
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: String,
email: String,
groups: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Group' }]
});
const GroupSchema = new mongoose.Schema({
name: String,
users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]
});
const User = mongoose.model('User', UserSchema);
const Group = mongoose.model('Group', GroupSchema);
async function addUserToGroup(userId, groupId) {
const session = await mongoose.startSession(); // 트랜잭션 시작
session.startTransaction();
try {
// 1. 유저 정보 가져오기
const user = await User.findById(userId).session(session);
if (!user) throw new Error('User not found');
// 2. 그룹 정보 가져오기
const group = await Group.findById(groupId).session(session);
if (!group) throw new Error('Group not found');
// 3. 유저의 groups 배열에 그룹 추가
user.groups.push(groupId);
await user.save({ session });
// 4. 그룹의 users 배열에 유저 추가
group.users.push(userId);
await group.save({ session });
// 모든 작업이 성공하면 커밋(commit)
await session.commitTransaction();
session.endSession();
console.log('User added to group successfully');
} catch (error) {
// 오류 발생 시 롤백(rollback)
await session.abortTransaction();
session.endSession();
console.error('Transaction failed, rolled back:', error);
}
}
// 실행 예제
// addUserToGroup('유저ID', '그룹ID');
트랜잭션 사용 시 주의할 점
session을 모든 쿼리에서 사용해야 함
트랜잭션 내에서 실행되는 모든 Mongoose 쿼리에는 .session(session)을 추가해야 함.
const user = await User.findById(userId).session(session);
중간에 오류가 발생하면 abortTransaction()을 호출하여 롤백
하나의 작업이라도 실패하면 전체 작업이 취소됨.
await session.abortTransaction();
트랜잭션이 끝난 후 session.endSession() 호출 필요
세션을 명시적으로 종료해야 함.
session.endSession();
withTransaction()을 활용한 트랜잭션 (더 간결한 코드)
MongoDB는 withTransaction()을 지원하여 더 깔끔한 트랜잭션 코드를 작성할 수 있음.
async function addUserToGroupWithTransaction(userId, groupId) {
const session = await mongoose.startSession();
await session.withTransaction(async () => {
const user = await User.findById(userId).session(session);
if (!user) throw new Error('User not found');
const group = await Group.findById(groupId).session(session);
if (!group) throw new Error('Group not found');
user.groups.push(groupId);
await user.save({ session });
group.users.push(userId);
await group.save({ session });
});
session.endSession();
console.log('Transaction successful');
}
트랜잭션이 필요한 경우
여러 개의 컬렉션을 동시에 수정해야 하고, 중간에 하나라도 실패하면 전체 취소가 필요할 때
예: User와 Group의 관계 설정, 은행 계좌 이체, 주문 시스템 등
트랜잭션이 필요하지 않은 경우
단순한 CRUD 작업 (예: 한 개의 User 정보 수정)
populate()로 관계를 관리하는 경우 (참조 방식의 경우 트랜잭션이 필요하지 않을 수도 있음)
MongoDB의 트랜잭션은 성능 비용이 있기 때문에, 필요한 경우에만 사용하는 것이 좋음.