[ NestJS ] Interceptor와 AOP 패턴에 대해서 알아보자
이번 포스팅에서는 Nest에서 Interceptor에 대해서 알아보려고 한다.
그전에 AOP와도 연관이 있으니 먼저 AOP 패턴에 대해서 알아보려고 한다.
왜냐하면 Nest에서 Intercepto가 AOP(Aspect-oriented programming)에서 영감을 받았기 때문이다.
AOP(Aspect-oriented programming) 란?
우리말로 관점(측면) 지향 프로그래밍이라고 한다.
목적으로는 모듈성을 높이는 것을 목표로 하는 프로그래밍 패러다임이다.
위의 그림을 보면 핵심기능 3가지가 있고 이 핵심 기능이 애플리케이션의 각각의 컨트롤러라고 생각해보자.
각각의 컨트롤러는 저마다 수행하는 기능이 있지만 공통적으로 수행하는 기능도 존재한다.
예를 들어 클라이언트가 서버에게 어떤 요청을 보냈는지에 대한 정보를 출력하는 Logging기능이 각 컨트롤러에 공통적으로 존재한다고 하면, 이 기능을 수평적으로 묶어 모듈화를 한 것이 AOP 패턴이라고 생각하면 이해하기가 쉽다.
물론 로깅하는 기능은 미들웨어에서도 충분히 구현이 가능하지만, 인터셉터를 이용해서도 구현이 가능하다.
이후에 차이점에 대해서 알아볼 예정이다. 우선, 공식문서에 나와있는 인터셉터의 예제를 살펴보도록 하자.
logging.interceptor.ts
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`)),
);
}
}
우선 위의 Before 부분은 컨트롤러가 실행되어지기 전에 먼저 수행되는 부분이다.
그리고 After .. 부분은 컨트롤러에서 수행되고 난 뒤 수행되는 부분이다.
여기서 Interceptor라는 이름이 왜 지어졌는지 알 수 있는 부분인것 같다.
컨트롤러를 두고 전후로 간섭해 특정 기능을 수행하는 녀석이라고 보면 될 것 같다.
이전 파이프 디자인 패턴에서 만들었던 get 요청을 사용해서 위의 인터셉터가 어떻게 수행되는지 실행 결과를 보도록 하자.
위의 예시를 가지고 살짝 변형하여 요청이 성공적으로 수행되었는지 확인하는 인터셉터를 먼저 만들어보자.
common/interceptors/success.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, map } from 'rxjs/operators';
@Injectable()
export class SuccessInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...'); // pre-controller
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)), // post-request
map((data)=> ({
success: true,
data,
})) // 여기서 data는 컨트롤러를 거친 후 응답(response)에 대한 data
);
}
}
// 공통된 기능을 수행하는 부분을 AOP 관점으로 모듈화를 시킨 것이다.
// 미들웨어와 인터셉터가 다른점은 실행 순서가 다르다는 점이다.
// 위의 인터셉터는 exception filter처럼 성공했을 때 데이터를 가공하는 인터셉터이다
Interceptor, middleware, pipe와 같은 것들은 각 컨트롤러에서 공통적으로 사용되어지기 때문에 common이라는 폴더를 두고
아래와 같이 폴더 구조를 바꿔주었다.
그리고 만든 interceptor를 cats controller 상위에 아래와 같이 의존성 주입(DI)를 시켜준다.
인터셉터는 인터페이스 @Injectable()를 구현하는 데코레이터로 주석이 달린 클래스 이므로 DI를 통해서 사용할 수 있다.
@Controller('cats')
@UseInterceptors(SuccessInterceptor) // Interceptor DI
export class CatsController {
constructor(
private readonly catsService: CatsService,
private readonly authService: AuthService
) { }
// 생략
@Get(':id')
getOneCat(@Param('id', ParseIntPipe, PositiveIntPipe) param: number) {
console.log('hello controller')
return { cats:'get one cat api' };
}
// 생략
}
위의 요청을 테스트 해보도록 하자.
get 요청으로 세팅하고 cats 컨트롤러이므로 cats와 파라미터로 id값이 필요하므로 임의로 123을 넣어서 요청을 보내보자.
위와 같이 요청을 보내면, terminal 에는 아래와 같이 결과가 출력된다.
컨트롤러를 실행하기 전, Before... 가 출력되고 hello controller는 컨트롤러 내부의 console문, 그리고 마지막 After... 에는 다시 인터셉터의 return 부분이 수행됨을 알 수 있다.
따라서 interceptor는 Pre-controller와 Post-controller 이 두 부분으로 나누어짐을 알 수 있었다.
대게 pre-controller 부분은 잘 사용하지 않고 컨트롤러를 수행하고 난 뒤 한번 더 data 검사를 하기 위해 Post-controller를 주로 활용한다고도 한다.
그리고 요청을 보낸 insomnia의 결과창을 보면 우리가 만든 인터셉터의 결과까지 포함되어 응답을 보낸 모습이다.
이렇게 요청을 보내면 프런트 측에서 요청이 정상적으로 수행되었는지, 아닌지 확실하게 알 수 있다.
이 부분은 success.interceptor.ts에서 rxjs의 map 함수를 사용해서 컨트롤러를 거친 후 응답에 대한 data와 함께 success: true라는 메시지를 같이 보내주어서 요청에 대한 결괏값이 위와 같은 결과가 나옴을 알 수 있다.
Nest의 요청 수명 주기
nest 공식문서에서 FAQ에 요청 수명주기에 대한 자세한 설명과 함께 아래로 내려보면 그림과 같이 요약이 나온다.
위의 순서가 nest에서의 요청 사이클이라고 보면 된다.
위의 순서대로 수행이 되다가 중간에 예외가 발생한다면 19번의 Exception filters로 예외처리가 된다고 보면 된다.
미들웨어와 인터셉터는 위의 요청 수명 주기에서 볼 수 있듯이 실행 순서에서 차이점이 있다는 점이다.
보통 인터셉터의 Before 부분은 인터셉터에서 보다는 미들웨어에서 처리를 주로 하는 편이라고 한다.
추가적으로 interceptor은 서비스의 return 값 데이터를 원하는 형식에 맞게 가공할 때 많이 사용된다.
이상으로 AOP 패턴과 nest에서 AOP를 사용한 interceptor에 대해서 알아보았다!!