이번 포스팅은 지난 포스팅에서 작성한 repository api
와 querybuilder
의 성능을 비교하기 위해 Interceptor
를 사용하며 알게 된 내용을 정리해 보았습니다.
NestJS
에서는 미들웨어, 파이프, 가드, 인터셉터 등 다양한 기능들을 보유하고 있습니다. 우선 NestJS
의 Request Lifecycle
부터 알아 보겠습니다.
미들웨어는 라우트 핸들러 이전에 호출되는 함수입니다. 미들웨어 기능은 애플리케이션의 요청-응답 주기에서 요청 및 응답 객체와
next()
미들웨어 함수에 액세스할 수 있습니다.
인터셉터는 핸들러가 호출되기 전후에 경로를 가로챕니다. 즉, 요청 및 응답을 조작하여 추가 논리를 바인딩하고 결과를 변환하는 등의 작업을 수행할 수 있습니다.
가장 큰 차이점은 받는 파리미터가 다르다는 점 입니다.
Middleware
는 파라미터로 request
, response
, next
받아HTTP
가 아니면 사용이 불가합니다.
반면에 Interceptor
는 파라미터로 ExecutionContext
와 CallHandler
를 받아 처리 합니다.
import { Type } from '../index';
import { ArgumentsHost } from './arguments-host.interface';
/**
* Interface describing details about the current request pipeline.
*
* @see [Execution Context](https://docs.nestjs.com/guards#execution-context)
*
* @publicApi
*/
export interface ExecutionContext extends ArgumentsHost {
/**
* Returns the *type* of the controller class which the current handler belongs to.
*/
getClass<T = any>(): Type<T>;
/**
* Returns a reference to the handler (method) that will be invoked next in the
* request pipeline.
*/
getHandler(): Function;
}
첫번째 파라미터인 ExecutionContext
는 ArgumentsHost
를 확장하여 현재 실행 프로세스에 대한 추가 세부 정보를 제공합니다.
ArgumentsHost
는 단순히 핸들러의 인수에 대한 추상화 역할을 합니다. 예를 들어 HTTP
서버 애플리케이션의 경우 호스트 객체는 Express
의 [request, response, next]
배열을 캡슐화합니다. 반면에 GraphQL
의 경우 호스트 객체에는 [root, args, context, info]
배열을 포함합니다.
때문에 Interceptor
는 HTTP
이외에도 WebSocket
, GraphQL
, RPC(Remote procedure call)
에서도 동작 가능합니다.
두번째는 Observable
객체인 CallHandler
입니다.
처리과정을 살펴보면 /cats
에 POST
요청이 들어오면. 이 요청은 CatsController
내부에 정의된 create()
핸들러로 향합니다. handle()
메서드를 호출하지 않는 인터셉터가 중간에 호출되면 create()
메서드는 실행되지 않습니다. handle()
이 호출되고 Observable
이 반환되면 create()
핸들러가 트리거됩니다. 그리고 응답 스트림이 Observable
을 통해 수신되면 스트림에서 추가 작업을 수행할 수 있으며 최종 결과는 호출자에게 반환됩니다.
여기서 Observable
를 좀더 알아보겠습니다.
RxJS
는observable sequences
를 사용하여 비동기 및 이벤트 기반 프로그램을 구성하기 위한 라이브러리입니다. 하나의 핵심 유형,Observable
, 위성 유형(Observer
,Schedulers
,Subjects
) 및Array
메서드(map
,filter
,reduce
,every
등)에서 영감을 받은 연산자를 제공하여 비동기 이벤트를 컬렉션으로 처리할 수 있습니다.
RxJS
에서 컬렉션은 단일 엔터티로 조작 및 변환할 수 있는 관찰 가능한 스트림 그룹을 나타냅니다. 컬렉션은 여러 Observable
을 결합하여 생성할 수 있으며 RxJS
에서 제공하는 다양한 연산자를 사용하여 조작할 수 있습니다.
스트림은 시간의 흐름에 따라 발생하는 일련의 이벤트입니다.
핵심 유형인 Observable
은 Observer
를 인수로 취한 다음 시간 경과에 따라 일련의 값을 생성하는 함수로 생각할 수 있습니다.
Observer
는 Observable
의 동작을 정의하는 객체로 next
, error
, complete
를 사용해 event
을 처리 합니다.
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Logger } from '@nestjs/common';
@Injectable()
export class QuerySpeedInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
return next.handle().pipe(
tap(async () => {
const end = Date.now();
const time = end - start;
const logger = new Logger();
logger.debug(`Query executed in ${time}ms.`, `Database`);
}),
);
}
}
Interceptor
내에서 쿼리 시작 시간은 Date.now()
메서드를 사용하여 기록됩니다. 그런 다음 next.handle()
메서드로 Observable
객체로 만들어 줍니다. 다음pipe
메서드가 호출되어 Observable
에 연산자를 적용할 수 있습니다.
RxJS
에서 tap
은 emitted value
나 Observable stream
을 수정하지 않고 Observable
이 내보낸 값에 대해 Side Effect
없이 수행할 수 있게 해주는 연산자입니다.
위의 경우 tap
연산자는 쿼리 실행 시간을 기록하는 함수를 실행하는 데 사용됩니다. tap
함수 내에서 Date.now()
를 사용하여 쿼리의 종료 시간을 기록하고 실행 시간을 얻기 위해 시작 시간과 종료 시간의 차이를 계산합니다. 마지막으로 debug
메서드를 사용하여 실행 시간을 기록하기 위해 Logger
인스턴스를 생성합니다.
@Get('/find/:text')
@UseFilters(QueryFailedExceptionFilter)
@UseInterceptors(QuerySpeedInterceptor)
async findRoomByText(@Param('text') text:string): Promise<RoomEntity[]> {
return await this.roomsService.findRoomByText(text);
}
컨트롤러에 적용해 줍시다.
요청이 수행될 때 마다 로그가 발생하는 모습.
참고
docs.nestjs.com/interceptors
RxJS
NestJS Interceptor와 Lifecycle
[NestJS] Lifecycle Events