트랜잭션 / QueryRunner

Gaeun·2023년 1월 16일
0

wecode TIL

목록 보기
19/24
post-custom-banner

장바구니에서 결제로 넘어갈 때, 많은 일들을 한 번에 수행하여야 한다.
예를 들면 order 테이블에 해당 유저의 정보를 입력해야하고, cart 테이블에 남아있던 해당 유저가 결제한 상품들의 정보를 지워야하고, 결제는 어떤 식으로 했으며, 해당 유저가 어떠한 상품들을 결제하였고 등등의 모든 일들이 한 번에 수행되어야만 한다.

만약 장바구니 페이지에서 결제 버튼을 눌러 사용자가 결제를 완료하였고, 결제 정보가 DB에 입력되었지만 어떠한 사정으로 인해 order 테이블에 해당 유저의 정보가 넘어가지 않았고, 장바구니 페이지에서 여전히 이를 확인할 수 있는 경우가 생긴다면 이는 사용자의 서비스의 불만족으로 이어질 뿐만 아니라, DB의 데이터를, 서비스를 믿을 수 없게 되는 상황까지 이어진다.

이를 방지하기 위해 트랜잭션이라는 개념을 알고 있어야만 한다. 분명 SQLD 공부할 때 배웠던 개념이었지만, 이를 프로젝트에 활용할 생각을 하지 못하고 있었다. 처음 PR을 올린 후, 호준님께서 멘토링을 진행해주셨고, 그때 트랜잭션에 대해 다시한번 생각할 수 있게 되었다.

1. 트랜잭션

트랜잭션(transaction)이란 데이터베이스 시스템에서 병행 제어 및 회복 작업 시 처리되는 작업의 논리적 단위이며, 하나의 트랜잭션은 commit 되거나 rollback 된다.

즉, 수행하여아 하는 모든 쿼리들을 하나의 단위로 묶고, 이 모든 쿼리들이 수행된다면 commit을, 중간에 오류가 생긴다면 수행된 쿼리들을 rollback하는 것이라고 생각하면 된다. 트랜잭션을 사용하면 쿼리 수행 시 오류가 생겼을 때 어떤 정보는 DB에 넘어가고, 어떤 정보는 DB에 넘어가지 않게 되는 불상사를 방지할 수 있게 된다.

따라서 DB 내에서 하나의 그룹으로 처리되어야 하는 명령문들을 모아 놓은 논리적인 작업 단위로서 여러 개의 명령어 집합이 정상적으로 처리되면 정상 종료되고, 하나의 명령어라도 잘못되면 전체 취소되는 것이 트랜잭션이다.

데이터의 일관성을 유지하면서 안정적으로 데이터를 복수할 수 있어 여러 쿼리들을 한 번에 수행해야 할 때에는 트랜잭션을 꼭 사용하여야 한다.

트랜잭션이 무엇인지는 알았지만 나는 여전히 궁금증이 생겼다. Node.js에서는, TypeORM에서는 어떤 식으로 트랜잭션을 사용해야 하는 것일까? 해당 정보는 이 링크에서 찾아볼 수 있었다.

2. QueryRunner

TypeORM은 QueryRunner를 사용하여 트랜잭션을 실행할 수 있다.

  1. QueryRunner를 생성하고,
  2. connect()를 통해 연결한 뒤,
  3. startTransaction()을 통해 트랜잭션을 시작하고,
  4. 수행하여야 할 쿼리들을 실행하고,
  5. 모든 쿼리들이 수행되었을 때엔 commitTransaction(), 에러가 발생되었을 때엔 rollbackTransaction()을 통해 제어하고,
  6. 이후에는 release()를 통해 QueryRunner를 해제한다.

아래의 예처럼 쓸 수 있다.

// create a new query runner
const queryRunner = dataSource.createQueryRunner()

// establish real database connection using our new query runner
await queryRunner.connect()

// now we can execute any queries on a query runner, for example:
await queryRunner.query("SELECT * FROM users")

// we can also access entity manager that works with connection created by a query runner:
const users = await queryRunner.manager.find(User)

// lets now open a new transaction:
await queryRunner.startTransaction()

try {
    // execute some operations on this transaction:
    await queryRunner.manager.save(user1)
    await queryRunner.manager.save(user2)
    await queryRunner.manager.save(photos)

    // commit transaction now:
    await queryRunner.commitTransaction()
} catch (err) {
    // since we have errors let's rollback changes we made
    await queryRunner.rollbackTransaction()
} finally {
    // you need to release query runner which is manually created:
    await queryRunner.release()
}

2-1. 실제로 내가 작성한 코드

const createOrder = async (userId, cartId, products, totalPrice) => {
  if (!products.length) throwCustomError("NO_PRODUCTS", 400);

  const queryRunner = appDataSource.createQueryRunner();

  await queryRunner.connect();
  await queryRunner.startTransaction();

  try {
    const createOrder = await queryRunner.query(
      `INSERT INTO
        orders (user_id, order_status_id)
      VALUES
        (${userId}, ${OrderStatusId.ORDER_DONE});
      `
    );

    const orderId = createOrder.insertId;

    await queryRunner.query(
      `DELETE FROM
        carts
      WHERE
        carts.id IN (?);
      `,
      [cartId]
    );

    await queryRunner.query(
      `UPDATE
          users
        SET
          points = points - ?
        WHERE
          id = ?
        `,
      [totalPrice, userId]
    );

    const query = `INSERT INTO
      order_product (order_id, product_id, quantity, order_status_id)
        VALUES ?;`;

    let values = [];
    for (let i = 0; i < products.length; i++) {
      values.push([
        orderId,
        products[i].productId,
        products[i].quantity,
        `${OrderStatusId.ORDER_DONE}`,
      ]);
    }

    await queryRunner.query(query, [values]);

    await queryRunner.query(
      `INSERT INTO
        payments (order_id, total_price, methods)
      VALUES (?, ?, ${PaymentMethodId.USER_CREDIT})
      `,
      [orderId, totalPrice]
    );

    await queryRunner.commitTransaction();
  } catch (err) {
    console.log(err);
    await queryRunner.rollbackTransaction();
  } finally {
    await queryRunner.release();
  }
};
profile
🌱 새싹 개발자의 고군분투 코딩 일기
post-custom-banner

0개의 댓글