NestJS 컨테이너, DI는 어떻게 작동할까 (3)
다시 들어가며
- 이어서 instanceLoader.createInstancesOfDependencies() 에 대해 알아보고, 의존성에 대한 인스턴스가 어떻게 만들어지고 컨테이너 초기화가 어떻게 진행되어 마무리되는지 알아보겠습니다.
1. IoC 컨테이너 부터 확인해보자
await ExceptionsZone.asyncRun(
async () => {
// 의존성 스캐너(dependenciesScanner)를 사용해 전달된 모듈의 모든 의존성을 스캔합니다.
await dependenciesScanner.scan(module);
// 인스턴스 로더(instanceLoader)를 사용해 모든 의존성의 인스턴스를 생성합니다.
await instanceLoader.createInstancesOfDependencies(); // 확인 필요!
// 의존성 스캐너(dependenciesScanner)를 사용해 애플리케이션의 제공자(Providers)를 적용합니다.
dependenciesScanner.applyApplicationProviders();
},
// asyncRun 블록을 종료하고, 예외가 발생할 경우 teardown을 처리하고 로그 자동 플러시 여부를 설정합니다.
teardown,
this.autoFlushLogs
);
- NestFactory.create 메서드에서 이번에는 instanceLoader.createInstancesOfDependencie를 살펴보려고 합니다.
2. InstanceLoader
createInstancesOfDependencies
public async createInstancesOfDependencies(
modules: Map<string, Module> = this.container.getModules(),
) {
// 각 모듈의 프로토타입을 생성합니다.
this.createPrototypes(modules);
try {
// 비동기적으로 모든 모듈의 인스턴스를 생성합니다.
await this.createInstances(modules);
} catch (err) {
// 실패 시
// 그래프 인스펙터를 통해 모듈을 검사합니다.
this.graphInspector.inspectModules(modules);
// 발생한 예외를 그래프 인스펙터에 부분적으로 등록합니다.
this.graphInspector.registerPartial(err);
throw err;
}
// 그래프 인스펙터를 통해 모듈을 검사합니다.
this.graphInspector.inspectModules(modules);
}
- initialize시 위에서 알아본 dependenciesScanner.scan() 이후에 해당 메소드를 호출합니다.
- 해당 메서드는 모든 모듈의 의존성을 로드하고 인스턴스화하는 역할을 수행합니다. 기본적으로 container의 모든 모듈을 인수로 사용합니다.
createPrototypes
// packages/core/injector/instance-loader.ts
private createPrototypes(modules: Map<string, Module>) {
modules.forEach(moduleRef => {
this.createPrototypesOfProviders(moduleRef);
this.createPrototypesOfInjectables(moduleRef);
this.createPrototypesOfControllers(moduleRef);
});
}
private createPrototypesOfProviders(moduleRef: Module) {
const { providers } = moduleRef;
providers.forEach(wrapper =>
this.injector.loadPrototype<Injectable>(wrapper, providers),
);
}
private createPrototypesOfControllers(moduleRef: Module) {
const { controllers } = moduleRef;
controllers.forEach(wrapper =>
this.injector.loadPrototype<Controller>(wrapper, controllers),
);
}
private createPrototypesOfInjectables(moduleRef: Module) {
const { injectables } = moduleRef;
injectables.forEach(wrapper =>
this.injector.loadPrototype(wrapper, injectables),
);
}
- createPrototypes 메서드는 각 모듈의 모든 프로토타입을 생성합니다.
- injector.loadPrototype을 알아보겠습니다.
// packages/core/injector/injector.ts
public loadPrototype<T>(
{ token }: InstanceWrapper<T>,
collection: Map<InstanceToken, InstanceWrapper<T>>,
contextId = STATIC_CONTEXT,
) {
// contextId = STATIC_CONTEXT
if (!collection) {
return;
}
// 토큰으로 메타데이터를 불러옵니다.
const target = collection.get(token);
// 프로토타입을 생성합니다.
const instance = target.createPrototype(contextId);
// 만약 새로 생성되었다면
if (instance) {
// InstanceWrapper로 한 번 감쌉니다.
const wrapper = new InstanceWrapper({
...target,
instance,
});
// 주어진 collection에 등록합니다. ex) collection: providers | controllers |injectables
collection.set(token, wrapper);
}
}
- 토큰으로 메타데이터를 불러오고, 해당 정보를 바탕으로 프로토타입을 생성한 후 컬렉션에 등록합니다.
- 여기서 사용하는 createPrototype 메서드에서도 더 알아보겠습니다.
// packages/core/injector/instance-wrapper.ts
public createPrototype(contextId: ContextId) {
const host = this.getInstanceByContextId(contextId);
// 인스턴스가 만들어졌다면 반환하지 않습니다.
if (!this.isNewable() || host.isResolved) {
return;
}
// 새로 만들어서 반환한다.
return Object.create(this.metatype.prototype);
}
public getInstanceByContextId(
contextId: ContextId,
inquirerId?: string,
): InstancePerContext<T> {
// 프로바이더 스코프가 TRANSIENT && inquirerId 존재
if (this.scope === Scope.TRANSIENT && inquirerId) {
return this.getInstanceByInquirerId(contextId, inquirerId);
}
// 해당 맥락에서의 인스턴스를 가져옵니다.
const instancePerContext = this.values.get(contextId);
return instancePerContext
// 이미 해당 맥락 내에서 인스턴스가 존재한다면 그대로 반환합니다.
? instancePerContext
// 존재하지 않으면, 정적 인스턴스로부터 복사해서 만들어냅니다.
: this.cloneStaticInstance(contextId);
}
- InstanceLoader는 의존성 주입에 사용될 인스턴스들을 load하는 클래스입니다.
- 해당 instanceLoader를 사용해 모든 의존성의 인스턴스를 생성하는데 호출됩니다. (instanceLoader.createInstancesOfDependencies())
- 모든 모듈의 프로토타입 / 인스턴스를 생성합니다.
- 각 모듈의 provider, injectable, controller 인스턴스를 생성합니다.
참고) 프로토타입은 왜 생성할까?
- NestJS에서 프로토타입을 생성하는 것은 의존성 주입(DI)에서 성능과 효율성을 높이기 위한 방법 중 하나입니다.
- 특정 클래스의 인스턴스를 생성하는 대신 클래스의 프로토타입만 생성하고, 이를 사용해 나중에 인스턴스 생성에 활용합니다.
- 역할
- 기본 속성 제공: 프로토타입은 객체가 상속받을 기본 속성들을 정의하는 역할을 합니다. 새 인스턴스를 생성할 때 클래스의 프로토타입을 복사하여 상속 구조를 유지합니다.
- 메서드 재사용: 프로토타입을 통해 모든 인스턴스가 동일한 메서드를 공유하게 되어 메모리를 절약하고, 메서드 호출 시 프로토타입 체인을 활용해 성능을 최적화합니다.
- 메모리 절약: 프로토타입을 미리 만들어두고 인스턴스 생성 시 이를 활용하면, 새로운 인스턴스를 만들 때마다 클래스 메타타입의 메서드와 속성을 복사할 필요가 없어 메모리 사용량을 줄일 수 있습니다.
- 성능 향상: 프로토타입을 통해 인스턴스가 필요한 메서드를 모두 공유하게 하므로 중복된 메서드를 인스턴스마다 생성할 필요가 없어 성능을 향상시킵니다.
- DI 컨테이너 관리: 의존성 주입 컨테이너가 각 인스턴스에 대한 프로토타입을 미리 생성하고 관리함으로써, 인스턴스 간의 복잡한 의존 관계를 보다 효율적으로 다룰 수 있습니다.
createInstances
// 모든 모듈의 인스턴스를 생성하는 비동기 메서드
private async createInstances(modules: Map<string, Module>) {
// 모든 모듈에 대한 인스턴스 생성을 병렬로 수행하기 위해 Promise.all을 사용하고
// 각 모듈에 대해 비동기 작업을 실행
await Promise.all(
[...modules.values()].map(async moduleRef => {
// 해당 모듈의 모든 프로바이더 인스턴스를 생성합니다.
await this.createInstancesOfProviders(moduleRef);
// 해당 모듈의 모든 injectable 인스턴스를 생성합니다.
await this.createInstancesOfInjectables(moduleRef);
// 해당 모듈의 모든 컨트롤러 인스턴스를 생성합니다.
await this.createInstancesOfControllers(moduleRef);
// 모듈 참조에서 모듈의 이름을 가져옵니다.
const { name } = moduleRef;
// 모듈 이름이 화이트리스트에 포함되어 있다면, 해당 모듈 초기화 메시지를 로거에 기록합니다.
this.isModuleWhitelisted(name) &&
this.logger.log(MODULE_INIT_MESSAGE`${name}`);
}),
);
}
// 해당 모듈의 모든 프로바이더 인스턴스를 생성하는 비동기 메서드
private async createInstancesOfProviders(moduleRef: Module) {
// 모듈 참조에서 프로바이더 목록을 가져옵니다.
const { providers } = moduleRef;
// 프로바이더 목록을 배열로 변환합니다.
const wrappers = [...providers.values()];
// 모든 프로바이더에 대한 인스턴스 생성을 병렬로 처리합니다.
await Promise.all(
wrappers.map(async item => {
// 각 프로바이더에 대해 인젝터가 해당 제공자를 로드하도록 호출합니다.
await this.injector.loadProvider(item, moduleRef);
// 그래프 인스펙터가 각 인스턴스 래퍼를 검사하도록 합니다.
this.graphInspector.inspectInstanceWrapper(item, moduleRef);
}),
);
}
// 해당 모듈의 모든 컨트롤러 인스턴스를 생성하는 비동기 메서드
private async createInstancesOfControllers(moduleRef: Module) {
// 모듈 참조에서 컨트롤러 목록을 가져옵니다.
const { controllers } = moduleRef;
// 컨트롤러 목록을 배열로 변환합니다.
const wrappers = [...controllers.values()];
// 모든 컨트롤러에 대한 인스턴스 생성을 병렬로 처리합니다.
await Promise.all(
wrappers.map(async item => {
// 각 컨트롤러에 대해 인젝터가 해당 컨트롤러를 로드하도록 호출합니다.
await this.injector.loadController(item, moduleRef);
// 그래프 인스펙터가 각 인스턴스 래퍼를 검사하도록 합니다.
this.graphInspector.inspectInstanceWrapper(item, moduleRef);
}),
);
}
// 해당 모듈의 모든 injectable 인스턴스를 생성하는 비동기 메서드
private async createInstancesOfInjectables(moduleRef: Module) {
// 모듈 참조에서 injectable 목록을 가져옵니다.
const { injectables } = moduleRef;
// injectable 목록을 배열로 변환합니다.
const wrappers = [...injectables.values()];
// 모든 injectable에 대한 인스턴스 생성을 병렬로 처리합니다.
await Promise.all(
wrappers.map(async item => {
// 각 인젝터블에 대해 인젝터가 해당 인젝터블을 로드하도록 호출합니다.
await this.injector.loadInjectable(item, moduleRef);
// 그래프 인스펙터가 각 인스턴스 래퍼를 검사하도록 합니다.
this.graphInspector.inspectInstanceWrapper(item, moduleRef);
}),
);
}
// 특정 모듈이 화이트리스트에 포함되어 있는지 확인하는 메서드
private isModuleWhitelisted(name: string): boolean {
// 모듈 이름이 InternalCoreModule의 이름과 다르면
// true를 반환하여 화이트리스트에 포함됨을 나타냅니다.
return name !== InternalCoreModule.name;
}
- createInstances 메서드는 각 모듈 별 프로바이더, injectable, controller를 불러오고 인스턴스화 시킵니다.
- 우선 대표적으로 불러오는 부분인 injector.loadProvider을 알아보겠습니다.
// packages/core/injector/injector.ts
public async loadProvider(
wrapper: InstanceWrapper<Injectable>,
moduleRef: Module,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
) {
const providers = moduleRef.providers;
await this.loadInstance<Injectable>(
wrapper,
providers,
moduleRef,
contextId,
inquirer,
);
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
}
- 프로바이더를 뽑아서 loadInstance 메서드로 인스턴스를 로드합니다.
- loadInstance를 조금 더 상세하게 보겠습니다.
// packages/core/injector/injector.ts
public async loadInstance<T>(
wrapper: InstanceWrapper<T>, // provider
collection: Map<InjectionToken, InstanceWrapper>, // metadatas
moduleRef: Module, // module
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
) {
// inquirer에 대한 고유 식별자를 가져옵니다.
const inquirerId = this.getInquirerId(inquirer);
// contextId와 inquirerId를 이용하여 현재 컨텍스트의 인스턴스 호스트를 가져옵니다.
const instanceHost = wrapper.getInstanceByContextId(
this.getContextId(contextId, wrapper),
inquirerId,
);
// 해당 인스턴스 호스트가 아직 초기화 중인지 확인합니다.
if (instanceHost.isPending) {
// wrapper에 있는 정착 신호(settlementSignal)를 가져옵니다.
const settlementSignal = wrapper.settlementSignal;
// inquirer가 존재하고, 해당 신호가 순환 의존성에 속한다면
if (inquirer && settlementSignal?.isCycle(inquirer.id)) {
// "${wrapper.name}"로 표시되는 순환 의존성 예외를 발생시킵니다.
throw new CircularDependencyException(`"${wrapper.name}"`);
}
// 초기화가 진행 중인 경우, 초기화 완료 후에 donePromise로 오류를 확인합니다.
return instanceHost.donePromise.then((err?: unknown) => {
// 오류가 있다면 오류를 발생합니다.
if (err) {
throw err;
}
});
}
// 해당 인스턴스 호스트와 wrapper에 정착 신호를 적용합니다.
const settlementSignal = this.applySettlementSignal(instanceHost, wrapper);
// wrapper의 토큰 또는 이름을 token으로 가져옵니다.
const token = wrapper.token || wrapper.name;
// wrapper의 inject 속성을 추출합니다.
const { inject } = wrapper;
// 전달된 토큰으로부터 해당 targetWrapper를 collection 맵에서 가져옵니다.
const targetWrapper = collection.get(token);
// 대상 wrapper가 정의되어 있지 않으면 에러 발생
if (isUndefined(targetWrapper)) {
throw new RuntimeException();
}
// 인스턴스 호스트가 이미 resolve되었다면 정착 신호를 완료로 설정하고 반환합니다.
if (instanceHost.isResolved) {
return settlementSignal.complete();
}
try {
// 현재 시간을 t0로 가져옵니다.
const t0 = this.getNowTimestamp();
// 인스턴스 배열을 매개변수로 하는 비동기 콜백을 정의합니다.
const callback = async (instances: unknown[]) => {
// 주입할 프로퍼티를 비동기적으로 확인합니다.
const properties = await this.resolveProperties(
wrapper,
moduleRef,
inject as InjectionToken[],
contextId,
wrapper,
inquirer,
);
// **클래스 인스턴스를 생성합니다.**
const instance = await this.instantiateClass(
instances,
wrapper,
targetWrapper,
contextId,
inquirer,
);
// 생성된 인스턴스에 확인된 프로퍼티를 적용합니다.
this.applyProperties(instance, properties);
// 초기화 시간을 계산하여 wrapper의 initTime 속성에 저장합니다.
wrapper.initTime = this.getNowTimestamp() - t0;
// 정착 신호를 완료로 설정합니다.
settlementSignal.complete();
};
// 생성자의 매개변수에 대한 의존성을 처리합니다. 콜백 함수를 호출합니다.
await this.resolveConstructorParams<T>(
wrapper,
moduleRef,
inject as InjectionToken[],
callback,
contextId,
wrapper,
inquirer,
);
} catch (err) {
// 에러가 발생하면 정착 신호를 오류로 설정합니다.
settlementSignal.error(err);
throw err;
}
}
- 전달받은 wrapper의 인스턴스가 초기화 중인지 확인하고 초기화 중이 아니라면 초기화를 진행합니다.
- 생성자의 매개변수에 대한 의존성을 처리하고, 콜백 함수를 통해 인스턴스를 만들어 반환합니다.
주어진 생성자 매개변수의 의존성을 찾고 인스턴스화 하는 과정
flowchart TD
K[resolveConstructorParams] --> J(resolveSingleParam)
J[resolveSingleParam] --> A(resolveComponentInstance)
A[resolveComponentInstance] --> B(lookupComponent)
B --> C{모듈의 provider에 있는가?}
C -->|yes| D[instanceWrapper 반환]
C -->|no| E(lookupComponentInParentModules)
E --> F(lookupComponentInImports)
F --> G{모듈의 provider, exports에 있는가?}
G -->|yes| H[instanceWrapper 반환]
G -->|no, 찾을때까지 DFS| F[lookupComponentInImports]
- 상세적으로 코드로 확인하겠지만, 하나의 생성자 의존성을 찾고 인스턴스화 하기까지 다음과 같은 과정을 거칩니다.
- 재귀를 돌며(DFS) 같은 토큰을 가진 모듈을 찾을 때까지 반복한다는 점이 흥미롭습니다!
resolveConstructorParams
// packages/core/injector/injector.ts
public async resolveConstructorParams<T>(
wrapper: InstanceWrapper<T>,
moduleRef: Module,
inject: InjectorDependency[],
callback: (args: unknown[]) => void | Promise<void>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
parentInquirer?: InstanceWrapper,
) {
let inquirerId = this.getInquirerId(inquirer);
// wrapper로부터 생성자 메타데이터를 가져옵니다.
const metadata = wrapper.getCtorMetadata();
// 메타데이터가 존재하고 컨텍스트가 정적(STATIC_CONTEXT)이 아닌 경우에 대해 조건문을 시작합니다.
if (metadata && contextId !== STATIC_CONTEXT) {
// 인스턴스를 새로 불러와서 콜백을 호출합니다.
// 생성자 메타데이터를 기반으로 의존성을 비동기적으로 불러옵니다.
const deps = await this.loadCtorMetadata(
metadata,
contextId,
inquirer,
parentInquirer,
);
// 가져온 의존성 목록을 callback 함수에 전달하고 메서드를 종료합니다.
return callback(deps);
}
// 팩토리 제공자일 경우 팩토리 의존성을, 그렇지 않은 경우 클래스 의존성을 가져옵니다.
// 각각의 위치에 맞는 토큰이나 타입을 반환해서 저장한다.
// 전자의 경우 PARAMTYPES_METADATA 메타데이터를 SELF_DECLARED_DEPS_METADATA 메타데이터로 덮어씌운 값으로 지정된다
// PARAMTYPES_METADATA -> 데코레이터가 달린 클래스의 생성자 매개변수의 타입을 알 수 있음
// nest가 아닌 reflect-metadata로 인해 알 수 있는 것
// SELF_DECLARED_DEPS_METADATA nest에서 찾아온 값이 아니고 명시적으로 Inject() 했을 때 저장됨
const isFactoryProvider = !isNil(inject);
const [dependencies, optionalDependenciesIds] = isFactoryProvider
? this.getFactoryProviderDependencies(wrapper)
: this.getClassDependencies(wrapper);
let isResolved = true;
const resolveParam = async (param: unknown, index: number) => {
try {
// 매개변수가 parentInquirer와 일치하는지 확인하고
// 그렇다면 parentInquirer의 인스턴스를 반환합니다.
if (this.isInquirer(param, parentInquirer)) {
return parentInquirer && parentInquirer.instance;
}
if (inquirer?.isTransient && parentInquirer) {
inquirer = parentInquirer;
inquirerId = this.getInquirerId(parentInquirer);
}
// 매개변수 하나의 의존성을 해결합니다.
// 매개변수 래퍼를 반환합니다.
const paramWrapper = await this.resolveSingleParam<T>(
wrapper,
param,
{ index, dependencies },
moduleRef,
contextId,
inquirer,
index,
);
// 매개변수 래퍼의 현재 맥락의 인스턴스를 가져옵니다.
const instanceHost = paramWrapper.getInstanceByContextId(
this.getContextId(contextId, paramWrapper),
inquirerId,
);
if (!instanceHost.isResolved && !paramWrapper.forwardRef) {
isResolved = false;
}
// 의존성 처리가 완료되었다면 의존성의 인스턴스를 반환합니다.
return instanceHost?.instance;
} catch (err) {
const isOptional = optionalDependenciesIds.includes(index);
if (!isOptional) {
throw err;
}
return undefined;
}
};
// 생성자의 의존성을 모두 돌며 resolveParam, 의존성 처리를 하고 인스턴스를 반환
const instances = await Promise.all(dependencies.map(resolveParam));
// 정상적으로 처리되었다면 생성자의 의존성 인스턴스들을 담아 콜백을 호출합니다.
isResolved && (await callback(instances));
}
- 생성자 매개변수의 모든 메타데이터를 가져오고, 모든 의존성을 돌면서 의존성을 처리하고 인스턴스를 반환합니다.
resolveSingleParam
// packages/core/injector/injector.ts
public async resolveSingleParam<T>(
wrapper: InstanceWrapper<T>,
// 처리할 의존성 정보 (토큰이나 타입)
param: Type<any> | string | symbol | any,
// 현재 처리할 의존성의 인덱스와, 생성자 내의 모든 의존성 정보를 갖는 배열
dependencyContext: InjectorDependencyContext,
moduleRef: Module,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
keyOrIndex?: symbol | string | number,
) {
if (isUndefined(param)) {
this.logger.log(
'Nest encountered an undefined dependency. This may be due to a circular import or a missing dependency declaration.',
);
throw new UndefinedDependencyException(
wrapper.name,
dependencyContext,
moduleRef,
);
}
// 의존성 정보(토큰)를 가져와서
const token = this.resolveParamToken(wrapper, param);
// 의존성을 처리한다.
return this.resolveComponentInstance<T>(
moduleRef,
token,
dependencyContext,
wrapper,
contextId,
inquirer,
keyOrIndex,
);
}
- 크게 특별한 코드는 없으며, resolveParamToken / resolveComponentInstance 메소드를 살펴보겠습니다.
resolveParamToken
// packages/core/injector/injector.ts
public resolveParamToken<T>(
wrapper: InstanceWrapper<T>,
param: Type<any> | string | symbol | any,
) {
// ForwardRef 상태가 아니라면 그대로 반환합니다.
if (!param.forwardRef) {
return param;
}
// 매개변수가 ForwardRef 상태이면,
// 해당 매개변수가 포함된 생성자의 클래스도 ForwardRef 상태로 바꿉니다.
wrapper.forwardRef = true;
// forwardRef 메서드를 통해 실제 값을 가져옵니다.
return param.forwardRef();
}
resolveComponentInstance
// packages/core/injector/injector.ts
public async resolveComponentInstance<T>(
moduleRef: Module,
token: InstanceToken,
dependencyContext: InjectorDependencyContext,
wrapper: InstanceWrapper<T>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
keyOrIndex?: symbol | string | number,
): Promise<InstanceWrapper> {
this.printResolvingDependenciesLog(token, inquirer);
this.printLookingForProviderLog(token, moduleRef);
// 모듈의 모든 프로바이더들을 가져와서
const providers = moduleRef.providers;
// 의존성을 찾는다.
// 1. 현재 모듈의 프로바이더들 중에, 현재 처리할 의존성의 토큰과 같은 토큰을 갖는
// 프로바이더가 있다면, 해당 정보를 가져와 저장하고 반환.
// 2. 없다면 부모 모듈, 즉 imports 배열에 있는 모듈들에서 찾아보고, 찾을 때까지
// 재귀적으로 반복.
const instanceWrapper = await this.lookupComponent(
providers,
moduleRef,
{ ...dependencyContext, name: token },
wrapper,
contextId,
inquirer,
keyOrIndex,
);
// 의존성 처리 완료, instanceWrapper 반환
return this.resolveComponentHost(
moduleRef,
instanceWrapper,
contextId,
inquirer,
);
}
- 의존성을 lookupComponent를 통해 탐색합니다.
lookupComponent
public async lookupComponent<T = any>(
providers: Map<Function | string | symbol, InstanceWrapper>,
moduleRef: Module,
dependencyContext: InjectorDependencyContext,
wrapper: InstanceWrapper<T>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
keyOrIndex?: symbol | string | number,
): Promise<InstanceWrapper<T>> {
const token = wrapper.token || wrapper.name;
const { name } = dependencyContext;
if (wrapper && token === name) {
throw new UnknownDependenciesException(
wrapper.name,
dependencyContext,
moduleRef,
{ id: wrapper.id },
);
}
// provider 들에서 찾는 이름이 있으면
if (providers.has(name)) {
const instanceWrapper = providers.get(name);
//provider에서 하나 꺼내옵니다.
this.printFoundInModuleLog(name, moduleRef);
//의존성 메타데이터를 집어 넣습니다.
this.addDependencyMetadata(keyOrIndex, wrapper, instanceWrapper);
// 찾았으니 반환합니다.
return instanceWrapper;
}
// provider들에서 찾는 이름이 없으면 부모 모듈에서 찾습니다.
return this.lookupComponentInParentModules(
dependencyContext,
moduleRef,
wrapper,
contextId,
inquirer,
keyOrIndex,
);
}
- provider에서 찾으면 반환합니다.
- 만약 provider에 없다면 부모 모듈에서 찾습니다.
lookupComponentInParentModules
public async lookupComponentInParentModules<T = any>(
dependencyContext: InjectorDependencyContext,
moduleRef: Module,
wrapper: InstanceWrapper<T>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
keyOrIndex?: symbol | string | number,
) {
// imports에서 찾습니다.
const instanceWrapper = await this.lookupComponentInImports(
moduleRef,
dependencyContext.name,
wrapper,
[],
contextId,
inquirer,
keyOrIndex,
);
if (isNil(instanceWrapper)) {
throw new UnknownDependenciesException(
wrapper.name,
dependencyContext,
moduleRef,
{ id: wrapper.id },
);
}
return instanceWrapper;
}
- 모듈의 imports에서 탐색을 시작해봅니다.
lookupComponentInParentModules
// 재귀는 여기서 발생합니다.
public async lookupComponentInImports(
moduleRef: Module,
name: InjectionToken,
wrapper: InstanceWrapper,
moduleRegistry: any[] = [],
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
keyOrIndex?: symbol | string | number,
isTraversing?: boolean,
): Promise<any> {
let instanceWrapperRef: InstanceWrapper = null;
// imports를 가져온다
const imports = moduleRef.imports || new Set<Module>();
const identity = (item: any) => item;
// import의 value들을 가져옵니다
let children = [...imports.values()].filter(identity);
// 재귀라면,
if (isTraversing) {
const contextModuleExports = moduleRef.exports;
children = children.filter(child =>
contextModuleExports.has(child.metatype),
);
}
for (const relatedModule of children) {
// moduleRegistry 이미 돌았던 것들에 리스트에 있으면 건너뜁니다.
if (moduleRegistry.includes(relatedModule.id)) {
continue;
}
this.printLookingForProviderLog(name, relatedModule);
// 한번 돌았으니 push한다.
moduleRegistry.push(relatedModule.id);
// import한 모듈의 providers, exports를 가져온다
const { providers, exports } = relatedModule;
// exports나 provider에 없으면 다시 재귀로 import 모듈의 import에서 찾는다 (재귀)
if (!exports.has(name) || !providers.has(name)) {
const instanceRef = await this.lookupComponentInImports(
relatedModule,
name,
wrapper,
moduleRegistry,
contextId,
inquirer,
keyOrIndex,
true,
);
// 여기서 찾으면 끝낸다.
if (instanceRef) {
this.addDependencyMetadata(keyOrIndex, wrapper, instanceRef);
return instanceRef;
}
// 못찾으면 한번 더 돈다. (해당 모듈의 다음 import)
continue;
}
// 여기까지 오면 exports나 provider에 있는 것이다.
this.printFoundInModuleLog(name, relatedModule);
// providers에서 가져온다.
instanceWrapperRef = providers.get(name);
// 넣는다
this.addDependencyMetadata(keyOrIndex, wrapper, instanceWrapperRef);
const inquirerId = this.getInquirerId(inquirer);
const instanceHost = instanceWrapperRef.getInstanceByContextId(
this.getContextId(contextId, instanceWrapperRef),
inquirerId,
);
if (!instanceHost.isResolved && !instanceWrapperRef.forwardRef) {
wrapper.settlementSignal?.insertRef(instanceWrapperRef.id);
await this.loadProvider(
instanceWrapperRef,
relatedModule,
contextId,
wrapper,
);
break;
}
}
return instanceWrapperRef;
}
- import를 순회합니다. 해당 모듈들의 exports, provider에서 탐색하고, 탐색하지 못하면 다음 import한 모듈로 넘어가서 탐색합니다.
- import를 모두 순회해도 없다면 본래 모듈의 exports, provider에서 탐색합니다.
- 이렇게 깊이 우선으로 탐색한 의존성을 반환합니다.
다시 resolveConstructorParams부터 인스턴스 생성까지
// packages/core/injector/injector.ts
public async resolveConstructorParams<T>(
wrapper: InstanceWrapper<T>,
moduleRef: Module,
inject: InjectorDependency[],
callback: (args: unknown[]) => void | Promise<void>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
parentInquirer?: InstanceWrapper,
) {
...
let isResolved = true;
const resolveParam = async (param: unknown, index: number) => {
try {
...
// 이제 잘 반환된 의존성을 사용합니다!
const paramWrapper = await this.resolveSingleParam<T>(
wrapper,
param,
{ index, dependencies },
moduleRef,
contextId,
inquirer,
index,
);
// 매개변수 래퍼의 현재 맥락의 인스턴스를 가져옵니다.
const instanceHost = paramWrapper.getInstanceByContextId(
this.getContextId(contextId, paramWrapper),
inquirerId,
);
if (!instanceHost.isResolved && !paramWrapper.forwardRef) {
isResolved = false;
}
// 의존성 처리가 잘 되었으니 완료되었다면 의존성의 인스턴스를 반환합니다.
return instanceHost?.instance;
} catch (err) {
const isOptional = optionalDependenciesIds.includes(index);
if (!isOptional) {
throw err;
}
return undefined;
}
};
const instances = await Promise.all(dependencies.map(resolveParam));
// 정상적으로 처리되었다면 생성자의 의존성 인스턴스들을 담아 콜백을 호출합니다.
isResolved && (await callback(instances));
}
- 이제 위에서 처리된 의존성이 리턴되는 것을 알 수 있습니다. (resolveSingleParam)
- 해당 의존성을 instanceHost?.instance 를 통해 인스턴스를 반환합니다.
- 이제 이렇게 모여진 생성자의 인스턴스들을 콜백에 넘깁니다!
callback
const callback = async (instances: unknown[]) => {
// 처리된 의존성들의 배열이 전달됩니다.
// 주입할 프로퍼티를 비동기적으로 확인합니다.
const properties = await this.resolveProperties(wrapper, moduleRef, inject as InjectionToken[], contextId, wrapper, inquirer);
// **클래스 인스턴스를 생성합니다. 인스턴스 생성!**
const instance = await this.instantiateClass(instances, wrapper, targetWrapper, contextId, inquirer);
// 생성된 인스턴스에 확인된 프로퍼티를 적용합니다.
// 위에서 처리한 의존성을 만들어진 인스턴스에 적용하는 메서드
this.applyProperties(instance, properties);
// 초기화 시간을 계산하여 wrapper의 initTime 속성에 저장합니다.
wrapper.initTime = this.getNowTimestamp() - t0;
// 정착 신호를 완료로 설정합니다.
settlementSignal.complete();
};
- 아래와 같이 AppModule에 있는 AppController가 AppService를 생성자를 통해 의존성을 주입해주고 있다고 가정하겠습니다.
import { Controller, Get, Post } from "@nestjs/common";
import { AppService } from "./app.service";
import { TestService } from "./test.service";
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly testService: TestService,
) { }
...
}
- 콜백 함수에 있는 targetWrapper / instances / instance 변수는 아래와 같습니다.
// 대상이 될 AppController
{
targetWrapper: InstanceWrapper {
isAlias: false,
scope: undefined,
values: WeakMap { <items unknown> },
token: [class AppController],
name: 'AppController',
metatype: [class AppController],
durable: undefined,
host: Module {
_metatype: [class AppModule],
container: [NestContainer],
_imports: [Set],
_providers: [Map],
_injectables: Map(0) {},
_middlewares: Map(0) {},
_controllers: [Map],
_entryProviderKeys: Set(0) {},
_exports: Set(0) {},
_distance: 0,
_initOnPreview: false,
_isGlobal: false,
_id: '0f80f9e022edd38efc8a1',
_token: '024ef4b961c95d3555c3f123e928e8434bab1f0f3746b0cd9e60594269b97899'
},
settlementSignal: SettlementSignal {
_refs: Set(0) {},
completed: true,
settleFn: [Function (anonymous)],
settledPromise: [Promise]
},
isTreeStatic: true,
initTime: 1.9916669987142086,
[Symbol(instance_metadata:cache)]: { dependencies: [Array] },
[Symbol(instance_metadata:id)]: 'c8a1271e1594d66877780'
}
}
// constructor params
{ instances: [ AppService {}, TestService {} ] }
// resolve 이후 결과
{
instance: AppController {
appService: AppService {},
testService: TestService {}
}
}
instantiateClass
public async instantiateClass<T = any>(
instances: any[], // 처리된 인스턴스들
wrapper: InstanceWrapper, // 토큰 정보 등
targetMetatype: InstanceWrapper,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
): Promise<T> {
const { metatype, inject } = wrapper;
const inquirerId = this.getInquirerId(inquirer);
// 현재 맥락에서 필요한 인스턴스
const instanceHost = targetMetatype.getInstanceByContextId(
this.getContextId(contextId, targetMetatype),
inquirerId,
);
const isInContext =
wrapper.isStatic(contextId, inquirer) ||
wrapper.isInRequestScope(contextId, inquirer) ||
wrapper.isLazyTransient(contextId, inquirer) ||
wrapper.isExplicitlyRequested(contextId, inquirer);
if (this.options?.preview && !wrapper.host?.initOnPreview) {
instanceHost.isResolved = true;
return instanceHost.instance;
}
if (isNil(inject) && isInContext) {
// 팩토리 프로바이더가 아니라면,
// new로 인스턴스를 생성해서 저장!!
instanceHost.instance = wrapper.forwardRef
? Object.assign(
instanceHost.instance,
new (metatype as Type<any>)(...instances),
)
// 인스턴스를 만들어서 저장합니다.
: new (metatype as Type<any>)(...instances);
} else if (isInContext) {
const factoryReturnValue = (targetMetatype.metatype as any as Function)(
...instances,
);
instanceHost.instance = await factoryReturnValue;
}
// 해당 의존성이 처리되었다고 명시
instanceHost.isResolved = true;
// 인스턴스 반환
return instanceHost.instance;
}
- 위 콜백에서 제시하였던 AppController를 기준으로 보았을 때 팩토리 프로바이더가 아니기 때문에 아래 분기를 타게 됩니다.
if (isNil(inject) && isInContext) {
// 팩토리 프로바이더가 아니라면,
// new로 인스턴스를 생성해서 저장!!
instanceHost.instance = wrapper.forwardRef
? Object.assign(
instanceHost.instance,
new (metatype as Type<any>)(...instances),
)
// 인스턴스를 만들어서 저장합니다.
: new (metatype as Type<any>)(...instances);
}
- 이때 새로 instances(생성자에 주입한 요소들)과 함께 인스턴스화하여 instanceHost.instance에 저장하는 것을 알 수 있습니다.
- 이후 이 instance를 반환하고 메서드를 종료합니다.
4. 다시 컨테이너 initialize로
await ExceptionsZone.asyncRun(
async () => {
// 의존성 스캐너(dependenciesScanner)를 사용해 전달된 모듈의 모든 의존성을 스캔합니다.
await dependenciesScanner.scan(module);
// 인스턴스 로더(instanceLoader)를 사용해 모든 의존성의 인스턴스를 생성합니다.
await instanceLoader.createInstancesOfDependencies();
// 의존성 스캐너(dependenciesScanner)를 사용해 애플리케이션의 제공자(Providers)를 적용합니다.
dependenciesScanner.applyApplicationProviders();
},
// asyncRun 블록을 종료하고, 예외가 발생할 경우 teardown을 처리하고 로그 자동 플러시 여부를 설정합니다.
teardown,
this.autoFlushLogs
);
- 이제 다시 initialize로 돌아왔습니다. 위의 과정들을 통해 createInstancesOfDependencies를 통해 인스턴스들을 생성하고, 저장하였습니다.
- 이제 모듈들이 모두 NestContainer에 등록되고 요소들을 포함하여 초기화가 되었고, provider / controller / injectable 모두 생성자까지 포함되어서 인스턴스화 되었습니다.
- 이렇게 저장해둔 인스턴스들과 정보들을 바탕으로 IoC 컨테이너가 필요에 맞게 골라서 사용합니다.
5. 마치며
- NestJS가 어떻게 초기화되고 컨테이너가 DI를 관리하는지에 대해 알아보았습니다.
- 생각보다 코드를 모두 까보는게 쉽지만은 않은 것 같습니다. 코드량이 많고 이해하지 못한 부분들이 아직 너무 많습니다.
- 두번 세번 더 살펴보면서 더 깊고 확실하게 계속 이해해보려고 합니다.
- 프레임워크를 사용할 때 비즈니스 로직을 잘 짜는것도 중요하지만 어떻게 프레임워크가 작동하는지 알아야 더 좋은 구조를 짤 수 있고 더 나은 프로덕트를 만들 수 있다고 생각해서 좋은 학습이였다고 생각합니다. 이외에도 시간이 있을 때마다 다른 core 기능들에 대해 더 깊게 파보려고 합니다!
