Handling Transactions (feat. NestJS, TypeORM)

백엔드·2023년 8월 4일
0

NestJS

목록 보기
5/7

들어가며

해당 글은 zum-custom-decorator, nestjs-transaction-decorator, nestjs-meetup의 자료를 보고 공부하며 영감을 받아 작성합니다 🙏

개요

NestJS에서 트랜잭션(custom decorator)가 필요한 이유를 AOP(Aspect-Oriented Programming) 관점에서 살펴보겠습니다.



위 사진처럼 프로세스마다 공통되는 기능을 횡단 관심사라고 부르며, 이러한 관심사들의 분리는 AOP, 즉 관점 지향 프로그래밍에서 모듈성을 증가시키기 위한 패러다임입니다.

핵심 비즈니스 로직 (비즈니스 로직이 담긴 서비스 메서드)은 트랜잭션과는 관련이 없습니다. 이는 데이터를 저장하거나 업데이트하는 등의 주요 작업을 수행합니다.

즉 AOP 관점에서, 트랜잭션과 관련된 로직을 커스텀 데코레이터로 구현하여 핵심 비즈니스 로직과 분리하고, 이를 필요로 하는 서비스 메서드에서 재사용 가능하도록 합니다.

이러한 접근 방식은 코드의 모듈성과 재사용성을 향상시키고, 데이터베이스 트랜잭션과 같은 공통적인 기능을 중앙에서 관리하여 유지보수를 용이하게 합니다.

접근 방법

  1. cls-hooked을 사용해 Transaction Decorator 구현
  2. NestJS LifeCycle 및 DiscoveryModule을 통한 Transaction Decorator 구현

cls-hooked을 사용해 Transaction Decorator 구현

참고자료 nestjs-transaction-decorator, nestjs-meetup

구현 방식

cls-hooked는 각 요청마다 Namespace라는 곳에 context를 생성하여 해당 요청만 접근할 수 있는 공간을 만들어줍니다.

이후 요청이 끝나면 해당 context를 닫아줍니다. 이를 이용해 요청이 들어오면 해당 요청에서만 사용할 entityManager를 생성하여 Namespace에 넣어 transaction decorator를 구현하였습니다.

  1. Namespace 생성 후 EntityManager를 심어주는 TransactionMiddleware

  2. Namespace에 있는 EntityManager에 접근할 수 있는 헬퍼 TransactionManager

  3. origin method를 transaction으로 wrapping 하는 Transaction Decorator


flow

transaction derocator flow with cls
  1. 요청이 들어오면, Modules에서 등록한 TransactionMiddleware을 통해 cls-hooked를 사용해 해당 요청에 대한 namespace에 EntityManager 등록

  2. Service에서 Transactional decorator를 사용하는 method에 class 인스턴스 생성 이전 시점에 접근해 Origin method를 Transaction Method로 wrapping

  3. TransactionManage를 통해 Repository에서 Transaction이 시작된 EntityManager를 꺼내와 transaction 및 original Function 실행


Transaction Middleware example

  1. 요청이 들어오면, 해당 요청에 대한 nameSpace가 존재하는 지 확인 후,
    존재하지 않는다면, nameSpace를 생성합니다.

  2. 해당 요청에 대한 nameSpace에 주입받은 EntityManager를 등록합니다.

@Injectable()
export class TransactionMiddleware implements NestMiddleware {
  constructor(
    private readonly em: EntityManager,
    @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: Logger,
  ) {}

  use(_req: Request, _res: Response, next: NextFunction) {
    const namespace =
      getNamespace(NAMESPACE_KEY) ?? createNamespace(NAMESPACE_KEY);

    return namespace.runAndReturn(async () => {
      Promise.resolve()
        .then(() => this.setEntityManager())
        .then(next);
    });
  }

  private setEntityManager() {
    const namespace = getNamespace(NAMESPACE_KEY)!;
    namespace.set(ENTITY_MANAGER_KEY, this.em);
  }
}

Transactional Decorator example

  1. Transactional decorator를 사용하는 method에 class 인스턴스 생성 이전 시점에 접근합니다.

  2. transactionWrapped function에서 해당 요청에 대한 nameSpace에 접근한 뒤, middleware에서 등록한 EntityManager에 접근합니다.

  3. EntityManager의 transaction 메소드를 실행시킨 후 TransactionManager에서 꺼내 쓸 수 있도록 callback인자로 받은 Transaction이 시작된 EntityManager를 nameSpace에 넣어줍니다.

  4. origin method를 Transaction Method로 wrapping 합니다.

export function Transactional() {
  return function (
    _target: Object,
    _propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<any>,
  ) {
    // save original method
    const originMethod = descriptor.value;

    // wrapped origin method with Transaction
    async function transactionWrapped(...args: unknown[]) {
      // validate nameSpace && get nameSpace
      const nameSpace = getNamespace(NAMESPACE_KEY);
      if (!nameSpace || !nameSpace.active)
        throw new InternalServerErrorException(
          `${NAMESPACE_KEY} is not active`,
        );

      // get EntityManager
      const em = nameSpace.get(ENTITY_MANAGER_KEY) as EntityManager;
      if (!em)
        throw new InternalServerErrorException(
          `Could not find EntityManager in ${NAMESPACE_KEY} nameSpace`,
        );

      return await em.transaction(async (tx: EntityManager) => {
        nameSpace.set(ENTITY_MANAGER_KEY, tx);
        return await originMethod.apply(this, args);
      });
    }

    descriptor.value = transactionWrapped;
  };
}

TransactionManager example

baseRepository에서는 TransactionManager를 주입받아, Transaction이 시작된 EntityManager에 접근할 수 있습니다.

@Injectable()
export class TransactionManager {
  getEntityManager(): EntityManager {
    const nameSpace = getNamespace(NAMESPACE_KEY);
    if (!nameSpace || !nameSpace.active)
      throw new InternalServerErrorException(
        `${NAMESPACE_KEY} is not active`,
      );
    return nameSpace.get(ENTITY_MANAGER_KEY);
  }
}

BaseRepository, userRepository example

  • protected getRepository method를 BaseRepository에 구현하여 BaseRepository를 상속받은 자식 repository는 각각 자신의 Repository에 접근할 수 있습니다.
  • TransactionManager를 주입받아 transaction decorator에서 해당 요청에 대한 namespace에 주입하였던 EntityManager에 접근할 수 있습니다.
@Injectable()
export abstract class BaseRepository<T extends BaseEntity> {
  protected abstract readonly txManager: TransactionManager;

  constructor(private readonly classType: ClassConstructor<T>) {}

  abstract getName(): EntityTarget<T>;

  protected getRepository(): Repository<T> {
    return this.txManager.getEntityManager().getRepository(this.getName());
  }
  
}
@Injectable()
export class UserRepository extends BaseRepository<User> {
  getName(): EntityTarget<User> {
    return User.name;
  }

  constructor(protected readonly txManager: TransactionManager) {
    super(User);
  }
}

다음글에서 이어집니다.

profile
백엔드 개발자

0개의 댓글