인터셉터는 @Injectable()
데코레이터로 주석이 달린 클래스이다.
인터셉터는 NestInterceptor
인터페이스를 구현해야 한다.
인터셉터에는 AOP 기술에서 영감을 받은 유용한 기능들이 있다.
인터셉터는 두개의 인수를 취하는 intercept()
메서드를 구현한다.
ExecutinContext
인스턴스(첫번째 인수): ArgumentsHost
에서 상속됨. 이를 확장하여 현재 실행 프로세스에 대한 추가 세부정보를 제공하는 몇가지 새로운 헬퍼 메서드를 추가함으로써 인터셉터를 빌드하는데 도움을 줌
CallHandler
(두번째 인수): 인터셉터의 특정 지점에서 route handler 메서드를 호출하는데 사용할 수 있는 handle()
메서드를 구현한다. intercept()
메서드 구현에서 handle()
메서드를 호출하지 않으면 route handler 메서드가 실행되지 않음
이를 통해 최종 route handler 실행 전과 후에 custom하게 로직을 구현할 수 있다.
route handler 메서드 실행 전 intercept()
메서드에서 커스텀 로직을 구현하고, handle()
메서드를 호출하면, route handler 메서드가 호출되어 로직을 수행한다.
이후 handle()
메서드는 Observerble
을 반환하기 때문에 RxJS 연산자를 이용해 응답을 조작할 수 있다.
인터셉터의 첫번째 사용사례: 사용자 상호작용 기록(ex: 사용자 호출 저장)
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(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
인터셉터를 바인딩하기 위해 @UseInterceptors()
데코레이터를 사용한다.
컨트롤러, 메서드, 전역 범위로 인터셉터를 바인딩할 수 있다.
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
내부 인스턴스를 전달하는 방법도 있다.
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
전역 인터셉터를 설정하기 위해서는 다음과 같이 하면된다.
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
마찬가지로 전역 인터셉터를 위와 같이 지정하게 되면, 종속성을 주입할 수 없게 되는데 이를 해결하기 위해 모든 모듈에서 직접 인터셉터를 설정하면 된다.
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
위에서 handle()
이 Observable
을 반환한다고 했다. 그렇기에 응답 스트림에는 route handler에서 반환된 값이 포함되어 있다. 이를 RxJS의 map()
연산자를 이용해 쉽게 변경할 수 있다.
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>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}
인터셉터는 전체 어플리케이션에서 발생하는 요구사항에 대한 재사용 가능한 솔루션을 만드는데 큰 가치를 둔다.
예를 들어, null
값이면 빈 문자열로 변환한다고 해보자.
import { Injectable, NestInterceptor, ExecutionContext, 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 ));
}
}
RxJS의 catchError()
연산자를 활용해 던져진 예외를 재정의할 수도 있다.
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: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(new BadGatewayException())),
);
}
}
어떤 경우에서는 handler 호출을 방지해야되는 경우도 있다.
한가지 예는 바로 응답시간 개선을 위한 캐시 구현이다.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}
CacheInterceptor
에는 하드코딩된 isCached
변수와 하드코딩된 응답 []
가 있다.
더 주목해야할 부분은 RxJS of()
연산자에 의해 생성된 새 스트림을 반환하기에 route handler가 전혀 호출되지 않는다는 것이다.
RxJS 연사자를 이용해 스트림을 조작할 수 있는 것으로 우리는 많은 것을 할 수 있다.
다음은 route 요청 시간 초과를 처리하는 예이다.
일정 시간 내에 route handler 메서드에서 아무것도 반환하지 않으면 오류 응답으로 종료한다.
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);
}),
);
};
};