nestjs_횡적 부가기능을 위한 interceptor
Interceptor란
Interceptor는 AOP(Aspect Oriented Programming)에서 영감을 받은 기술이다. AOP는 관점지향 프로그래밍 패러다임을 의미하는데, 부가 기능을 모듈화하여 코드의 재사용성을 극대화하는 방법이다.
Intercepts는 이러한 일들을 할 수 있다.
- 메소드 실행 전/후 추가적인 로직을 적용하고싶을 때
- 함수에서 반환된 결과값을 변형하고 싶을 때
- 함수에서 발생한 예외를 변형하고 싶을 때
- 기본적인 함수의 기능을 확장시키고 싶을 때
- 조건에 따라 함수를 오버라이딩하고싶을 때. (ex: 캐싱)
Interceptor은 @Injectable() 데코레이터로 주석을 달 수 있다. 즉, 의존성 주입이 가능하다는 말이다.
전체 요청 생애 주기 내 Interceptor의 위치
nest.js의 request의 생에 주기는 이렇다.
- Incoming request
- Globally bound middleware
- Module bound middleware
- Global guards
- Controller guards
- Route guards
- Global interceptors (pre-controller)
- Controller interceptors (pre-controller)
- Route interceptors (pre-controller)
- Global pipes
- Controller pipes
- Route pipes
- Route parameter pipes
- Controller (method handler)
- Service (if exists)
- Route interceptor (post-request)
- Controller interceptor (post-request)
- Global interceptor (post-request)
- Exception filters (route, then controller, then global)
- Server response
https://docs.nestjs.com/faq/request-lifecycle
Interceptor 생성
// common/interceptors/success.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class SuccessInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Pre-controller interceptor stage
console.log('Before...');
const now = Date.now();
// Post-request interceptor stage
return next
.handle()
.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
}
}
공식문서에서 제공하는 템플릿 인터셉터 파일을 만들었다. 인터셉터는 여러 모듈에서도 사용하는 일종의 횡적으로 기능을 수행하기 때문에 특정 모듈 내 종속시키는 것 보다는 따로 common 폴더를 만들어서 관리를 하면 좋다.
Interceptor 적용
...
@Controller('cats')
@UseInterceptors(SuccessInterceptor)
@UseFilters(HttpExceptionFilter)
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
getAllCat() {
// Node.js : throw new Error
// throw new HttpException('api is not available 😭', 401);
return 'all cat';
}
@Get(':id')
getOneCat(@Param('id', ParseIntPipe, PositiveIntPipe) param) {
console.log(typeof param);
return 'one cat';
}
...
cats 컨트롤러에 SuccessInterceptor를 Intercept를 붙였다.
비즈니스 로직에서 number을 반환하고 이 전과 후에 Intercept가 모두 수행된 것을 알 수 있다.
전체 생애주기에서 interceptor가 크게 2 영역으로 구분되는데, 이 두 영역이 실제 코드에서는 Before과 After 부분에 해당한다.
Intercept 변형
data는 그대로두고 success 프로퍼티만 추가하고 싶다면 어떻게 해야할까? success 성공 여부 데이터를 추가하면 프론트엔드 개발자가 편하게 응답 데이터를 이해할 수 있을 것 같다. controller 반환값에 다른 프로퍼티를 추가하는 intercept로 커스터마이징 해보자.
// common/interceptors/success.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable()
export class SuccessInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Pre-controller interceptor stage
console.log('Before...');
// Post-request interceptor stage
// Controller return값 변환 방식 변경
return next.handle().pipe(
map((data) => ({
success: true,
data: data,
})),
);
}
}
map은 rxjs 라이브러리의 메소스다. data에 success, data 프로퍼티를 추가해주었다. success를 추가한 이유는 프론트 단이 이 메시지를 보고 응답 결과를 판단하기 쉽게 하기 위함이다.
...
@Controller('cats')
@UseInterceptors(SuccessInterceptor)
@UseFilters(HttpExceptionFilter)
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
getAllCat() {
// Node.js : throw new Error
// throw new HttpException('api is not available 😭', 401);
console.log('getAllCat controller hanlder :D');
return { cats: 'get all cat please!' };
}
@Get(':id')
getOneCat(@Param('id', ParseIntPipe, PositiveIntPipe) param) {
console.log(typeof param);
return 'one cat';
}
...
컨트롤러 단의 코드를 보면 getAllCat은 객체를, getOneCat은 단순 문자열만 반환한다. 이들이 interceptor을 통해 success 프로퍼티와 함께 반환 되면 어떻게 되는지 보자.
data는 동일하되 success 프로퍼티가 잘 추가되었다.