NestJS 프레임워크 사용을 위한 기본 개념에 대해 간략하게 정리해 보고자 한다.
본 포스팅에서는 Pipe 패턴과 Interceptors 에 대해 알아보도록 하겠다.
< 목차 >
- Pipes
- Interceptors
1. Pipes
파이프(Pipe)는 클라이언트 요청에서 들어오는 데이터에 대해 유효성 검사 및 변환을 수행하여 서버가 원하는 데이터를 얻을 수 있도록 도와주는 클래스이다. 파이프에는 다음과 같은 두가지 일반적인 사용 사례가 있다.
- 변환(transformation) : 입력 데이터를 원하는 형식으로 변환 (ex. 문자열에서 정수로)
- 유효성 검사(validation) : 입력 데이터를 평가하고 유효하면 변경하지 않고 전달한다. 그렇지 않으면 데이터가 올바르지 않을 때 예외를 발생시킨다.
다음은 NestJS 공식문서에서 제공해주는 파이프에 관한 예제 코드이다.
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
위와 같이 id 값을 받아서 동적 라우팅을 진행할 때 ParseIntPipe를 @Param( ) 데코레이터의 두번째 인자값으로 전달한 것을 확인할 수 있다. 이처럼 파이프를 사용하기 위해서는 파이프 클래스의 인스턴스를 적절한 컨텍스트에 바인딩해야 한다.
ParseIntPipe를 통해 string 타입으로 들어온 id 값을 number 타입으로 타입 변환해 줄 수 있다. 또한 id 값이 numeric string으로 전달되지 않은 경우에는 validation 에러까지 발생시킬 수 있다.
GET localhost:3000/abc
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
추가로 하나의 파이프는 이전 파이프에서 전달된 결과를 입력값으로 받아 또 다른 결과값을 내놓게 되는데 이러한 방식으로 여러 개의 파이프들을 나열해서 Pipes and Filters pattern을 구현할 수 있다.
다음은 Microsoft에서 제공해주는 Pipes and Filters pattern에 대한 내용이다. 참고해보면 좋을 듯 하다.
https://learn.microsoft.com/en-us/azure/architecture/patterns/pipes-and-filters
2. Interceptors
공식문서에 따르면 NestJS의 Interceptor는 AOP(Aspect Oriented Programming; 관점 지향 프로그래밍)에서 영감을 받아 만들어진 기능이라고 한다. AOP는 간단히 말해서 관심사의 분리(기능의 분리) 라고 할 수 있다.
위와 같이 업무 로직을 포함하는 기능을 핵심 기능(Core Concerns) , 핵심 기능을 도와주는 로깅/보안/트랜잭션과 같은 부가적인 기능을 부가 기능(Cross-cutting Concerns)이라고 한다.
객체 지향의 기본 원칙을 지키면서 프로그래밍을 하여도 핵심 기능에서 부가 기능을 분리해서 모듈화 하는 것은 굉장히 어렵다. AOP는 부가 기능을 Aspect로 정의하여 핵심 기능에서 부가 기능을 분리함으로써 핵심 기능을 설계하고 구현할 때 객체 지향적인 가치를 지킬 수 있도록 도와주는 개념이다. 다시 말해, AOP는 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리하고 분리한 부가 기능을 Aspect 라는 독특한 모듈 형태로 만들어서 설계하고 개발하는 방법이라고 할 수 있다.
다시 Interceptor로 돌아와서, NestJS에서는 Interceptor를 통해 다음의 기능들을 구현할 수 있게 된다.
- 메소드 실행 전/후에 추가적인 로직 바인딩
- 함수에서 반환된 결과 변환
- 함수에서 발생한 예외 변환
- 기본 기능 동작 확장
- 특정 조건(ex. 캐싱 목적)에 따라 함수를 완전히 재정의
다음은 NestJS 공식문서에서 제공해주는 LoggingInterceptor의 예제 코드이다.
// 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> {
// Controller 실행 이전
console.log('Before...');
const now = Date.now();
// Controller 실행 이후
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
인터셉터(Interceptor)는 @Injectable( ) 데코레이터가 붙는 의존성 주입이 가능한 클래스이며 NestInterceptor 인터페이스를 implements(구현)해야 한다. 그리고 인터셉터는 Controller 실행 이전과 이후 시점 모두에서 실행 가능하다.
인터셉터의 실행 시점에 대해 좀더 정확히 알기 위해서는 NestJS 공식 문서에 나와있는 Request lifecycle을 보는 것이 도움이 된다.
- 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
위와 같이 pre-controller 부분에서 interceptors가 작동하며 Controller 실행 이후 post-request 부분에서도 interceptor가 작동하는 것을 확인할 수 있다. 다시 말해, 인터셉터를 사용하면 Controller가 시작하기 전과 Controller가 끝난 후의 시점을 나눠서 코딩하는 것이 가능해진다.
Controller 실행 이전 부분은 주로 middleware에서 처리하고 인터셉터의 경우 대부분 Controller가 return 한 데이터를 받아서 가공할 때 주로 사용한다. 다음과 같이 @UseInterceptors( ) 데코레이터를 사용해 Controller에 붙여서 적용할 수 있다.
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
'Node > NestJS' 카테고리의 다른 글
NestJS - MongoDB 연결하기 & 환경 변수 설정 (0) | 2023.04.08 |
---|---|
NestJS - Exception filter 설정 (0) | 2023.03.16 |
NestJS - LoggerMiddleware 설정 (0) | 2023.03.13 |
NestJS - 캡슐화(Encapsulation) & Modules (0) | 2023.03.10 |
NestJS - 의존성 주입(DI) & Providers (0) | 2023.03.10 |