NestJS middleware는 어떻게 작동할까 (4)
들어가며
- 목표는 이전 글과 동일합니다.
- nestJS 미들웨어의 코드가 어떻게 구성되어 있는지 확인해본다.
- 직접 사용해본다.
- 사용해보며 문제가 있거나 / 이슈가 열려있다면 기여해본다.
- 이번 글에서는 이전 글에서 분석한 resloveMiddleware 이후에, 인스턴스화 된 미들웨어를 실제로 등록하는 부분부터 알아보겠습니다.
- 마지막 글입니다!
1. nestApplication, registerMiddleware
- resolveMiddleware 을 이제 알아보았으니, 해당 메소드를 호출하던 호출부로 다시 돌아가볼까요?
register
public async register(
middlewareContainer: MiddlewareContainer,
container: NestContainer,
config: ApplicationConfig,
injector: Injector,
httpAdapter: HttpServer,
graphInspector: GraphInspector,
options: TAppOptions,
) {
...
await this.resolveMiddleware(middlewareContainer, modules);
}
- MiddlewareModule 의 register 메소드는 다음과 같이 resolveMiddleware를 마지막으로 호출하고 끝냅니다.
- 이제 더 올라가서, register를 호출하는 호출부로 돌아가보겠습니다.
nestApplication
// packages/core/nest-application.ts
export class NestApplication
extends NestApplicationContext<NestApplicationOptions>
implements INestApplication
{
...
public async registerModules() {
this.registerWsModule();
...
// 해당 부분!
await this.middlewareModule.register(
this.middlewareContainer,
this.container,
this.config,
this.injector,
this.httpAdapter,
this.graphInspector,
this.appOptions,
);
}
public async init(): Promise<this> {
if (this.isInitialized) {
return this;
}
this.applyOptions();
await this.httpAdapter?.init();
const useBodyParser =
this.appOptions && this.appOptions.bodyParser !== false;
useBodyParser && this.registerParserMiddleware();
await this.registerModules();
await this.registerRouter();
await this.callInitHook();
await this.registerRouterHooks();
await this.callBootstrapHook();
this.isInitialized = true;
this.logger.log(MESSAGES.APPLICATION_READY);
return this;
}
public async registerRouter() {
await this.registerMiddleware(this.httpAdapter);
const prefix = this.config.getGlobalPrefix();
const basePath = addLeadingSlash(prefix);
this.routesResolver.resolve(this.httpAdapter, basePath);
}
public async listen(port: number | string): Promise<any>;
public async listen(port: number | string, hostname: string): Promise<any>;
public async listen(port: number | string, ...args: any[]): Promise<any> {
this.assertNotInPreviewMode('listen');
!this.isInitialized && (await this.init());
...
}
private async registerMiddleware(instance: any) {
await this.middlewareModule.registerMiddleware(
this.middlewareContainer,
instance,
);
}
}
- 위에서 살펴본 middlewareModule.register 메소드를 registerModules에서 사용하는 것을 볼 수 있습니다. (모듈을 만들때, 미들웨어 모듈들도 생성됩니다)
- 이제 이 생성된 미들웨어 모듈들을 활용해서 registerMiddleware 를 하는 부분을 살펴보겠습니다.
- registerRouter메소드인데요, 해당 메소드에서 registerMiddleware 를 호출하는 부분을 볼 수 있습니다.
registerMiddleware
public async registerMiddleware(
middlewareContainer: MiddlewareContainer,
applicationRef: any,
) {
const configs = middlewareContainer.getConfigurations();
const registerAllConfigs = async (
moduleKey: string,
middlewareConfig: MiddlewareConfiguration[],
) => {
for (const config of middlewareConfig) {
await this.registerMiddlewareConfig(
middlewareContainer,
config,
moduleKey,
applicationRef,
);
}
};
const entriesSortedByDistance = [...configs.entries()].sort(
([moduleA], [moduleB]) => {
return (
this.container.getModuleByKey(moduleA).distance -
this.container.getModuleByKey(moduleB).distance
);
},
);
for (const [moduleRef, moduleConfigurations] of entriesSortedByDistance) {
await registerAllConfigs(moduleRef, [...moduleConfigurations]);
}
}
- MiddlewareContainer에서 가져온 미들웨어 설정들을 순회하며, 각각의 설정을 애플리케이션의 특정 경로에 등록합니다.
registerAllConfigs
const registerAllConfigs = async (
moduleKey: string,
middlewareConfig: MiddlewareConfiguration[],
) => {
for (const config of middlewareConfig) {
await this.registerMiddlewareConfig(
middlewareContainer,
config,
moduleKey,
applicationRef,
);
}
};
- 각 모듈의 미들웨어 설정들을 순회하며 registerMiddlewareConfig 메서드를 통해 개별 설정을 등록합니다.
registerMiddlewareConfig
public async registerMiddlewareConfig(
middlewareContainer: MiddlewareContainer,
config: MiddlewareConfiguration,
moduleKey: string,
applicationRef: any,
) {
const { forRoutes } = config;
for (const routeInfo of forRoutes) {
await this.registerRouteMiddleware(
middlewareContainer,
routeInfo as RouteInfo,
config,
moduleKey,
applicationRef,
);
}
}
- config에서 경로 기반 미들웨어 설정 (forRoutes)을 추출합니다.
- 각 경로 설정 (routeInfo)에 대해 registerRouteMiddleware 메서드를 호출해 미들웨어를 등록합니다.
registerRouteMiddleware
public async registerRouteMiddleware(
middlewareContainer: MiddlewareContainer,
routeInfo: RouteInfo,
config: MiddlewareConfiguration,
moduleKey: string,
applicationRef: any,
) {
const middlewareCollection = [].concat(config.middleware);
const moduleRef = this.container.getModuleByKey(moduleKey);
for (const metatype of middlewareCollection) {
const collection = middlewareContainer.getMiddlewareCollection(moduleKey);
const instanceWrapper = collection.get(metatype);
if (isUndefined(instanceWrapper)) {
throw new RuntimeException();
}
if (instanceWrapper.isTransient) {
return;
}
this.graphInspector.insertClassNode(
moduleRef,
instanceWrapper,
'middleware',
);
const middlewareDefinition: Entrypoint<MiddlewareEntrypointMetadata> = {
type: 'middleware',
methodName: 'use',
className: instanceWrapper.name,
classNodeId: instanceWrapper.id,
metadata: {
key: routeInfo.path,
path: routeInfo.path,
requestMethod:
(RequestMethod[routeInfo.method] as keyof typeof RequestMethod) ??
'ALL',
version: routeInfo.version,
},
};
this.graphInspector.insertEntrypointDefinition(
middlewareDefinition,
instanceWrapper.id,
);
await this.bindHandler(
instanceWrapper,
applicationRef,
routeInfo,
moduleRef,
collection,
);
}
}
const middlewareCollection = [].concat(config.middleware);
const moduleRef = this.container.getModuleByKey(moduleKey);
- config.middleware를 배열로 변환하여 middlewareCollection에 저장합니다.
- moduleKey에 해당하는 모듈 참조를 가져옵니다.
for (const metatype of middlewareCollection) {
const collection = middlewareContainer.getMiddlewareCollection(moduleKey);
const instanceWrapper = collection.get(metatype);
if (isUndefined(instanceWrapper)) {
throw new RuntimeException();
}
if (instanceWrapper.isTransient) {
return;
}
- middlewareCollection을 순회하며, 각 미들웨어에 대한 instanceWrapper를 가져옵니다.
- instanceWrapper가 정의되지 않았거나 일시적인(transient) 경우 예외를 처리합니다.
this.graphInspector.insertClassNode(
moduleRef,
instanceWrapper,
'middleware',
);
const middlewareDefinition: Entrypoint<MiddlewareEntrypointMetadata> = {
type: 'middleware',
methodName: 'use',
className: instanceWrapper.name,
classNodeId: instanceWrapper.id,
metadata: {
key: routeInfo.path,
path: routeInfo.path,
requestMethod:
(RequestMethod[routeInfo.method] as keyof typeof RequestMethod) ??
'ALL',
version: routeInfo.version,
},
};
this.graphInspector.insertEntrypointDefinition(
middlewareDefinition,
instanceWrapper.id,
);
await this.bindHandler(
instanceWrapper,
applicationRef,
routeInfo,
moduleRef,
collection,
);
- 그래프 검사기를 통해 미들웨어 클래스 노드를 삽입합니다.
- 미들웨어 엔트리포인트 정의를 삽입합니다.
- bindHandler 메서드를 호출해 미들웨어 핸들러를 바인딩합니다.
- middlewareDefinition 메타데이터의 경우 graphInspector(그래프 검사기)를 위해 설정하고 적용하는 부분이며, 실제 미들웨어 적용은 bindHandler 메서드를 통해 이루어집니다.
- MiddlewareEntrypointMetadata와 Entrypoint는 그래프 검사 및 시각화 목적으로 사용됩니다. 실제 미들웨어 적용에는 사용되지 않습니다!
bindHandler
private async bindHandler(
wrapper: InstanceWrapper<NestMiddleware>,
applicationRef: HttpServer,
routeInfo: RouteInfo,
moduleRef: Module,
collection: Map<InjectionToken, InstanceWrapper>,
) {
const { instance, metatype } = wrapper;
if (isUndefined(instance?.use)) {
throw new InvalidMiddlewareException(metatype.name);
}
const isStatic = wrapper.isDependencyTreeStatic();
if (isStatic) {
const proxy = await this.createProxy(instance);
return this.registerHandler(applicationRef, routeInfo, proxy);
}
const isTreeDurable = wrapper.isDependencyTreeDurable();
await this.registerHandler(
applicationRef,
routeInfo,
async <TRequest, TResponse>(
req: TRequest,
res: TResponse,
next: () => void,
) => {
try {
const contextId = this.getContextId(req, isTreeDurable);
const contextInstance = await this.injector.loadPerContext(
instance,
moduleRef,
collection,
contextId,
);
const proxy = await this.createProxy<TRequest, TResponse>(
contextInstance,
contextId,
);
return proxy(req, res, next);
} catch (err) {
let exceptionsHandler = this.exceptionFiltersCache.get(instance.use);
if (!exceptionsHandler) {
exceptionsHandler = this.routerExceptionFilter.create(
instance,
instance.use,
undefined,
);
this.exceptionFiltersCache.set(instance.use, exceptionsHandler);
}
const host = new ExecutionContextHost([req, res, next]);
exceptionsHandler.next(err, host);
}
},
);
}
- bindHandler 메서드를 통해 미들웨어 인스턴스를 실제 경로에 바인딩합니다.
- 미들웨어 인스턴스를 HTTP 서버에 핸들러로 등록하여 요청을 처리합니다.
const { instance, metatype } = wrapper;
- wrapper에서 미들웨어 인스턴스와 클래스 타입을 추출합니다.
const isStatic = wrapper.isDependencyTreeStatic();
if (isStatic) {
const proxy = await this.createProxy(instance);
return this.registerHandler(applicationRef, routeInfo, proxy);
}
- 인스턴스가 정적 종속성 트리를 가지고 있는지 확인합니다.
- 정적 종속성 트리라면 바로 프록시를 생성하고 핸들러를 등록합니다.
await this.registerHandler(applicationRef, routeInfo, proxy)
- 핸들러를 등록합니다.
- 정적이지 않은 경우, 컨텍스트 ID를 가져오고, 종속성을 주입한 후 프록시를 생성하여 핸들러를 등록합니다.
await this.registerHandler(
applicationRef,
routeInfo,
async <TRequest, TResponse>(
req: TRequest,
res: TResponse,
next: () => void,
) => {
try {
const contextId = this.getContextId(req, isTreeDurable);
const contextInstance = await this.injector.loadPerContext(
instance,
moduleRef,
collection,
contextId,
);
const proxy = await this.createProxy<TRequest, TResponse>(
contextInstance,
contextId,
);
return proxy(req, res, next);
} catch (err) {
let exceptionsHandler = this.exceptionFiltersCache.get(instance.use);
if (!exceptionsHandler) {
exceptionsHandler = this.routerExceptionFilter.create(
instance,
instance.use,
undefined,
);
this.exceptionFiltersCache.set(instance.use, exceptionsHandler);
}
const host = new ExecutionContextHost([req, res, next]);
exceptionsHandler.next(err, host);
}
},
);
- 동적 핸들러를 등록합니다.
const contextId = this.getContextId(req, isTreeDurable);
- 요청과 내구성을 기반으로 컨텍스트 ID를 생성합니다.
const contextInstance = await this.injector.loadPerContext(
instance,
moduleRef,
collection,
contextId,
);
- injector를 사용하여 컨텍스트 인스턴스를 로드합니다.
const proxy = await this.createProxy<TRequest, TResponse>(
contextInstance,
contextId,
);
- 컨텍스트 인스턴스를 기반으로 프록시를 생성합니다.
return proxy(req, res, next);
-
요청, 응답, next 함수를 프록시를 통해 실행합니다.
-
해당 코드는 registerHandler 메서드의 proxy 매개변수로 전달됩니다.
// 해당 함수는 아래에서 사용된다 (middlewareFunction)
async <TRequest, TResponse>(
req: TRequest,
res: TResponse,
next: () => void,
) => {
try {
const contextId = this.getContextId(req, isTreeDurable);
const contextInstance = await this.injector.loadPerContext(
instance,
moduleRef,
collection,
contextId,
);
const proxy = await this.createProxy<TRequest, TResponse>(
contextInstance,
contextId,
);
return proxy(req, res, next);
} catch (err) {
let exceptionsHandler = this.exceptionFiltersCache.get(instance.use);
if (!exceptionsHandler) {
exceptionsHandler = this.routerExceptionFilter.create(
instance,
instance.use,
undefined,
);
this.exceptionFiltersCache.set(instance.use, exceptionsHandler);
}
const host = new ExecutionContextHost([req, res, next]);
exceptionsHandler.next(err, host);
}
},
// 위에서 전달된 proxy 함수가 활용됩니다.
private async registerHandler(
applicationRef: HttpServer,
routeInfo: RouteInfo,
proxy: <TRequest, TResponse>(
req: TRequest,
res: TResponse,
next: () => void,
) => void,
) {
const { method } = routeInfo;
const paths = this.routeInfoPathExtractor.extractPathsFrom(routeInfo);
const isMethodAll = isRequestMethodAll(method);
const requestMethod = RequestMethod[method];
const router = await applicationRef.createMiddlewareFactory(method);
const middlewareFunction = isMethodAll
? proxy // 활용
: <TRequest, TResponse>(
req: TRequest,
res: TResponse,
next: () => void,
) => {
if (applicationRef.getRequestMethod(req) === requestMethod) {
return proxy(req, res, next);
}
return next();
};
const pathsToApplyMiddleware = [];
paths.some(path => path.match(/^\/?$/))
? pathsToApplyMiddleware.push('/')
: pathsToApplyMiddleware.push(...paths);
pathsToApplyMiddleware.forEach(path => router(path, middlewareFunction));
}
- 전달받은 proxy 함수를 활용해서 register합니다.
const { method } = routeInfo;
const paths = this.routeInfoPathExtractor.extractPathsFrom(routeInfo);
const isMethodAll = isRequestMethodAll(method);
const requestMethod = RequestMethod[method];
- 경로 및 메서드를 추출합니다.
- isMethodAll 변수는 해당 메서드가 모든 메서드를 의미하는지 여부를 나타냅니다.
const router = await applicationRef.createMiddlewareFactory(method);
const middlewareFunction = isMethodAll
? proxy
: <TRequest, TResponse>(
req: TRequest,
res: TResponse,
next: () => void,
) => {
if (applicationRef.getRequestMethod(req) === requestMethod) {
return proxy(req, res, next);
}
return next();
};
- 미들웨어 팩토리를 생성합니다.
- 미들웨어 함수를 정의합니다.
const pathsToApplyMiddleware = [];
paths.some(path => path.match(/^\/?$/))
? pathsToApplyMiddleware.push('/')
: pathsToApplyMiddleware.push(...paths);
pathsToApplyMiddleware.forEach(path => router(path, middlewareFunction));
- 경로에 미들웨어를 적용합니다.
- 우선 paths 배열에서 루트 경로 ('/' 또는 빈 문자열 '')를 확인합니다.
- paths.some(path => path.match(/^/?$/))는 경로 배열 중 하나라도 루트 경로 ('/' 또는 '')와 일치하는지 확인합니다.
- path.match(/^/?$/) 정규식은 루트 경로('/' 또는 빈 문자열 '')를 찾습니다.
- 조건에 따라 경로를 추가합니다:
- 일단 루트 경로가 있을 경우, pathsToApplyMiddleware.push('/')
- **'/'**를 pathsToApplyMiddleware 배열에 추가합니다.
- 만약 루트 경로가 없을 경우, pathsToApplyMiddleware.push(...paths)
- paths 배열의 모든 경로를 pathsToApplyMiddleware 배열에 추가합니다.
- 일단 루트 경로가 있을 경우, pathsToApplyMiddleware.push('/')
- 마지막으로 경로에 대해 미들웨어를 바인딩합니다.
- pathsToApplyMiddleware 배열의 각 경로에 대해 미들웨어 함수를 바인딩합니다.
- **router(path, middlewareFunction)**를 호출하여 각 경로에 middlewareFunction을 적용합니다.
+proxy?
- proxy 함수는 미들웨어가 요청을 처리하는 동안 요청을 해당 미들웨어로 프록시하고, 미들웨어 처리가 끝나면 **next()**를 호출하여 다음 미들웨어나 라우트 핸들러로 요청을 전달하는데요, 이를 통해 미들웨어가 각 요청을 올바르게 처리하고, 종속성을 주입하며, 예외를 처리할 수 있습니다.
- 미들웨어는 특정 경로와 HTTP 메서드에 대해 요청을 처리할 수 있도록 설정됩니다!
예시
- 해당 코드를 이해하기 너무 어려웠습니다. 예시로 풀어서 다시한번 보려고합니다.
@Injectable()
class UserMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
console.log('user middleware!');
next();
}
}
- 다음과 같은 미들웨어가 있다고 가정하겠습니다.
const wrapper = new InstanceWrapper({
metatype: UserMiddleware,
instance: new UserMiddleware(),
});
await bindHandler(
wrapper,
httpServerInstance,
{ path: '/users', method: RequestMethod.GET },
userModule,
middlewareCollection,
);
- 해당 미들웨어에 적용된 routeInfo는 다음과 같다고 가정하겠습니다.
{ path: '/users', method: RequestMethod.GET } - UserMiddleware가 /users 경로에 바인딩됩니다.
const paths = this.routeInfoPathExtractor.extractPathsFrom(routeInfo); // ['/users', '/products']
const isMethodAll = isRequestMethodAll(method); // false
const requestMethod = RequestMethod[method]; // 'GET'
- registerHandler 메소드에서 다음과 같은 정보로 추출됩니다.
const router = await applicationRef.createMiddlewareFactory(method); // GET 메서드에 대한 미들웨어 팩토리 생성
- 미들웨어 팩토리가 생성됩니다.
const middlewareFunction = isMethodAll
? proxy // pass
: <TRequest, TResponse>(
req: TRequest,
res: TResponse,
next: () => void,
) => {
if (applicationRef.getRequestMethod(req) === requestMethod) {
return proxy(req, res, next);
}
return next();
};
- isMethodAll이 false이므로 두 번째 조건부 함수 정의가 사용됩니다.
const pathsToApplyMiddleware = [];
paths.some(path => path.match(/^\/?$/))
? pathsToApplyMiddleware.push('/')
: pathsToApplyMiddleware.push(...paths); // ['/users']
pathsToApplyMiddleware.forEach(path => router(path, middlewareFunction)); // 각 경로에 미들웨어 함수 바인딩
- 각 경로에 대해서 적용합니다.
- 여기서는 전달받은 /user 경로 + GET method에 대해서 middlewareFunction을 적용합니다.
- /users 경로에 대해 GET 요청이 들어오면 해당 미들웨어가 실행됩니다.
모듈 정렬
const entriesSortedByDistance = [...configs.entries()].sort(
([moduleA], [moduleB]) => {
return (
this.container.getModuleByKey(moduleA).distance -
this.container.getModuleByKey(moduleB).distance
);
},
);
- 모듈들을 distance 기준으로 정렬합니다.
- distance는 모듈 간의 종속성을 기반으로 한 거리 값입니다.
- 정렬을 왜 하는가?
- dependency hierarchy를 고려하여 올바른 순서대로 미들웨어를 초기화하고 등록하기 위함입니다.
- 첫번째, 종속성 해결을 위해서 입니다.
- 모듈 간에 종속성이 있는 경우, 종속된 모듈이 먼저 초기화되어야 합니다. 예를 들어, ModuleB가 ModuleA에 의존하는 경우, ModuleA가 먼저 초기화되어야 ModuleB가 올바르게 동작할 수 있습니다.
- 이러한 종속성을 고려하여 모듈을 정렬하면, 필요한 모듈들이 먼저 초기화되고, 이후에 이를 참조하는 모듈들이 초기화됩니다.
- 두번째, 올바른 초기화 순서를 지키기 위해서 입니다.
- 위와 비슷한 이유입니다. 미들웨어 등록 과정에서 미들웨어가 참조하는 다른 모듈들이 올바르게 초기화된 상태여야 합니다. 그렇지 않으면 미들웨어가 올바르게 동작하지 않을 수 있습니다.
- 정렬을 통해 초기화 순서를 보장함으로써, 미들웨어가 필요한 종속성을 확실하게 사용할 수 있게 합니다.
- 세번째, 의존성 그래프를 구성하기 위해서 입니다.
- NestJS는 의존성 그래프를 통해 모듈과 미들웨어의 관계를 구성합니다. 이 그래프를 구성할 때 올바른 순서로 모듈을 처리해야 전체적인 애플리케이션 구조가 올바르게 형성됩니다.
미들웨어 설정 등록
for (const [moduleRef, moduleConfigurations] of entriesSortedByDistance) {
await registerAllConfigs(moduleRef, [...moduleConfigurations]);
}
- 정렬된 모듈들의 미들웨어 설정들을 순회하며 registerAllConfigs 함수를 호출해 미들웨어를 등록합니다.
- registerMiddlewareConfig 메서드는 특정 모듈의 미들웨어 설정을 등록하는 역할을 합니다.
registerMiddleware 정리
- registerMiddleware 메서드는 MiddlewareContainer에서 모든 미들웨어 설정을 가져와 각 모듈에 대해 순차적으로 미들웨어를 등록합니다.
- registerMiddlewareConfig 메서드는 특정 모듈의 미들웨어 설정을 경로 기반으로 등록합니다.
- registerRouteMiddleware 메서드는 특정 경로에 미들웨어를 실제로 등록하고(프록시를 설정하여 미들웨어를 거치게 만듦), 그래프 검사기와 연동하여 미들웨어를 관리합니다.
2. 다시 마지막 nestApplication, init()
public async init(): Promise<this> {
if (this.isInitialized) {
return this;
}
this.applyOptions();
await this.httpAdapter?.init();
const useBodyParser =
this.appOptions && this.appOptions.bodyParser !== false;
useBodyParser && this.registerParserMiddleware();
await this.registerModules();
await this.registerRouter();
await this.callInitHook();
await this.registerRouterHooks();
await this.callBootstrapHook();
this.isInitialized = true;
this.logger.log(MESSAGES.APPLICATION_READY);
return this;
}
- registerRouter() 에서 해당 과정을 거쳐 미들웨어들이 등록됩니다.
- 그리고 아래 메소드들을 호출하고 nestApplication이 initialized됩니다!
- 아래 메소드에 대해서는 다음 주제에서 알아보도록 하겠습니다.
- 이제 미들웨어가 적용된 nestApplication을 사용할 수 있게 되었습니다.
이어가며
- 너무 긴 과정이였습니다! 다음 글에는 긴 글을 읽기 쉽게 총 정리하고 마무리하려고합니다.
