Interceptors
Interceptors
NestInterceptor
인터페이스를 구현하고 @Injectable()
데코레이터가 어노테이션으로 붙는 클래스이다.
interceptor 는 AOP 관점에서 탁월한 능력을 몇가지 갖고 있다.
- method 가 실행되기 전과 후에 외부 로직을 bind 할 수 있다.
- 함수에 return 을 변형시킬 수 있다.
- 함수에 예외를 변형시킨다.
- 함수에 기본동작을 확장한다.
- 특정 조건에 따라 완전히 함수를 오버라이딩 할 수 있다.
Basics
Interceptor 는 intercept()
메서드를 구현한다. intercept()
메서드는 두개의 매개변수를 갖는다. 하나는 ExecutionContext
인스턴이다( guard 와 정확히 같은 객체). ExecutionContext
는 ArgumentsHost
를 상속한다. 두번째 매개변수는 Call handler
이다.
Execution context
ArgumentHost
를 확장했고, ExecutionContext
는 현재 실행 프로세스를 세부적인 사항을 제공하는 헬퍼 메서드를 추가했다.
Call handler
CallHandler
인터페이스는 route handler method 를 interceptor 에 특정 시점에 호출하여 사용할 수 있는 handler()
메서드를 구현한다. 만약 interceptor()
메서드에서 handler()
메서드를 호출하지 않는다면 route handler method 는 실행되지 않을 것이다.
이것은 interceptor()
메서드가 효과적으로 request/response 스트립을 wrap 했다는 것을 의미한다. 결과적으로 route handler method 가 호출되기 전과 후에 호출되는 커스텀 로직을 구현할 수 있게된다. handler()
가 호출되기 전에 로직을 작성하면 route handler method 가 호출되기 전 실행되는 로직을 만들 수 있는 것은 분명한데, 호출된 이후에는 어떻게 알 수 있을까? 답은 handler()
메서드가 Observable
를 반환하기 때문에 알 수 있다. RxJs
기능을 사용하여 응답을 조작하는 작업을 할 수 있다. AOP 관점에서 handler()
메서드는 추가 로직이 추가되어야 하는 지점을 나타내는 Pointcut
이라고 불린다.
handler()
가 호출되는 시점에서 Controller 에 메서드가 트리거 되고 Observable
를 반환하기 때문에 stream 안에서 추가작인 작업을 수행할 수 있다.
Aspect interception
첫번째 use case 는 사용자의 intercation을 로그로 남기는 LoggingInterceptor 이다.
1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; |
NestInterceptor<T,R>
는 generic interface 이다. T 는 응답 스트림 Observable<T>
R 은 Wrapping 된 Observable<R>
을 나타낸다.
interceptors 또한, constructor 를 통해서 주입될 수 있다.
Binding interceptors
인터셉터를 세팅하기 위해서 @UseInterceptors()
를 사용한다. pipes
나 guards
와 마찬가지로 controller-scoped, method-scoped, global-scoped 모두 가능하다.
1 | @UseInterceptors(LoggingInterceptor) |
이제 컨트롤러에 요청마다 다음과 같은 로그를 확인할 수 있다.
1 | Before... |
위에 예에서 Type을 전달하여 프레임워크에 인스턴스화에 대한 책임을 맡기고 종속성을 주입했다. 또한, 인스턴스를 넘기는 것도 가능하다.
1 | @UseInterceptors(new LoggingInterceptor()) |
Controller 레벨에서 설정했기 때문에 모든 메서드마다 Logging 이 실행된다. 만약 특정 함수로 제한하고 싶다면 method 레벨에서 설정해주면 된다.
전역으로 설정하고 싶다면 app 에 useGlobalInterceptors()
를 사용하여 등록해주면 된다.
1 | const app = await NestFactory.create(AppModule); |
이렇게 작성하면 dependnecy injection 측면에서 좋지 않기 때문에 module 에 주입하도록 작성할 수 있다.
1 | import { Module } from '@nestjs/common'; |
Response mapping
handler()
는 Observable
를 반환한다. 따라서 RxJS 의 map()
기능을 사용할 수 있다.
응답 매핑 기능은 라이브러리별 응답전략에서는 작동하지 않는다. (@Res 에서 작동 안함)
RxJS 의 map
을 사용하여data property 를 응답에 추가해 클라이언트에게 전달하는 TransformInterceptor
를 구현해 보자.
1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; |
Nest interceptors 는 동기와 비동기 모두 동작한다. 필요하면 async
키워드를 사용할 수 있다.
interceptor 는 애플리케이션 전체를 가로지르는 좋은 해결책이 된다. 만약 모든 응답에서 null 인 값을 빈 스트링으로 바꿔야 한다고 가정해보자. 이 경우 interceptor 구현하고 전역으로 사용하도록 주입하면 된다.
1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; |
Exception mapping
RxJS 의 catchError()
를 사용하여 예외를 override 할수도 있다.
1 | import { |
Stream overriding
응답 시간을 줄이기 위해 캐시를 사용해 응답을 완전히 바꾸는 등의 작업도 할 수 있다.
1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; |
여기서는 하드 코딩 된 isCached 와 [] 를 사용했지만 중요한 점은 RxJS 의 of 를 사용하여 생성된 스트림을 여기에서 반환하므로 route handler 가 호출되지 않는다는 것이다. 만약 CacheInterceptor 를 사용하는 메서드에서는 응답이 빈 리스트로 바로 반환될 것이다.
More operators
route request 에 timouts 를 주고 싶다고 가정해보자. 일정 기간동안 응답 하지 않으면 error 를 반한하게 한다.