Custom Transactional Decorator Challenger

๋ฌธ์ œ

  • NestJS์—์„œ TypeORM์„ ์‚ฌ์šฉํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜์„ ๊ตฌํ˜„ํ•  ๋•Œ, ํ•˜๋‚˜์˜ ๋ฉ”์„œ๋“œ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ์ฑ…์ž„์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

  • ์ค‘๋ณต๋œ ์ฝ”๋“œ๊ฐ€ ์กด์žฌํ•œ๋‹ค.

NestJS ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋Š” Transactions๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋‘๊ฐ€์ง€ ๋ฐฉ๋ฒ•

  • QueryRunner ์‚ฌ์šฉ

async createMany(users: User[]) {
  const queryRunner = this.dataSource.createQueryRunner();

  await queryRunner.connect();
  await queryRunner.startTransaction();
  try {
    await queryRunner.manager.save(users[0]);
    await queryRunner.manager.save(users[1]);

    await queryRunner.commitTransaction();
  } catch (err) {
    // since we have errors lets rollback the changes we made
    await queryRunner.rollbackTransaction();
  } finally {
    // you need to release a queryRunner which was manually instantiated
    await queryRunner.release();
  }
}
  • dataSource.transaction

async createMany(users: User[]) {
  await this.dataSource.transaction(async manager => {
    await manager.save(users[0]);
    await manager.save(users[1]);
  });
}

์ฝ”๋“œ

async updateInfo(
    data: EditUserInfoData,
  ): Promise<UserInfoResponse> {
    // QueryRunner Connect
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();  
    
    // Transaction Start
    await queryRunner.startTransaction();
    
    try {
    
    // ์‹ค์ œ ๋กœ์ง
      const userAccount = await queryRunner.manager.findOneBy(UserAccount, {
        id: id,
        deleted: false,
      });
      if (!userAccount) {
        throw new UserAccountNotFoundError();
      }
    
      userAccount.nickname = data.nickname;
      userAccount.address = data.address;
      userAccount.introduction = data.introduction
        ? data.introduction
        : userAccount.introduction;
      userAccount.updatedAt = new Date();
      await queryRunner.manager.save(UserAccount, userAccount);
      return {
        nickname: userAccount.nickname.value,
        address: userAccount.address.value,
        introduction: userAccount.introduction?.value,
      };
      
      
    } catch (err) {
    // Rollback
      await queryRunner.rollbackTransaction();
    } finally {
    // Release
      await queryRunner.release()
    }
  }

์œ„ ์ฝ”๋“œ์—์„œ try {} ๋ฌธ ์™ธ์˜ ์ฝ”๋“œ ์ค‘๋ณต๋œ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  • TypeScript์˜ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ณตํ†ต ๋ถ€๋ถ„์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•  ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

ํ•ด๊ฒฐ ๊ณผ์ •

Step1

@Transactional() Decorator๋ฅผ ๊ตฌํ˜„ํ•ด์„œ ๊ณตํ†ต ๋กœ์ง์„ Decorator์—์„œ ๊ตฌํ˜„ํ•˜๋„๋ก ํ–ˆ๋‹ค.

  • ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง

@Transactional()
async updateInfo(data: EditUserInfoData) {
  const userAccount = await this.findById(data.userId);
  userAccount.nickname = data.nickname;
  userAccount.address = data.address;
  const now = new Date();
  userAccount.updatedAt = now;
  return this.userAccountRepository.save(userAccount, { transaction: false });
}
  • ๊ณตํ†ต ๋กœ์ง(Transactional Decorator)

export function Transactional(
  isolationLevel?: IsolationLevel,
): MethodDecorator {
  return (
    _target: any,
    _propertyKey: string,
    descriptor: PropertyDescriptor,
  ) => {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const dataSource: DataSource = this.dataSource;
      if (!dataSource) {
        throw new Error('DataSource is not injected');
      }

      // ํŠธ๋žœ์žญ์…˜์„ ์ด๋ฏธ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ์ถ”๊ฐ€๋กœ ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•˜์ง€ ์•Š๋Š”๋‹ค.
      const existsTransaction = args.find(
        (arg) => arg.connection !== undefined,
      );
      if (existsTransaction) {
        return originalMethod.apply(this, args);
      }

      // ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•œ๋‹ค.
      const queryRunner: QueryRunner = dataSource.createQueryRunner();
      await queryRunner.connect();
      await queryRunner.startTransaction(
        isolationLevel ? isolationLevel : DEFAULT_ISOLATION_LEVEL,
      );

      try {
        args = args.concat(queryRunner);
        const result = await originalMethod.apply(this, args);
        await queryRunner.commitTransaction();
        return result;
      } catch (err) {
        await queryRunner.rollbackTransaction();
        throw err;
      } finally {
        await queryRunner.release();
      }
    };
  };
}

Step1 ๋ฌธ์ œ์ 

๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ Connect์™€ ๊ณตํ†ต ๋กœ์ง์˜ Connect๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์—, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๊ฐ์‹ธ๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ์˜€์œผ๋‚˜ ์ด๋ฅผ ๋‹ฌ์„ฑํ•˜์ง€ ๋ชปํ–ˆ๋‹ค.

  • ๊ณตํ†ต ๋กœ์ง์€ Connect 47

  • ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์€ Conenct 48

Step2

๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ QueryRunner๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•˜๊ณ , ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ํ†ตํ•ด ์ด๋ฏธ ํŠธ๋žœ์žญ์…˜์ด ์‚ฌ์šฉ ์ค‘์ธ์ง€ ํ™•์ธํ•˜๊ณ , ํ•„์š”์— ๋”ฐ๋ผ ์ถ”๊ฐ€์ ์œผ๋กœ ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜ QueryRunner๋ฅผ ์ฃผ์ž…ํ•œ๋‹ค.

  • ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง

@Transactional()
async updateInfo(data: EditUserInfoData, queryRunner?: QueryRunner) {
  const userAccount = await this.findByIdWithQueryRunner(
    data.userId,
    queryRunner,
  );
  userAccount.nickname = data.nickname;
  userAccount.address = data.address;
  userAccount.introduction = data.introduction
    ? data.introduction
    : userAccount.introduction;
  const now = new Date();
  userAccount.updatedAt = now;
  await this.saveWithQueryRunner(userAccount, queryRunner);
  return {
    nickname: userAccount.nickname.value,
    address: userAccount.address.value,
    introduction: userAccount.introduction?.value,
  };
}


async findByIdWithQueryRunner(
  id: ObjectId,
  queryRunner: QueryRunner,
): Promise<UserAccount> {
  const userAccount = await queryRunner.manager.findOneBy(UserAccount, {
    id: id,
  });
  if (!userAccount) {
    throw new UserAccountNotFoundError();
  }
  return userAccount;
}

async saveWithQueryRunner(
  userAccount: UserAccount,
  queryRunner: QueryRunner,
) {
  return queryRunner.manager.save(UserAccount, userAccount);
}
  • ๊ณตํ†ต ๋กœ์ง(Transactional Decorator)

export function Transactional(
  isolationLevel?: IsolationLevel,
): MethodDecorator {
  return (
    _target: any,
    _propertyKey: string,
    descriptor: PropertyDescriptor,
  ) => {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const dataSource: DataSource = this.dataSource;
      if (!dataSource) {
        throw new Error('DataSource is not injected');
      }

      // ํŠธ๋žœ์žญ์…˜์„ ์ด๋ฏธ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ์ถ”๊ฐ€๋กœ ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•˜์ง€ ์•Š๋Š”๋‹ค.
      const existsTransaction = args.find(
        (arg) => arg.connection !== undefined,
      );
      if (existsTransaction) {
        return originalMethod.apply(this, args);
      }

      // ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ํŠธ๋žœ์žญ์…˜์„ ์ƒ์„ฑํ•œ๋‹ค.
      const queryRunner: QueryRunner = dataSource.createQueryRunner();
      await queryRunner.connect();
      await queryRunner.startTransaction(
        isolationLevel ? isolationLevel : DEFAULT_ISOLATION_LEVEL,
      );

      try {
        args = args.concat(queryRunner);
        const result = await originalMethod.apply(this, args);
        await queryRunner.commitTransaction();
        return result;
      } catch (err) {
        await queryRunner.rollbackTransaction();
        throw err;
      } finally {
        await queryRunner.release();
      }
    };
  };
}
  • ๊ฒฐ๊ณผ

ํ•˜๋‚˜์˜ connect ์—์„œ ํ•˜๋‚˜์˜ transactional ์•ˆ์—์„œ ๋ชจ๋“  ๋กœ์ง์ด ์‹คํ–‰ํ•œ๋‹ค.

๊ฒฐ๋ก 

NestJS์™€ TypeORM์„ ์‚ฌ์šฉํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ์ฝ”๋“œ ์ค‘๋ณต์„ ์ค„์ด๊ณ , ๋ฉ”์„œ๋“œ์˜ ์ฑ…์ž„์„ ๋ถ„์‚ฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ํƒ๊ตฌํ–ˆ๋‹ค.

  • Transactional Decorator์˜ ๋„์ž…

  • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ์˜ ๋ถ„๋ฆฌ

  • ํ•˜๋‚˜์˜ Connect ๋‚ด์—์„œ์˜ ํŠธ๋žœ์žญ์…˜ ์‹คํ–‰

๊ฒฐ๋ก ์ ์œผ๋กœTransactional ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์˜ ์‚ฌ์šฉ๊ณผ QueryRunner์˜ ์ ์ ˆํ•œ ๊ด€๋ฆฌ๋ฅผ ํ†ตํ•ด, NestJS ๋ฐ TypeORM ํ™˜๊ฒฝ์—์„œ ํŠธ๋žœ์žญ์…˜์„ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋ฅผ ๊ตฌ์ถ•ํ–ˆ๋‹ค. ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ์— ๋Œ€ํ•œ ์ค‘๋ณต ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ๋ช…ํ™•์„ฑ ๋ฐ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œ์ผฐ๋‹ค.

๋ฌผ๋ก ,์ด ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์–ด๋–ค ์ถ”๊ฐ€์ ์ธ ์˜ํ–ฅ์„ ๋ฏธ์น ์ง€๋Š” ์•„์ง ๋ถˆํ™•์‹คํ•˜๋‹ค. ํ…Œ์ŠคํŠธ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์ง€์†์ ์œผ๋กœ ํ™•์ธํ•ด์„œ ๊ฐœ์„  ์ž‘์—…ํ•  ์˜ˆ์ •์ด๋‹ค.

Last updated

Was this helpful?