interceptor

https://docs.nestjs.com/interceptors

interceptor

interceptor 의 조건

다음 조건을 만족하는 클래스이다.

  • @Injectable() 로 데코레이터 돼있다.
  • NestInterceptor 인터페이스를 구현한다.
Interceptors's AOP technique

Interceptors 는 Aspect Oriented Programming 에 영감을 받았다.

  • 메서드 실행 전후로 추가적인로직을 바인드 할 수 있다.
  • 함수로부터 반환된 결과를 변형할 수 있다.
  • 함수로부터 던져진 예외를 변형할 수 있다.
  • 기본 함수의 행위를 확장할 수 있다.
  • 특정 조건에 의존하고 있는 함수를 완전히 override 할 수 있다. (e.g., for caching purposes)

Basics#

  • interceptor 는 intercept() 를 구현한다.
/**
* Interface providing access to the response stream.
*
* @see [Interceptors](https://docs.nestjs.com/interceptors)
*
* @publicApi
*/
export interface CallHandler<T = any> {
/**
* Returns an `Observable` representing the response stream from the route
* handler.
*/
handle(): Observable<T>;
}
/**
* Interface describing implementation of an interceptor.
*
* @see [Interceptors](https://docs.nestjs.com/interceptors)
*
* @publicApi
*/
export interface NestInterceptor<T = any, R = any> {
/**
* 커스텀 Interceptor 의 implement 을 위한 메서드
*
* @param context an `ExecutionContext` object providing methods to access the
* route handler and class about to be invoked.
* @param next a reference to the `CallHandler`, which provides access to an
* `Observable` representing the response stream from the route handler.
*/
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<R> | Promise<Observable<R>>;
}

ExecutionContext#

  • ArgumentsHost 를 확장하며, 새로운 헬퍼 메서드를 가진다.
    • 현재 실행 프로세스의 세부정보를 가진다.
      • Controller, Methods, Execution Contexts 에서 광범위하게 작동할수 있는 generic 한 interceptors 를 만들 수 있다.

Call handler#

  • CallHandler 인터페이스는 handle() 을 구현한다.

    • handle() 는 Interceptor 에서 원하는 시점에, Route Handler 메서드를 호출할 수 있게 해준다.
    • handle() 을 구현하지 않으면, Route Handler 메서드는 실행되지 않는다.
  • intercept() 메서드는 효율적으로 Request/Response 스트림을 래핑한다.

  • 그 결과, 마지막 route handler 의 실행 전후에 커스텀 로직을 구현할 수 있다.

  • handle() 이 호출되기 전에 실행되는 interceptor() 메서드에, 코드를 작성할 수 있다.

  • handle() 메서드는 Observable 을 반환하기 때문에, 강력한 RxJS 연산자를 이용하여 Response 를 추가적으로 조작할 수 있다.

  • Aspect Oriented Programming 기술을 사용하자면,

    • Pointcut → Route Handler 의 호출(handle() 을 호출하는것)
    • 추가적인 로직이 삽입되는 지점을 의미한다.
  • 예를들어, POST/cats 요청에서, CatsController 컨트롤러 내부에 create() 핸들러를 목적으로 했을 때,

    • handle() 을 호출하지 않는 interceptor 가 호출되었을 때, create() 는 실행되지 않는다.

Aspect interception#

logging.interceptor.ts :: storing user calls, asynchronously dispatching events or calculating a timestamp
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe( // rxjs
tap(() => console.log(`After... ${Date.now() - new}ms`));
);
}
}
NestInterceptor<T, R>
  • T
    • Observable<T> 의 타입,
    • response 스트림을 서포트 한다.
  • R
    • Observable<R> 에 의해 래핑된 값의 타입
note

Interceptor 는

  • controllers, providers, guards 등과 같이, 생성자에서 주입될 수 있다.
  • handle() 는 RxJS Observable 을 리턴한다
  • RxJS 으로, stream 을 조작할 수 있는 많은 연산자를 가진다.
  • tap()
    • observable stream 이 정상적으로 종료되거나, 예외적으로 종료될 때 익명로깅을 호출한다.
    • 그렇지 않는 경우에도 Response 사이클에 방해되지 않는다.

Binding interceptors#

cats.controllers.ts:: 프레임워크에게 초기화와 생성자 주입의 책임을 넘긴다.
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
interceptor 의 scope
  • controller-scoped
  • method-scoped
  • global-scoped
cats.controller.ts
@UseInterceptors(new LoggingInterceptor())
exports class CatsContrller {}
전역 Interceptor 바인딩::모듈밖에 있기 때문에 의존성주입을 할 수 없다.
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
app.module.ts::모듈을 사용하여 전역 Interceptor 사용하기
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
// 선언되는 모듈에 상관없이 Interceptor 는 전역적으로 작동한다.
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
]
})
export class AppModule {}

Response mapping#

  • handle() 은 Observable 을 반환하고, stream 에는 route handler 에서 반환된 값이 포함되어 있으므로,
  • RxJS 의 map() 연산자를 사용하여 쉽게 변경할 수 있다.
transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercep(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handler().pipe(map(data => ({ data })));
}
}
/* response of Request
{
"data": []
}
*/
tip

async intercept(...) 비동기 메서드로 선언할 수 있다.

transform each occurrence of a null value to an empty string ''
import { Injectable, NestInterceptor, ExceutionContext, Callhandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'
@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(map(value => value === null ? '' : value ));
}
}

Exception mapping#

errors.interceptor.ts :: RxJS's catchError() - override thrown exceptions
import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
Callhandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExceutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(() => new BadgatewayException()))),
);
}
}

Stream overriding#

cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
interceptor(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]); // return a new stream & route handler won't be called
}
return next.handle();
}
}

핸들러의 호출을 막고 different Value 를 반환하고 싶은 경우

  • 응답속도 향상을 위한 cache 구현
  • 일반적인 방법으로 구현하고 싶다면, Reflector 와 커스텀 데코레이터로 구현할 수 있다.

More Operators#

timeout.interceptor.ts :: handle timeouts on route requests
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
}
}
Last updated on