TDL

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의 트랜잭션은 성능 비용이 있기 때문에, 필요한 경우에만 사용하는 것이 좋음.