인터셉터는 Injectable()
데코레이터로 주석이 달린 클래스이며, NestInterceptor 인터페이스를 구현해야 한다. 인터셉터에는 AOP(Aspect Oriented Programming) 기술에서 영감을 받은 유용한 기능 세트가 있다.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Request, Response } from 'express';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request: Request = context.switchToHttp().getRequest();
const response: Response = context.switchToHttp().getResponse();
return next.handle();
}
}
각 인터셉터는 두개의 인수를 취하는 intercept()
메소드를 구현한다.
첫번째는 ExcutionContext
인스턴스이다. 이 인스턴스를 통해 라우터 핸들러의 요청과 응답 세부정보를 얻을 수 있다.
두번째 인수는 CallHandler
이다. 이 인터페이스는 인터셉터의 특정지점에서 라우트 핸들러 메서드를 호출하는데 사용할 수 있는 handle() 메서드를 구현한다. handle() 메서드를 호출하지 않으면 라우터 핸들러 메서드는 전혀 실행되지 않는다.
이 접근방식은 최종 라우터 핸들러 실행 전과 후에 커스텀 로직을 추가할 수 있다. handle() 메서드는 Observable을 반환하기 때문에 강력한 rxjs 연산자를 사용하여 응답을 추가로 조작할 수 있다. AOP 용어를 사용하여 라우트 핸들러의 호출을 Pointcut
이라고 한다.
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`)),
);
}
}
인터셉터를 사용하여 사용자 상호작용을 기록하는 예시이다. 위의 예시는 응답주기를 방해하지 않는 tap()
연산자를 사용했다.
스트림에는 라우트 핸들러에서 반환된 값이 포함되어 있으므로 rxjs의 map()
연산자를 사용하여 쉽게 변경할 수 있다.
응답 매핑 기능은 라이브러리별 응단 전략에서 작동하지 않는다.
@Res() 객체를 직접 사용하는 것은 금지된다!
인터셉터는 전체 애플리케이션에서 발생하는 요구사항에 대한 재사용 가능한 솔루션을 만드는데 큰 가치를 둔다.
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 ));
}
}
위의 예제는 null 값의 각 항목을 빈 문자열 ' ' 로 변환하는 인터셉터이다. 인터셉터를 전역적으로 바인딩하여 등록된 각 핸들러가 자동으로 사용하도록 할 수 있다.
rxjs의 catchError()
연산자를 사용하여 throw 예외를 재정의할 수 있다. Sentry를 사용하여 에러를 기록할 때 유용하다.
import { Injectable, NestInterceptor, ExecutionContext, 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(() => error)),
);
}
}
때로는 핸들러 호출을 완전히 방지하고 대신 다른 값을 반환해야 하는 경우가 있다. 캐시에서 응답을 반환하는 캐시 인터셉터를 알아본다.
import { Injectable, ExecutionContext, CallHandler, NestInterceptor } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { Request, Response } from 'express';
import { RedisCacheService } from 'src/redisCache/redis-cache.service';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(private cacheManager: RedisCacheService) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request: Request = context.switchToHttp().getRequest();
const response: Response = context.switchToHttp().getResponse();
const cacheData = await this.cacheManager.get('abcdefg');
if (!cacheData) {
return next.handle();
}
return of(
response.status(200).json({
errorMessage: null,
result: cacheData.result,
}),
);
}
}
캐시데이터가 있다면 rxjs of()
연산자에 의해 생성된 새 스트림을 여기에 반환하므로 라우트 핸들러가 전혀 호출되지 않는다. 누군가 CacheInterceptor 를 사용하는 엔드포인트를 호출하면 응답이 즉시 반환된다.
인터셉터를 설정하기 위해 @nestjs/common
패키지에서 가져온 @UseInterceptor()
데코레이터를 사용한다. 인터셉터는 컨트롤러 범위, 메서드 범위 또는 전역범위일 수 있다.
컨트롤러 & 메서드 범위
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
export class CatsController {
@Get()
@UseInterceptors(LoggingInterceptor)
async getInfo() {
...
}
}
전역 범위
// main.ts
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
위의 예시는 모듈의 컨텍스트 외부에서 수행되므로 종속성을 주입할 수 없다. 이 문제를 해결하기 위해 다음 구성을 사용하여 모든 모듈에서 직접 인터셉터를 설정한다.
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}