NestJS middleware는 어떻게 작동할까 (3)
들어가며
- 목표는 이전 글과 동일합니다.
- nestJS 미들웨어의 코드가 어떻게 구성되어 있는지 확인해본다.
- 직접 사용해본다.
- 사용해보며 문제가 있거나 / 이슈가 열려있다면 기여해본다.
- 이번 글에서는 이전 글에서 분석한 MiddlewareBuilder 를 사용하는 부분부터알아보겠습니다.
돌아보며
- middlewareModule의 loadConfiguration 메소드에서 MiddlewareBuilder를 생성, 구성을 하고 사용하는 부분이 있었습니다.
public async loadConfiguration(
middlewareContainer: MiddlewareContainer,
moduleRef: Module, // 모듈에 대한 참조, configure함수 호출
moduleKey: string,
) {
// 현재 모듈에 대한 참조에서 인스턴스를 가져옴
const { instance } = moduleRef;
// 모듈에 configure 메서드가 없을 경우 미들웨어를 구성하지 않고 종료
if (!instance.configure) {
return;
}
// 미들웨어를 구성하고 빌드하는데에 사용되는 MiddlewareBuilder 생성
// 모듈마다 MiddlewareBuilder를 생성
const middlewareBuilder = new MiddlewareBuilder(
this.routesMapper,
this.httpAdapter,
this.routeInfoPathExtractor,
);
try {
// 모듈의 configure 메서드를 호출하여 미들웨어 빌더를 전달
// 모듈에서 미들웨어 구성을 수행
await instance.configure(middlewareBuilder);
} catch (err) {
if (!this.appOptions.preview) {
throw err;
}
const warningMessage =
`Warning! "${moduleRef.name}" module exposes a "configure" method that throws an exception in the preview mode` +
` (possibly due to missing dependencies). Note: you can ignore this message, just be aware that some of those conditional middlewares will not be reflected in your graph.`;
this.logger.warn(warningMessage);
}
if (!(middlewareBuilder instanceof MiddlewareBuilder)) {
return;
}
// 미들웨어 빌더를 사용해서 config 빌드
const config = middlewareBuilder.build();
// 빌드된 config을 미들웨어 컨테이너에 삽입
middlewareContainer.insertConfig(config, moduleKey);
}
- 이전글에서 알아 보았듯이, middlewareBuilder.build() 의 경우 [...this.middlewareCollection] 즉 middlewareCollection의 배열을 반환합니다.
- 이렇게 반환된 config을 사용하여 처리하는 부분부터 살펴보겠습니다.
1. loadConfiguration
middlewareContainer.insertConfig(config, moduleKey);
- 빌드된 config을 미들웨어 컨테이너에 삽입합니다.
insertConfig
public insertConfig(
configList: MiddlewareConfiguration[],
moduleKey: string,
) {
const middleware = this.getMiddlewareCollection(moduleKey);
const targetConfig = this.getTargetConfig(moduleKey);
const configurations = configList || [];
const insertMiddleware = <T extends Type<unknown>>(metatype: T) => {
const token = metatype;
middleware.set(
token,
new InstanceWrapper({
scope: getClassScope(metatype),
durable: isDurable(metatype),
name: token?.name ?? token,
metatype,
token,
}),
);
};
configurations.forEach(config => {
[].concat(config.middleware).map(insertMiddleware);
targetConfig.add(config);
});
}
- insertConfig 메서드는 미들웨어 설정 목록(config List config)과 모듈 키(moduleKey)를 인자로 받습니다.
const middleware = this.getMiddlewareCollection(moduleKey);
- getMiddlewareCollection 메서드를 호출하여 해당 모듈 키에 대한 미들웨어 컬렉션을 가져옵니다.
getMiddlewareCollection
public getMiddlewareCollection(
moduleKey: string,
): Map<InjectionToken, InstanceWrapper> {
if (!this.middleware.has(moduleKey)) {
const moduleRef = this.container.getModuleByKey(moduleKey);
this.middleware.set(moduleKey, moduleRef.middlewares);
}
return this.middleware.get(moduleKey);
}
- 주어진 moduleKey에 대한 미들웨어 컬렉션을 반환합니다.
private readonly middleware = new Map<
string,
Map<InjectionToken, InstanceWrapper>
>();
- 해당 Map에서 가져옵니다.
- 모듈 키에 대한 미들웨어 컬렉션이 없으면 새로 가져와서 설정합니다.
const targetConfig = this.getTargetConfig(moduleKey);
- getTargetConfig 메서드를 호출하여 해당 모듈 키에 대한 타겟 설정을 가져옵니다.
getTargetConfig
private getTargetConfig(moduleName: string) {
if (!this.configurationSets.has(moduleName)) {
this.configurationSets.set(
moduleName,
new Set<MiddlewareConfiguration>(),
);
}
return this.configurationSets.get(moduleName);
}
- 주어진 moduleName에 대한 타겟 설정을 반환합니다.
private readonly configurationSets = new Map<
string,
Set<MiddlewareConfiguration>
>();
- 해당 Map에서 가져옵니다.
- 타겟 설정이 없으면 새로 생성하여 설정합니다.
const configurations = configList || [];
const insertMiddleware = <T extends Type<unknown>>(metatype: T) => {
const token = metatype;
middleware.set(
token,
new InstanceWrapper({
scope: getClassScope(metatype),
durable: isDurable(metatype),
name: token?.name ?? token,
metatype,
token,
}),
);
};
- config List config가 제공되지 않으면 빈 배열로 초기화합니다.
- insertMiddleware 함수는 미들웨어 클래스를 받아서 InstanceWrapper로 감싸고 middleware 맵에 추가합니다.
- InstanceWrapper는 클래스의 인스턴스를 관리하는 데 사용됩니다.
- scope, durable, name, metatype, token 속성을 설정합니다.
configurations.forEach(config => {
[].concat(config.middleware).map(insertMiddleware);
targetConfig.add(config);
});
- configurations 배열을 돌며 다음을 수행합니다.
- **[].concat(config.middleware)**를 사용하여 middleware를 배열로 변환합니다.
- **map(insertMiddleware)**를 호출하여 각 미들웨어를 insertMiddleware 함수로 처리합니다.
- insertMiddleware 함수는 미들웨어 클래스를 받아서 InstanceWrapper로 감싸고 middleware 맵에 추가합니다. (middleware 맵에 추가)
- **targetConfig.add(config)**를 호출하여 targetConfig 세트에 설정을 추가합니다. (targetConfig 맵에 추가)
- 해당 과정을 통해 전체 미들웨어를 관리하는 middlewareContainer의 필드인 middleware set에 미들웨어에 대한 인스턴스를 추가하고, config urationSets config set에 미들웨어의 설정에 대한 값들을 추가하여 저장합니다.
- 이러한 과정을 통해서 각 모듈의 미들웨어들이 초기화되고 저장되는 것 입니다.
예시
const configList = [
{
middleware: [SomeMiddleware],
forRoutes: ['/user'],
},
{
middleware: [AnotherMiddleware],
forRoutes: ['/admin'],
}
];
const moduleKey = 'SomeModule';
middlewareContainer.insertConfig(configList, moduleKey);
- moduleKey에 대한 미들웨어 컬렉션을 가져옵니다.
- moduleKey에 대한 타겟 설정을 가져옵니다.
- 각 설정을 순회하며, 각 설정의 미들웨어를 insertMiddleware 함수로 처리하여 미들웨어 컬렉션에 추가하고, 설정을 타겟 설정에 추가합니다.
+middleware, configurationSets이 뭔데?
middleware
private readonly middleware = new Map<
string,
Map<InjectionToken, InstanceWrapper>
>();
- 역할
- 이 맵은 모듈 키(string)를 기준으로 각 모듈의 미들웨어 인스턴스를 관리합니다.
- 각 모듈 키에 대해 또 다른 맵을 저장하며, 이 내부 맵은 미들웨어의 InjectionToken과 해당 미들웨어의 InstanceWrapper를 매핑합니다.
- 모듈안에 여러가지 미들웨어가 있을 수 있기 때문입니다.
- 구조
- 최상위 키: 모듈 키(string), 예를 들어 'AppModule', 'UserModule' 등입니다.
- 내부 맵: 미들웨어의 InjectionToken을 키로 하고, InstanceWrapper를 값으로 가지는 맵입니다.
- e.g
{
'AppModule': {
SomeMiddlewareToken: InstanceWrapper { ... },
AnotherMiddlewareToken: InstanceWrapper { ... },
},
'UserModule': {
UserMiddlewareToken: InstanceWrapper { ... },
},
}
configurationSets
private readonly configurationSets = new Map<
string,
Set<MiddlewareConfiguration>
>();
- 역할
- 이 맵은 모듈 키(string)를 기준으로 각 모듈의 미들웨어 설정(MiddlewareConfiguration)을 관리합니다.
- 각 모듈 키에 대해 Set을 저장하며, 이 집합은 해당 모듈의 모든 미들웨어 설정을 포함합니다.
- 구조
- 최상위 키: 모듈 키(string), 예를 들어 'AppModule', 'UserModule' 등 입니다.
- 값: 해당 모듈의 미들웨어 설정입니다.
- e.g
{
'AppModule': Set<MiddlewareConfiguration> { ... },
'UserModule': Set<MiddlewareConfiguration> { ... },
}
2. resolveMiddleware
public async resolveMiddleware(
middlewareContainer: MiddlewareContainer,
modules: Map<string, Module>,
) {
const moduleEntries = [...modules.entries()];
const loadMiddlewareConfiguration = async ([moduleName, moduleRef]: [
string,
Module,
]) => {
// 해당 메소드를 살펴보았었습니다.
await this.loadConfiguration(middlewareContainer, moduleRef, moduleName);
await this.resolver.resolveInstances(moduleRef, moduleName);
};
await Promise.all(moduleEntries.map(loadMiddlewareConfiguration));
}
- 위에서 알아본 loadConfiguration 메소드는 각 모듈에서 middlewareBuilder를 통해서 설정된 구성을 초기화하고 해당 정보들을 middlewareContainer에 저장하는 역할을 하였습니다.
- resolveMiddleware 통해서 모든 모듈에 대해서 해당 작업을 수행하는 것을 확인할 수 있습니다.
- 이제 살펴보지 않은 그 아래에 있는 메소드인this.resolver.resolveInstances 에 대해서 알아보겠습니다.
resolveInstances
await this.resolver.resolveInstances(moduleRef, moduleName);
- 미들웨어 인스턴스를 생성하고 필요한 종속성을 주입하여 준비 상태로 만듭니다.
- 우선 미들웨어 클래스로부터 인스턴스를 생성하고, 해당 인스턴스에 필요한 의존성을 주입합니다. 그리고 미들웨어 인스턴스를 초기화합니다.
- 이를 통해 미들웨어가 실제로 사용될 수 있는 상태되면, 미들웨어는 요청을 처리할 준비가 완료됩니다.
MiddlewareResolver
import { InjectionToken } from '@nestjs/common';
import { Injector } from '../injector/injector';
import { InstanceWrapper } from '../injector/instance-wrapper';
import { Module } from '../injector/module';
import { MiddlewareContainer } from './container';
export class MiddlewareResolver {
constructor(
private readonly middlewareContainer: MiddlewareContainer,
private readonly injector: Injector,
) {}
public async resolveInstances(moduleRef: Module, moduleName: string) {
const middlewareMap =
this.middlewareContainer.getMiddlewareCollection(moduleName);
const resolveInstance = async (wrapper: InstanceWrapper) =>
this.resolveMiddlewareInstance(wrapper, middlewareMap, moduleRef);
await Promise.all([...middlewareMap.values()].map(resolveInstance));
}
private async resolveMiddlewareInstance(
wrapper: InstanceWrapper,
middlewareMap: Map<InjectionToken, InstanceWrapper>,
moduleRef: Module,
) {
await this.injector.loadMiddleware(wrapper, middlewareMap, moduleRef);
}
}
resolveInstances
public async resolveInstances(moduleRef: Module, moduleName: string) {
const middlewareMap =
this.middlewareContainer.getMiddlewareCollection(moduleName);
const resolveInstance = async (wrapper: InstanceWrapper) =>
this.resolveMiddlewareInstance(wrapper, middlewareMap, moduleRef);
await Promise.all([...middlewareMap.values()].map(resolveInstance));
}
- **getMiddlewareCollection(moduleName)**를 호출하여 해당 모듈의 미들웨어 컬렉션(InstanceWrapper)을 가져옵니다.
- **Promise.all([...middlewareMap.values()].map(resolveInstance))**를 사용하여 모든 미들웨어 인스턴스를 비동기적으로 해결합니다.
- resolveInstance 를 통해 각 InstanceWrapper를 처리합니다.
resolveMiddlewareInstance
private async resolveMiddlewareInstance(
wrapper: InstanceWrapper,
middlewareMap: Map<InjectionToken, InstanceWrapper>,
moduleRef: Module,
) {
await this.injector.loadMiddleware(wrapper, middlewareMap, moduleRef);
}
- **this.injector.loadMiddleware(wrapper, middlewareMap, moduleRef)**를 호출하여 미들웨어 인스턴스를 생성하고 필요한 종속성을 주입합니다.
InstanceWrapper는 인스턴스가 아니다
- 처음에는 InstanceWrapper 가 인스턴스인데, loadMiddleware로 인스턴스를 생성한다는 것이 이해가 가질 않았습니다.
- 결론적으로 InstanceWrapper가 실제 인스턴스는 아닙니다. InstanceWrapper는 클래스 타입을 포함하고 있습니다. (여기서는 미들웨어 클래스와 해당 클레스의 메타데이터를 래핑합니다.)
- InstanceWrapper는 단순히 클래스 타입과 그 인스턴스를 포함하는 래퍼입니다.
- loadMiddleware 메서드는 InstanceWrapper를 사용하여 실제 인스턴스를 생성하고 필요한 종속성을 주입합니다. 이 과정을 통해 미들웨어 인스턴스가 생성되고 초기화됩니다.
loadMiddleware
public async loadMiddleware(
wrapper: InstanceWrapper,
collection: Map<InjectionToken, InstanceWrapper>,
moduleRef: Module,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
) {
const { metatype, token } = wrapper;
const targetWrapper = collection.get(token);
if (!isUndefined(targetWrapper.instance)) {
return;
}
targetWrapper.instance = Object.create(metatype.prototype);
await this.loadInstance(
wrapper,
collection,
moduleRef,
contextId,
inquirer || wrapper,
);
}
-
파라미터
- wrapper: 미들웨어 인스턴스를 감싸는 InstanceWrapper
- collection: 미들웨어 인스턴스를 저장하는 Map
- moduleRef: 현재 모듈 참조
- contextId: 컨텍스트 ID (기본값은 STATIC_CONTEXT)
- inquirer: 인스턴스를 요청하는 다른 InstanceWrapper (옵션)
-
타겟 래퍼 가져오기
const targetWrapper = collection.get(token);
-
collection에서 token에 해당하는 targetWrapper를 가져옵니다.
-
인스턴스 존재 확인
if (!isUndefined(targetWrapper.instance)) { return; }
-
targetWrapper에 인스턴스가 이미 존재하면 더 이상 작업을 진행하지 않습니다.
-
인스턴스 생성
targetWrapper.instance = Object.create(metatype.prototype);
-
미들웨어 클래스의 프로토타입을 기반으로 새로운 인스턴스를 생성합니다.
-
인스턴스 로드
await this.loadInstance(wrapper, collection, moduleRef, contextId, inquirer || wrapper);
- loadInstance 메서드를 호출하여 인스턴스를 로드하고 종속성을 주입합니다.
loadInstance
public async loadInstance<T>(
wrapper: InstanceWrapper<T>,
collection: Map<InjectionToken, InstanceWrapper>,
moduleRef: Module,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
) {
const inquirerId = this.getInquirerId(inquirer);
const instanceHost = wrapper.getInstanceByContextId(
this.getContextId(contextId, wrapper),
inquirerId,
);
if (instanceHost.isPending) {
const settlementSignal = wrapper.settlementSignal;
if (inquirer && settlementSignal?.isCycle(inquirer.id)) {
throw new CircularDependencyException(`"${wrapper.name}"`);
}
return instanceHost.donePromise.then((err?: unknown) => {
if (err) {
throw err;
}
});
}
const settlementSignal = this.applySettlementSignal(instanceHost, wrapper);
const token = wrapper.token || wrapper.name;
const { inject } = wrapper;
const targetWrapper = collection.get(token);
if (isUndefined(targetWrapper)) {
throw new RuntimeException();
}
if (instanceHost.isResolved) {
return settlementSignal.complete();
}
try {
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 = 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;
}
}
- 인스턴스 가져오기
const instanceHost = wrapper.getInstanceByContextId(...);
-
주어진 컨텍스트 ID와 inquirerId를 사용하여 인스턴스를 가져옵니다.
-
대기 중인 인스턴스 확인
if (instanceHost.isPending) { ... }
-
인스턴스가 대기 중인 상태라면, 대기 완료를 기다립니다.
-
정착 신호 적용
const settlementSignal = this.applySettlementSignal(instanceHost, wrapper);
-
정착 신호를 적용하여 인스턴스 로드가 완료되었는지 확인합니다.
-
토큰 및 타겟 래퍼 가져오기
const token = wrapper.token || wrapper.name;
const targetWrapper = collection.get(token);
-
토큰을 사용하여 타겟 래퍼를 가져옵니다.
-
타겟 래퍼 확인:
if (isUndefined(targetWrapper)) { throw new RuntimeException(); }
-
타겟 래퍼가 존재하지 않으면 예외를 발생시킵니다.
-
인스턴스 생성 및 종속성 주입:
await this.resolveConstructorParams<T>(...);
const instance = await this.instantiateClass(...);
this.applyProperties(instance, properties);
- resolveConstructorParams 메서드를 사용하여 생성자 파라미터를 해결하고 인스턴스를 생성합니다.
- instantiateClass 메서드를 사용하여 인스턴스를 생성합니다
- applyProperties 메서드를 사용하여 속성을 주입합니다.
- 해당 과정을 통해서 진짜 (미들웨어) 인스턴스가 만들어집니다!
- 결론적으로 resolveMiddleware 메소드는 모든 모듈을 순회하며 구성을 초기화하고, 해당 정보들을 실제 인스턴스로 만드는 과정입니다.
이어가며
- 모든 모듈에 대해 미들웨어 구성을 로드 / 처리 / 컨테이너에 삽입하는 부분만 살펴보았는데요, 이번에도 글이 너무 길어지는 관계로 이를 적용하는 부분에 대해서는 다음 글에 이어서 작성하겠습니다.
