Skip to content

반복되는 transaction 코드 줄이기 ​

공식문서의 방법 ​

공식문서에서는 transaction 관리에 대해서 아래와 같이 하기를 추천한니다.
물론 read에 대해서는 간단하게 repository api를 쓰기만해도 됩니다.
그런데 아래와 같이 하면 무진장 코드가 반복됩니다.
작은 단위로 transaction을 관리 할 수 있게 되면 안정성은 높아질 것 같습니다.

typescript
async transaction<T>(target: EntityTarget<T>): Promise<T> {
  const queryRunner = this.dataSource.createQueryRunner();

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

  try {
    queryRunner.manager.getRepository(target);
    await queryRunner.commitTransaction();
    return result;
  } catch (error) {
    await queryRunner.rollbackTransaction();
    throw error;
  } finally {
    await queryRunner.release();
  }
}

찾은 대안 ​

나는 코드의 반복을 줄이고 transaction을 한번만 하기위해서 아래와 같은 서비스를 만들었습니다.
work callback 함수에 queryRunner를 인수로 주고 그 안에서 repository 데이터 처리를 하였습니다.

typescript
@Injectable()
export class QueryRunnerService {
  constructor(private readonly dataSource: DataSource) {}

  async runInTransaction<T>(
    work: (queryRunner: QueryRunner) => Promise<T>
  ): Promise<T> {
    const queryRunner = this.dataSource.createQueryRunner("master");

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

    try {
      const result = await work(queryRunner);
      await queryRunner.commitTransaction();
      return result;
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      await queryRunner.release();
    }
  }
}

실제 사용은 아래와 같이 진행 됩니다.

typescript
async someReposity() {
  return await this.queryRunnerService.runInTransaction(
    async (queryRunner) => {

  })
}

새로 찾은 대안 ​

reference ​

https://velog.io/@hee_kyoung11/AsyncLocalStorage를-이용해-Transaction-관심사-분리하기-lines-40-감소

분석 ​

블로그에 나온 코드는 사실 좀 난해하였다.
왜나면 공식문서에도 잘 나오지 않는 @nestjs/core 사용하는 부분이 많았다.
과정은 이렇다.

  • transaction에 관한 custom decorator를 만들어 meta data를 입력
  • @nestjs/core를 이용해서 각 instance의 prototype에 접근
  • instance의 각 method를 순회하여 custom decorator로 입력한 meta data에 맞게 메서드를 만들어 준다.
  • 메서드는 AsyncLocalstorage를 이용하여 비동기가 일어나는 동안 queryrunner를 저장하고 이용할 수 있게 해준다.
  • 사용하는 서비스나 레파지토리에서 custom decorator를 쓰고 AsyncLocalstorage에서 queryrunner를 받아서 사용한다.

transaction.decorator.ts ​

typescript
import { SetMetadata, applyDecorators } from "@nestjs/common";
import { EntityTarget, ObjectLiteral, Repository } from "typeorm";
import { AsyncLocalStorage } from "async_hooks";

export const TRANSACTION_KEY = Symbol("TRANSACTION_KEY");
export type DAEATORM = "default" | "ilsan";
export function TypeormTranscation(orm: DAEATORM = "default") {
  return applyDecorators(SetMetadata(TRANSACTION_KEY, orm));
}

export function getLocalStorageQueryRunner<T extends ObjectLiteral>(
  target: EntityTarget<T>
): Repository<T> {
  const localStorage = new AsyncLocalStorage();
  const queryRunner = localStorage.getStore();
  return queryRunner["qr"]?.manager;
}

transaction.service.ts ​

typescript
import { Injectable, OnModuleInit } from "@nestjs/common";
import { DiscoveryService, MetadataScanner, Reflector } from "@nestjs/core";
import { InjectDataSource } from "@nestjs/typeorm";
import { DataSource } from "typeorm";
import * as _ from "lodash";
import { DAEATORM, TRANSACTION_KEY } from "../decorators/transaction.decorator";
import { AsyncLocalStorage } from "async_hooks";

@Injectable()
export class TransactionService implements OnModuleInit {
  constructor(
    private readonly discoveryService: DiscoveryService,
    private readonly reflector: Reflector,
    private readonly metadataScanner: MetadataScanner,
    @InjectDataSource("default") private readonly defaultDataSource: DataSource,
    @InjectDataSource("ilsan") private readonly ilsanDataSource: DataSource,
    private readonly als: AsyncLocalStorage<any>
  ) {}

  onModuleInit() {
    const providers = this.discoveryService.getProviders();

    const instances = _(providers)
      .filter(
        (a) => a.isDependencyTreeStatic() && !(!a.metatype || !a.instance)
      )
      .map(({ instance }) => instance)
      .value();

    instances.forEach((instance) => {
      const names = this.metadataScanner.getAllMethodNames(
        Object.getPrototypeOf(instance)
      );

      for (const name of names) {
        const originalMethod = instance[name];
        const metadata = this.reflector.get<DAEATORM>(
          TRANSACTION_KEY,
          originalMethod
        );
        switch (metadata) {
          case "default":
            instance[name] = this.typeormTransaction(originalMethod, instance);
            return;
          case "ilsan":
            instance[name] = this.typeormIlsanTransaction(
              originalMethod,
              instance
            );
        }
      }
    });
  }

  private typeormTransaction(originalMethod: any, instance: any) {
    return async function (...args: any[]) {
      const queryRunner = this.defaultDataSource.createQueryRunner();

      await this.als.run({ queryRunner }, async () => {
        await queryRunner.connect();
        await queryRunner.startTransaction();

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

  private typeormIlsanTransaction(originalMethod: any, instance: any) {
    return async function (...args: any[]) {
      const queryRunner = this.ilsanDataSource.createQueryRunner();

      await this.als.run({ queryRunner }, async () => {
        await queryRunner.connect();
        await queryRunner.startTransaction();

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

transaciton.module.ts ​

typescript
import { TransactionService } from "./services/transaction.servcie";
import { AsyncLocalStorage } from "async_hooks";

@Module({
  providers: [
    TransactionService,
    {
      provide: AsyncLocalStorage,
      useValue: new AsyncLocalStorage(),
    },
  ],
  exports: [AsyncLocalStorage],
})
export class TransactionModule {}

적용서비스 ​

typescript
@TypeormTranscation()
async someRepository(user: UserDto) {
  const queryRunner = getLocalStorageQueryRunner();
  const newUser = queryRunner.create<UserEntity>(user);
  const result = await queryRunner.save(newUser);
  return result
}

결과 ​

코드의 양이 상당히 줄어드는 걸 볼 수 있습니다. 하지만 해당코드는 진입장벽이 있는 편입니다.
일단 하나하나 찾아보면서 @nestjs/core에 대한 공부가 필요하다는 것을 알았습니다.

기존 service에 AsyncLocalStorage 적용 ​

그래도 새롭게 알게 된 것을 적용하고 매번 인수를 전달해야하는 불편함을 없애고자 asynclocalstorage를 적용해보았습니다.

코드변경 ​

typescript
async runLocalStorage<T>(work: () => Promise<T>): Promise<T> {
  const queryRunner = this.dataSource.createQueryRunner();

  return await this.als.run({ queryRunner }, async () => {
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      const result = await work();
      await queryRunner.commitTransaction();
      return result;
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      await queryRunner.release();
    }
  });
}

async getQueryRunner(): Promise<QueryRunner> {
  return this.als.getStore()['queryRunner'].manager;
}

코드사용 ​

typescript
async someRepository(user:UserDto) {
  return result = await this.queryRunnerService.runLocalStrage(async () => {
    const queryRunner = this.queryRunnerService.getLocalStorageQueryRunner();
    const newUser = queryRunner.create<UserEntity>(user);
    const result = await queryRunner.save(newUser);
    return result
  })
}

데코레이터를 이용하는게 더 깔끔합니다