NestJs Mongoose Transaction 적용

Seunghwa's Devlog·2023년 2월 13일
2

NestJs

목록 보기
7/15
post-thumbnail

로직을 만들다 보면 transation을 사용해야 하는 경우가 필연적으로 생기기 마련이다. 서비스 로직을 만들면서 transaction을 사용할 경우가 있었는데, NestJS와 mongoose에서 어떻게 사용하는지 포스팅 하고자 한다.

먼저 트랜잭션이 무엇인지에 대해 설명해보자면

트랜잭션(Transaction)은 데이터베이스의 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산들을 의미한다.

특징으로는

  1. 트랜잭션은 데이터베이스 시스템에서 병행 제어 및 회복 작업 시 처리되는 작업의 논리적 단위이다.
  2. 사용자가 시스템에 대한 서비스 요구 시 시스템이 응답하기 위한 상태 변환 과정의 작업단위이다.
  3. 하나의 트랜잭션은 Commit되거나 Rollback된다.

또한 트랜잭션은 ACID의 성질을 가진다.

Atomicity(원자성)

  • 트랜잭션의 연산은 데이터베이스에 모두 반영되든지 아니면 전혀 반영되지 않아야 한다.
  • 트랜잭션 내의 모든 명령은 반드시 완벽히 수행되어야 하며, 모두가 완벽히 수행되지 않고 어느하나라도 오류가 발생하면 트랜잭션 전부가 취소되어야 한다.

Consistency(일관성)

  • 트랜잭션이 그 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 변환한다.
  • 시스템이 가지고 있는 고정요소는 트랜잭션 수행 전과 트랜잭션 수행 완료 후의 상태가 같아야 한다.

Isolation(독립성,격리성)

  • 둘 이상의 트랜잭션이 동시에 병행 실행되는 경우 어느 하나의 트랜잭션 실행중에 다른 트랜잭션의 연산이 끼어들 수 없다.
  • 수행중인 트랜잭션은 완전히 완료될 때까지 다른 트랜잭션에서 수행 결과를 참조할 수 없다.

Durablility(영속성,지속성)

  • 성공적으로 완료된 트랜잭션의 결과는 시스템이 고장나더라도 영구적으로 반영되어야 한다.

연산에는 Commit / Rollback 2가지의 연산이 존재한다.

Commit
하나의 트랜잭션이 성공적으로 끝났고, DB가 일관성있는 상태일 때 이를 알려주기 위해 사용하는 연산이다.

Rollback
하나의 트랜잭션 처리가 비정상적으로 종료되어 트랜잭션 원자성이 깨진 경우

transaction이 정상적으로 종료되지 않았을 때, Transaction의 시작 상태로 roll back 할 수 있음.

NestJS + mongoose에서의 Transaction 적용

@Injectable()
export class OrderRepository {
    constructor(
      @InjectModel(Order.name)
      private readonly orderModel: Model<Order>,
      private readonly orderProductRepository: OrderproductRepository,
      private readonly optionRepository: OptionRepository,
      @InjectConnection() private readonly connection: mongoose.Connection,
    ) {}

 	...
  }
  

@InjectConnection 데코레이터를 사용하여 mongoose connection을 DI 해준다.

 async create(
    createOrderProductDTO: CreateOrderProductDTO[],
    createOrderDTO: CreateOrderDTO,
  ): Promise<Order> {
    const order = await this.orderModel.create(createOrderDTO);

    if (!order) {
      throw new NotFoundException();
    }

    // Transaction session 생성
    const session = await this.connection.startSession();
    let finalOrderResult; // return value 지정 위한 변수
    await session.withTransaction(async () => {
      const orderProduct = await this.orderProductRepository.create(
        createOrderProductDTO,
      );

      if (!orderProduct) {
        throw new NotFoundException();
      }

      // orderProduct에 orderId 저장
      orderProduct.map(
        async (orderProduct) =>
          await this.orderProductRepository.update(
            orderProduct._id.toString(),
            {
              orderId: order._id.toString(),
            },
          ),
      );

      // orderProduct quantity만큼 option의 stockAmount 감소
      orderProduct.map(async (orderProduct) =>
        this.optionRepository.update(
          orderProduct.selectedOption._id.toString(),
          {
            stockAmount:
              (
                await this.optionRepository.findOption(
                  orderProduct.selectedOption._id.toString(),
                )
              ).stockAmount - orderProduct.quantity,
          },
          session,
        ),
      );

      // order에 orderProductId 새로운 필드로 추가
      const updateOrderResult = await this.orderModel.findByIdAndUpdate(
        order._id.toString(),
        {
          orderProduct: orderProduct.map((orderProduct) => orderProduct._id),
        },
        { new: true },
      );
      finalOrderResult = updateOrderResult;
    });

    session.endSession();
    return finalOrderResult;
  }

withTransaction 메소드를 사용하여 트랜잭션의 모든 단계를 수동으로 제어하는 것을 좀 더 편리하게 하나의 메소드로 사용할 수 있다.

수동으로 제어하기



  async create(
    createOrderProductDTO: CreateOrderProductDTO[],
    createOrderDTO: CreateOrderDTO,
  ): Promise<Order> {
    const order = await this.orderModel.create(createOrderDTO);

    if (!order) {
      throw new NotFoundException();
    }

    // Transaction session 생성
    const session = await this.connection.startSession();
    session.startTransaction();
	// Transaction 성공시
    try {
      let finalOrderResult; // return value 지정 위한 변수

      const orderProduct = await this.orderProductRepository.create(
        createOrderProductDTO,
      );

      if (!orderProduct) {
        throw new NotFoundException();
      }

      // orderProduct에 orderId 저장
      orderProduct.map(
        async (orderProduct) =>
          await this.orderProductRepository.update(
            orderProduct._id.toString(),
            {
              orderId: order._id.toString(),
            },
          ),
      );

      // orderProduct quantity만큼 option의 stockAmount 감소
      orderProduct.map(async (orderProduct) =>
        this.optionRepository.update(
          orderProduct.selectedOption._id.toString(),
          {
            stockAmount:
              (
                await this.optionRepository.findOption(
                  orderProduct.selectedOption._id.toString(),
                )
              ).stockAmount - orderProduct.quantity,
          },
          session,
        ),
      );

      // order에 orderProductId 새로운 필드로 추가
      const updateOrderResult = await this.orderModel.findByIdAndUpdate(
        order._id.toString(),
        {
          orderProduct: orderProduct.map((orderProduct) => orderProduct._id),
        },
        { new: true },
      );
      // eslint-disable-next-line prefer-const
      finalOrderResult = updateOrderResult;
      await session.commitTransaction();
      return finalOrderResult;
     // Transaction 실패시
    } catch (err) {
      await session.abortTransaction(); // 실패시 rollback
      throw err;
    } finally {
      session.endSession();
    }
  }

제가 작성한 글에서 틀린 부분이 있다면 말씀해주시면 감사하겠습니다.

profile
에러와 부딪히고 새로운 것을 배우며 성장해가는 과정을 기록합니다!

2개의 댓글

comment-user-thumbnail
2023년 6월 23일

감사합니다 :)

답글 달기
comment-user-thumbnail
2023년 10월 23일

mongoose에서 트랜잭션을 사용할때 MongoDB는 Replica Set 으로 구성하지 않아도 괜찮은가요?
아니면 이미 적용을 하신 상태이신가요?

답글 달기