2021. 11. 5. 19:12ㆍ개발/Node & Javascript
개요
- 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단에서 컨트롤한다고 생각하고 유의해서 사용해야할 것으로 보인다. ( 요 부분은 따로 실험을 해보아야 할 것으로 보인다. )
정리
간단하게 정리하자면 아래와 같다.
- 동적으로 component를 사용하고 싶을 때는 moduleRef를 사용하자.
- singleton일 때는 get을 사용하고, 다른 모듈에서 땡겨오고 싶을 때는 strcit옵션을 false로 주도록 하자.
- transient 또는 request scope의 component를 가져올 때는 resolve를 사용하고, contextId를 통해서 생성 또는 재활용할 수 있다.
츌처
'개발 > Node & Javascript' 카테고리의 다른 글
Nest.js 탐험기 5 - microservice (grpc) 를 사용해보자 - 통신방식 구현 (2) | 2022.01.23 |
---|---|
Nest.js 탐험기 4 - microservice (grpc) 를 사용해보자 - 튜토리얼편 (2) | 2021.12.28 |
Nest.js - Typescript alias를 사용해보자 (1) | 2021.10.17 |
Clinic.js를 사용해보자 (2) | 2021.05.01 |
Lerna, Yarn workspace를 사용해보자. (1) | 2021.04.11 |