NestJS-TypeORM

김명일·2022년 7월 7일
9

NestJS와 TypeORM

최근 TypeORM의 버전이 0.3.x로 올라가면서 많은 변경이 생겼다. 그 중 큰변화는 기존에 있던 레퍼지토리가 변경된 것이라고 생각합니다.

import {EntityRepository, Repository} from "typeorm";
import {User} from "../entity/User";

@EntityRepository(User)
export class UserRepository extends Repository<User> {}
@Module({
  imports:[TypeOrmModule.forFeature([User,UserRepository]),
  providers:[UserService],
})

기존에는 위와 같이 커스텀 레퍼지토리 클래스를 만들어 개발할 수 있었지만, @EntityRepository() 데코레이터가 depreceated되면서 사용이 위와 같이 사용할 수 없어졌다.

// user.repository.ts
export const UserRepository = dataSource.getRepository(User).extend({
    findByName(firstName: string, lastName: string) {
        return this.createQueryBuilder("user")
            .where("user.firstName = :firstName", { firstName })
            .andWhere("user.lastName = :lastName", { lastName })
            .getMany()
    },
})

// user.controller.ts
export class UserController {
    users() {
        return UserRepository.findByName("Timber", "Saw")
    }
}

공식문서에 따르면 위와 같이 커스텀 레퍼지터리를 사용하라고 하지만 NestJS에서는 보통 TypeOrmModule이라는 @nestjs/typeorm 패키지를 사용하여 dataSource를 저렇게 쉽게 가져올 수 없다. 또한 provider와 IoC컨테이너를 활용하는 Nest 프레임워크에 적합하지 않은 방법이라는 생각도 들었다.

간단하게 생각하고 찾은 방법은 3가지 정도이고, 어떤게 좋은 방법인지는 확실하게는 모르겠다.


Provider

provider는 Nest에 있는 기본 개념으로 의존성 주입(DI)될 수 있는 것들을 의미하며, Nest의 런타임 횐경속에서 각각의 provider들은 적절하게 주입된다.

Nest에서 의존성을 적절히 주입해주기 위해선 @Injectable() 데코레이터를 통해 Nest의 IoC컨테이너에 등록해주어야한다. 아래와 같이 주입되기 원하는 클래스위에 @Injectable() 데코레이터를 선언해줌으로서 UserService는 Nest의 IoC컨테이너에 등록된다.

@Injectable()
export class UserService {
}

이렇게 생성된 유저 서비스는 다른 클래스에 주입되어 사용할 수 있게 된다.

export class UserController {
	constructor(private readonly userService:UserService){};
}

이 때 기본적으로 UserService 는 싱글톤 인스턴스로 언제 어디서 주입하던 같은 인스턴스가 주입되게 된다.

이를 scope 라고하는데 DEFAULT, REQUEST, TRANSIENT가 있다.

  • DEFAULT: 싱글톤
  • REQUEST: 요청시마다 새로운 인스턴스를 주입한다.
  • TRANSIENT: 주입시마다 새로운 인스턴스를 주입하다.

Scope는 성능적 문제를 일으킬 수 있고 계층적 구조를 가지기 때문에 적절히 사용해야 한다.


Custom Repository

그렇다면 어떻게 커스텀 레퍼지토리를 만들고 사용해야 할까?

방법 1

간단하게 다른 provider들과 똑같이 사용하는 방법이 있다.

// user.repository.ts
@Injectable()
export class UserRepository extends Repository<User> {
  
  constructor(private readonly dataSource: DataSource){
    const baseRepository = dataSource.getRepository(User);
    super(baseRepository.target, baseRepository.manager, baseRepository.queryRunner);
  }
}
// user.module.ts
@Module({
  providers: [UserRepository]
})
export class UserModule {}

Repository<T> 는 TypeORM에서 사용되는 repository클래스로 아래와 같은 생성자를 통해 인스턴스화 된다. 따라서 dataSource를 injection받아 적절하게 값을 넣어준 방법이다.

 constructor(target: EntityTarget<Entity>, manager: EntityManager, queryRunner?: QueryRunner);

방법 2

위의 방법을 사용해 @CustomRepository() 데코레이터를 사용하는 방법이다. 0.2.x 버전에서 사용되던 @EntityRepository() 데코레이터와 방법이 유사하며 0.2.x에서 0.3.x버전으로 올릴 경우 사용하면 좋을 것 같다.

준비

우선 아래와 같이 CustomRepository 데코레이터를 정의해준다. SetMetadata는 어딘가에서 사용될 수 있는 메타데이터를 설정하는 것을 의미한다. Key-Value 방식으로 저장된다.

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

export const TYPEORM_EX_CUSTOM_REPOSITORY = 'TYPEORM_EX_CUSTOM_REPOSITORY';

export function CustomRepository(entity: Function): ClassDecorator {
  return SetMetadata(TYPEORM_EX_CUSTOM_REPOSITORY, entity);
}
export declare const SetMetadata: <K = string, V = any>(metadataKey: K, metadataValue: V) => CustomDecorator<K>;

SetMetadata에 의해 저장된 정보는 Reflector 클래스를 통해 접근할 수 있다. 이를 통해 아래와 같이 동적으로 모듈을 생성하는 TypeORMCustomModule을 정의해준다.

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

export class TypeOrmCustomModule {
  // CustomeRepository를 입력받음
  public static forCustomRepository<T extends new (...args: any[]) => any>(...repositories: T[]): DynamicModule {
    const providers: Provider[] = [];


    for (const repository of repositories) {
      // 각각의 커스텀레퍼지토리에 TYPEORM_EX_CUSTOM_REPOSITORY Key로 정의된 메타데이터를 가져온다
      const entity = Reflect.getMetadata(TYPEORM_EX_CUSTOM_REPOSITORY, repository);

      if (!entity) {
        continue;
      }
	  // dataSource를 주입받아 방법 1과 같이 커스텀 레퍼지토리를 만들어주고 provider에 추가한다.
      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: TypeOrmCustomModule,
      providers,
    };
  }
}

사용

TypeORM 0.2.x 버전과 유사하게 사용가능하다.

// user.repository.ts
@CustomRepository(User)
export class UserRepository extends Repository<User> {}
// user.module.ts
@Module({
  imports: [TypeOrmCustomModule.forCustomRepository(UserRepository)]
})
export class UserModule {}

방법 3

TypeORM 문서에서 볼 수 있는 Custom Repository를 생성하는 방법을 사용한다. provider를 추가하는 방법에는 여러가지가 있는데, 그 중 Factory를 사용하는 방법이다. 이를 위해선 필수적으로 서비스에서 UserRepository 타입을 선언하기 위해 interface 를 정의해주어야 한다.

// user.repository.ts
export interface UserRepository extends Repository<User> {
  getById(id: number): Promise<Board>;
}

export const UserRepositoryFactory = (dataSource: DataSource) =>
 // TypeORM문서에서 Custom Repository를 만드는 방법
dataSource.getRepository(User).extend({
    getById(id: number) {
      return this.findOneBy({ id });
    },
  });
// user.module.ts
@Module({
  providers: [
    {
      provide: 'UserRepository',
      useFactory: UserRepositoryFactory,
      inject: [getDataSourceToken()],
    },
  ],
  exports:['UserRepository']
})

대신 이 방법은 사용시 아래와 같이 @Inject('UserRepository')를 추가해주어야 하는 불편함이 있다. 또한 repository를 exports하기 위해선 위와 같이 String을 exports하고 import한 모듈 내에서 아래와 마찬가지로 사용할 수 있다.

// user.service.ts
@Injectable()
export class UserService {
  constructor(@Inject('UserRepository') private readonly userRepository: UserRepository){}
}
  • useFactory를 사용하는 방식에서 이렇게 동적으로 생성된 repository도 다른 provider들과 마찬가지로 싱글톤인지 궁금해서 테스트를 해보았다. 결과적으로 useFactory에서도 다른 설정을 해주지 않는 경우 싱글톤 인스턴스가 생성되고 주입되었다.

reference

profile
주니어 백엔드 🐶🦶🏻📏

0개의 댓글