[NestJs] Custom Repository와 함께 춤을 (feat. 인터페이스)

NestJs와 TypeORM을 같이 사용할 때, 대부분 TypeORM에서 기본 제공하는 Repository 구현체에 제네릭으로 해당 레포지토리를 통해 접근하려는 엔티티 클래스를 주입해서 Service 클래스의 생성자에 추가하여 사용하였었다. 

 

TypeORM에서 제공하는 해당 방식을 사용하면 Repository를 따로 구성하지 않고, 엔티티, 서비스 클래스 만으로 간결하게 구현할 수 있다는 장점이 있었지만, 추후 TypeORM에서 다른 라이브러리로의 전환시, 리팩토링 비용이 많이 들어갈 가능성을 배제할 수 없다. ORM 마다 제공하는 API가 다르기 때문에 interface에 필요한 스펙을 명시해두고, 구현체를 따로 구현하는 방식을 도입했다.

 

1. 인터페이스 기반 Custom Repository 구현


인터페이스는 Iuser.repository.ts, 해당 인터페이스의 구현체는 user.repository.ts로 분리하여 구현하였다.

 

인터페이스 IUserRepository

import { UserEntity } from '../user.entity';

export interface IUserRepository {
  findByEmail(email: string): Promise<UserEntity | undefined>;
  findByName(name: string): Promise<UserEntity | undefined>;
  findById(id: number): Promise<UserEntity | undefined>;
}

 

 

구현체 UserRepository

import { IUserRepository } from './Iuser.repository';
import { Repository } from 'typeorm';
import { UserEntity } from '../user.entity';
import { CustomRepository } from '../../../util/decorator/custom-repository/custom-repository.decorator';

@CustomRepository(UserEntity)
export class UserRepository
  extends Repository<UserEntity>
  implements IUserRepository
{
  async findByEmail(email: string): Promise<UserEntity | undefined> {
    return this.findOne({
      where: { email: email },
    });
  }

  async findByName(name: string): Promise<UserEntity | undefined> {
    return this.findOne({
      where: { name: name },
    });
  }

  async findById(id: number): Promise<UserEntity | undefined> {
    return this.findOne({
      where: { id: id },
    });
  }
}
  • CustomRepository(UserEntity)
    • 직접 Repository를 생성하여 사용할 때, TypeORM의 EntityRepository 데코레이터를 사용하였었다.
    • EntityRepository 데코레이터는 매개변수로 받은 Entity 클래스를 TypeORM 모듈 내부 MetadataArgsStorage 클래스에 위치한 배열로 구성된 entityRepositories 에 저장해주는 역할을 한다.
    • 기본적으로 TypeORM에서 제공해주는 EntityRepository 데코레이터의 경우, TypeORM 0.3 메이저 버전 업데이트시에 deperecated 되어서 EntityRepository 역할을 대신 해줄 데코레이터를 직접 구현해야한다.
  • extends Repository<T>
    • TypeORM이 제공하는 Repository 구현체를 상속받아서 기본 메소드 사용에 활용하였다.
    • Ex) this.findOne(), this.find(), this.queryBuilder()
  • implements IUserRepository
    • 인터페이스 상에 미리 구현해둔 스펙을 implements 해서 구현체에 정의한다.

2. DI 및 EntityRepository 관련 이슈 해결


EntityRepository 데코레이터가 deperecated되면서 해당 데코레이터가 하던 역할을 유사하게 해줄 데코레이터를 직접 구현해야하는 이슈가 생겼다. 임시방편으로 Deperecated된 EntityRepository 데코레이터를 사용해도 지금 당장은 별다른 이슈는 없지만, TypeORM 측에서는 DataMapper 패턴 보다 Active Record 패턴 전략을 권장하는 분위기의 메세지를 남겨두었기 때문에 언제 어떤 버전에서 Deprecated된 데코레이터를 삭제할지 모르므로 유사한 역할을 할 수 있는 데코레이터를 구현하여 사용하는 것이 안정성 측면에서 좋을 것이라 판단하였다.

 

데코레이터 CustomRepository

import { SetMetadata } from '@nestjs/common';

export const CUSTOM_REPOSITORY = 'CUSTOM_REPOSITORY';

// eslint-disable-next-line @typescript-eslint/ban-types
export function CustomRepository(entity: Function): ClassDecorator {
  return SetMetadata(CUSTOM_REPOSITORY, entity);
}

 

CustomRepository 데코레이터는 CustomRepository(UserEntity) 와 같이 EntityRepository 데코레이터와 유사한 형태로 사용하도록 엔티티를 매개변수로 받아서 해당 엔티티의 메타 데이터에 CUSTOM_REPOSITORY를 추가하도록 구현하였다.

 

모듈 CustomRepositoryModule

import { DynamicModule, Provider } from '@nestjs/common';
import { CUSTOM_REPOSITORY } from './custom-repository.decorator';
import { getDataSourceToken, TypeOrmModule } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';

export class CustomRepositoryModule extends TypeOrmModule {
  public static forCustomRepository<T extends new (...args: any[]) => any>(
    repositories: T[],
  ): DynamicModule {
    const providers: Provider[] = [];

    for (const repository of repositories) {
      const entity = Reflect.getMetadata(CUSTOM_REPOSITORY, repository);

      if (!entity) {
        continue;
      }

      providers.push({
        inject: [getDataSourceToken()],
        provide: repository,
        useFactory: (dataSource: DataSource): typeof repository => {
          const baseRepository = dataSource.getRepository<any>(entity);
          return new repository(
            baseRepository.target,
            baseRepository.manager,
            baseRepository.queryRunner,
          );
        },
      });
    }

    return {
      exports: providers,
      module: CustomRepositoryModule,
      providers,
    };
  }
}

CustomRepository 데코레이터에서 매개변수로 받은 엔티티의 메타데이터에 추가한 CUSTOM_REPOSITORY 값으로 해당 Repository에 대한 의존성을 주입하는 목적으로 DynamicModule을 리턴하는 메소드를 가진 모듈 클래스를 생성하였다.

 

DynamicModule과 관련된 개념은 현재 글과의 주제와 크게 연관되지 않아 링크로 대체하겠다.

 

2023-11-03 @ HyunTaek Oh

Nestjs - 동적 모듈

hyuntaek5.github.io

 

해당 모듈 클래스를 사용해서 CustomRepository인 UserRepository의 의존성을 UserModule에 추가해준다.

@Module({
  imports: [CustomRepositoryModule.forCustomRepository([UserRepository])],
  exports: [UserService],
  providers: [UserService],
  controllers: [UserController],
})
export class UserModule {}

 

마무리 


TypeORM이 기존 EntityRepository 데코레이터를 Deprecated한 덕분에 커스텀 레포지토리를 사용할 때, Nestjs Metadata/Reflector 쪽과 관련된 내용을 찾아보게되었던 계기가 된 것 같다. 혹시라도 EntityRepository 데코레이터가 depercated되어 CustomRepository 데코레이터를 구현한 히스토리를 확인하고 싶다면 아래의 링크를 참고하는 것도 좋을 거 같다.

 

This is an improvement to allow @nestjs/typeorm@8.1.x to handle CustomRepository. I won't explain it specifically, but it will h

This is an improvement to allow @nestjs/typeorm@8.1.x to handle CustomRepository. I won't explain it specifically, but it will help in some way. https://github.com/nestjs/typeorm/pull/1233 - RE...

gist.github.com