https://docs.nestjs.com/interceptors
interceptor는 @Injectable()
데코레이터를 사용하고, NestInterceptor
인터페이스를 구현한다.
Aspect Oriented Programming(AOP)를 적용 → 핵심 기능에서 부가적인 기능을 독립적인 애스펙트로 빼냄
따라서,
이런 것들을 가능하게 한다.
인터셉터는 intercept()
메서드를 구현한다. 이 메서드는 요청/응답 스트림을 잘 포장한다. 그 결과로, 최종 라우트 핸들러 실행 전후에 커스텀 로직 추가 가능
매개변수
ExecutionContext
인스턴스 (Guards에서 본 그것)CallHandler
ArgumentsHost를 extend하면서 여러 헬퍼 메서드(현재 실행 프로세스에 관한 추가 정보 제공)를 추가
→ 더욱 일반적인(컨트롤러, 메서드, 실행 컨텍스트의 집합을 가로지르는ㅋㅋ) 인터셉트 구현에 도움이 됨
CallHandler
인터페이스는 handle()
메서드를 구현한다.
intercept()
메서드의 구현에서 handle()
을 호출해야 라우트 핸들러 메서드 사용 가능handle()
메서드는 Observable
을 리턴 → RxJS 연산자를 이용하여 응답 조작 가능 (RxJS랑 옵저버블이 뭐임?)AOP에서는, 라우트 핸들러 호출(using handle()
)은 Pointcut 이라고 부른다. (추가적인 로직이 들어오는 곳)
POST /cats 요청이 들어왔다고 치자.
유저 상호작용을 로깅하기 위해 인터셉트를 사용하는 예시를 보자.
간단하게 LogginInterceptor
를 만들어 봅시다.
nest g in logging
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> {
return next.handle();
}
}
NestInterceptor<T, R>
은 제네릭 인터페이스
T:Observable<T>
의 타입
R: type of the value wrapped byObservable<R>
?? 무슨말이지
다른 것들과 마찬가지로
constructor
를 통한 DI 가능
intercept()
메서드를 아래와 같이 수정
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`)),
);
}
아마
Before… 찍힘 → 라우트 핸들러 실행(by handle()
) → After… 걸린시간ms 찍힘
인듯..하다
handle()
이 RxJS Observable
을 리턴하기 때문에, 스트림 조작에 사용할 수 있는 오퍼레이터의 종류가 다양하다.
위의 코드에서 tap()
오퍼레이터를 사용했음. 이건 observable 스트림이 정상/예외 종료 시 콘솔로그(After 머시기)를 호출하지만, 그렇지 않으면 응답 사이클에 간섭하지 않는다.
(tap()
은 RxJS에 있음 - npm i rxjs
)
인터셉터 설정은 @UseInterceptors()
데코레이터를 사용한다.
다른 것들 처럼.. controller-scoped, method-scoped, global-scoped 가능
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
마찬가지로.. 타입을 넘겨줌으로써 프레임워크에 DI 책임 전가함. 인스턴스 넘겨줘도 됨
글로벌로 설정하기 위해서는 어플리케이션 인스턴스의 useGlobalInterceptors()
메서드를 사용한다.
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
하지만 이젠.. 눈치 챌 수 있음.. 이렇게 하면 DI가 안된다~ 왜? 모듈 밖이라서
그렇다면 해결 방법은 역시나 모듈에서 설정하는 것이겠죠?
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
useClass
is not the only way of dealing with custom provider registration. Learn more here.
자 handle()은 Observable을 리턴한다. 스트림은 라우트 핸들러가 리턴한 값을 가지고 있다. 따라서, RxJS의 map() 연산자를 이용하면 응답을 변경할 수 있다.
주의!
응답 매핑 기능은 특정 라이브러리의 응답과 같이 사용할 수 없음
@Res() 객체를 직접 사용하는 것은 금지된다.
TransformInterceptor
를 만듭시다.
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 })));
}
}
map() 연산자를 보면 라우트 핸들러의 응답을 data로 받아서 { data: 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 ));
}
}
짠 이 인터셉트를 전역으로 등록하면 자동으로 null을 빈 문자열로 바꿔준다.
이번엔 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())),
);
}
}
핸들러 호출을 막고 다른 값을 리턴해야 하는 경우가 있다.
→ 응답 시간 향상을 위해 캐시를 구현
현실적으로는, TTL, 캐시 무효화, 캐시 사이즈 등을 고려해야 하지만.. 일단은 간단한 구조만 봅시다.
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();
}
}
of()
는 새로운 스트림을 리턴(라우터 핸들러는 호출되지 않음)
캐시인터셉터를 사용하는 엔드포인트에 접근하면, 응답이 바로 반환된다.
일반적인 솔루션(?)을 만드는 대신, Reflector와 커스텀 데코레이터를 이용할 수 있다.
다양하게 있지만.. 마지막으로 timeout 설정 예시를 봅시다.
주어진 시간 내에 리턴이 없으면 예외를 던진다.
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);
}),
);
};
};