Nest.js - moduleRef를 활용 해보자

2021. 11. 5. 19:12개발/Node & Javascript

728x90
반응형

개요

  • component를 type에 따라서 다르게 처리해야 되는 경우가 생긴다.
  • 이 때 사용하는 것이 moduleRef이다.
  • 이는 spring의 beanFactory와 유사하며 특정 context에 대한 component를 load하는데 도움이 된다.
  • component의 scope에 따라서 다르게 생성될 수 있다는 점에 유의하자.

예제 1 - 기본 예제

1. 시나리오

  • 요즘 차에 관심이 많아서 한번 차랑 연관지어서 한 번 살펴보도록 하자.
  • 기아 차를 생산하는 KiaSCarService와 현대 차를 생산하는 HyundaiCarService를 생성하자.
  • 그리고 moduleRef를 활용해서 query에 맞게 차를 생성해주도록 하자.

2. 코드

코드는 아래와 같이 간단하다.


/**
 * Entity
 */
export abstract class Car {
  abstract run(): void;
}

export class KiaCar extends Car {
  run(): void {
    console.log('kia!!! kia!!!');
  }
}

export class HyundaiCar extends Car {
  run(): void {
    console.log('hyundai!!! hyundai!!!');
  }
}


/***
 * Service 관련
 */
export abstract class CarService {
  abstract create(): Car;
}

@Injectable()
export class HyundaiCarService extends CarService {
  constructor() {
    super();
  }

  create(): Car {
    return new HyundaiCar();
  }
}

@Injectable()
export class KiaCarService extends CarService {
  constructor() {
    super();
  }

  create(): Car {
    return new KiaCar();
  }
}

/**
 * moduleRef로 대신 생성
 */
export const MANUFACTURES: Record<string, Type<CarService>> = {
  'kia': KiaCarService,
  'hyundai': HyundaiCarService,
};

export class CarFactory {
  static async create(manufacturer: string, ref: ModuleRef): Promise<Car> {
    return (await ref.get(MANUFACTURES[manufacturer], { strict: false })).create();
  }
}

/**
 * controller
 */
@Controller('/v1/cars')
export class CarController {
  constructor(private readonly moduleRef: ModuleRef) {}

  @Post()
  async create(@Query('manufacturer') manufacturer: string): Promise<Car> {
    const car = await CarFactory.create(manufacturer, this.moduleRef);
    car.run();
    return car;
  }
}

/**
 * 모듈로 내보내기 
 */
@Module({
  controllers: [CarController],
  providers: [...Object.values(MANUFACTURES)],
})
export class CarModule {}

/**
 * 앱 모듈에 등록해주자.
 */
@Module({
  imports: [
    CarModule,
    ...]
 })
export class AppModule

3. 결과

서버를 띄운 후 아래와 같이 curl을 날려보자.
아래와 같이 manufacturer에 따라서 알맞은 서비스의 service를 이용해 차를 생성하는 것을 볼 수 있다.


# 아래 curl을 실행하면 kia!!! kia!!!가 로그에 찍힌다.
curl --location --request POST 'localhost:3000/v1/cars?manufacturer=kia'

# hyundai!!! hyundai!!!가 로그에 찍힌다.
curl --location --request POST 'localhost:3000/v1/cars?manufacturer=hyundai'

예제 2 - 만약에 해당 context에 provider가 없을 경우

위의 예제에서 아래와 같이 HyundaiCarService를 제외해보자.

/**
 * 모듈로 내보내기
 */
@Module({
  controllers: [CarController],
  providers: [KiaCarService], // 기존 providers: [...Object.values(MANUFACTURES)],
})
export class CarModule {}

그리고 다시 curl을 hyndai로 호출해보면 아래와 같은 에러를 볼 수 있다.

[Nest] 26341   - 11/05/2021, 6:22:54 PM   [ExceptionsHandler] Nest could not find HyundaiCarService element (this provider does not exist in the current context) +3331ms
Error: Nest could not find HyundaiCarService element (this provider does not exist in the current context)

즉, 주어진 context에서 해당 service를 찾지 못한다는 것을 알 수 있다.
그렇다면 아래와 같이 다른 module에 해당 service를 넣어주고, appModule에 넣어주고 호출해보자.

@Module({
  providers: [HyundaiCarService],
})
export class HyundaiModule {}

// 다음에 appModule에 넣어주는걸 잊지말자

이렇게 하면, 문제없이 동작하는 것을 볼 수 있다.
어떻게 이렇게 되는 것일까?
우리가 CarFactory부분을 다시 살펴보자.

export class CarFactory {
  static async create(manufacturer: string, ref: ModuleRef): Promise<Car> {
    return (await ref.get(MANUFACTURES[manufacturer], { strict: false })).create();
  }
}

이 때 우리가 strict: false옵션을 준 것을 알 수 있다.
이는 해당 모듈 내의 context 뿐만이 아니라 global context를 찾아서 해당 Service를 가져올지 말지를 결정한다.
따라서 strict: true로 되어 있으면 외부에 있는 Service를 가져오지 못하게 되어 에러가 다시 발생하는 것을 볼 수 있다.

예제 3. scope을 transient 또는 request scope으로 변경하여 실행

이번에는 다시 1번 예제에서 HyundaiService의 scope을 변경해보자.

@Injectable({ scope: Scope.REQUEST }) // scope 추가
export class HyundaiCarService extends CarService {
  constructor() {
    super();
    // 로그를 잘 살펴보자
    console.log('created');
  }

  create(): Car {
    return new HyundaiCar();
  }
}

그리고 다시 curl을 호출하면 아래와 같은 에러를 볼 수 있다.

Error: HyundaiCarService is marked as a scoped provider. Request and transient-scoped providers can't be used in combination with "get()" method. Please, use "resolve()" instead.

이 때는 get()이 아닌 resolve()를 호출하면 된다.

export class CarFactory {
  static async create(manufacturer: string, ref: ModuleRef): Promise<Car> {
    // return (await ref.get(MANUFACTURES[manufacturer], { strict: true })).create();
    return (await ref.resolve(MANUFACTURES[manufacturer])).create();
  }
}

그런데, curl을 계속 찍다보면 constructor에 남긴 log가 계속 나온다. 즉, instance가 계속 생성된다는 것을 확인 할 수 있다.
이는 Transient나 Request의 scope를 지닌 component들은 singleton이 아니고, 특정 context 기반으로 계속 생성이 되기 때문이다.
그렇다면 contextId를 단일로 고정시켜 보자.

export class CarFactory {
  // contextId 고정
  static contextId = ContextIdFactory.create();
  static async create(manufacturer: string, ref: ModuleRef): Promise<Car> {
    // return (await ref.get(MANUFACTURES[manufacturer], { strict: true })).create();
    return (await ref.resolve(MANUFACTURES[manufacturer], CarFactory.contextId)).create();
  }
}

이렇게 특정 contextId를 지정하면 최초 1회만 생성이 되고, 그 이후에는 재활용하는 것을 볼 수 있다.
하지만 contextId에 매핑된 인스턴스가 무한정 매핑되면 메모리 문제가 있기 때문에 정리를 할텐데, 이 타이밍이 언제일지는 framework단에서 컨트롤한다고 생각하고 유의해서 사용해야할 것으로 보인다. ( 요 부분은 따로 실험을 해보아야 할 것으로 보인다. )

정리

간단하게 정리하자면 아래와 같다.

  1. 동적으로 component를 사용하고 싶을 때는 moduleRef를 사용하자.
  2. singleton일 때는 get을 사용하고, 다른 모듈에서 땡겨오고 싶을 때는 strcit옵션을 false로 주도록 하자.
  3. transient 또는 request scope의 component를 가져올 때는 resolve를 사용하고, contextId를 통해서 생성 또는 재활용할 수 있다.

츌처

728x90
반응형