์ธํฐ์ ํฐ
์ธํฐ์ ํฐ (interceptor)๋ @Injectable() ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ๋ถ์ ํด๋์ค์ด๋ฉฐ, NestInterceptor ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํฉ๋๋ค.
์ธํฐ์ ํฐ๋ ๊ด์ ์งํฅ ํ๋ก๊ทธ๋๋ฐ (AOP) ๊ธฐ๋ฒ์์ ์๊ฐ์ ๋ฐ์ ์ ์ฉํ ๊ธฐ๋ฅ๋ค์ ์ ๊ณตํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ค์๊ณผ ๊ฐ์ ์์ ์ด ๊ฐ๋ฅํฉ๋๋ค:
- ๋ฉ์๋ ์คํ ์ /ํ์ ์ถ๊ฐ ๋ก์ง์ ๋ฐ์ธ๋ฉ
- ํจ์์์ ๋ฐํ๋ ๊ฒฐ๊ณผ๋ฅผ ๋ณํ
- ํจ์์์ ๋์ง ์์ธ๋ฅผ ๋ณํ
- ๊ธฐ๋ณธ ํจ์ ๋์์ ํ์ฅ
- ํน์ ์กฐ๊ฑด์ ๋ฐ๋ผ ํจ์ ์์ฒด๋ฅผ ์์ ํ ๋์ฒด (์: ์บ์ฑ ๋ชฉ์ )
๊ธฐ๋ณธ ์ฌํญ
๊ฐ ์ธํฐ์ ํฐ๋ intercept() ๋ฉ์๋๋ฅผ ๊ตฌํํ๋ฉฐ, ์ด ๋ฉ์๋๋ ๋ ๊ฐ์ ์ธ์๋ฅผ ๋ฐ์ต๋๋ค. ์ฒซ ๋ฒ์งธ ์ธ์๋ ExecutionContext ์ธ์คํด์ค์ด๋ฉฐ, ์ด๋ ๊ฐ๋์์ ์ฌ์ฉํ๋ ๊ฒ๊ณผ ๋์ผํ ๊ฐ์ฒด์ ๋๋ค. ExecutionContext๋ ArgumentsHost๋ฅผ ์์๋ฐ์ต๋๋ค. ArgumentsHost๋ ์์ ์์ธ ํํฐ ์ฑํฐ์์ ์ดํด๋ณธ ๋ฐ์ ๊ฐ์ด, ์๋ ํธ๋ค๋ฌ์ ์ ๋ฌ๋ ์ธ์๋ค์ ๊ฐ์ธ๋ ๋ํผ ๊ฐ์ฒด์ด๋ฉฐ, ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํ์ ๋ฐ๋ผ ๋ค์ํ ์ธ์ ๋ฐฐ์ด์ ํฌํจํฉ๋๋ค. ์ด ์ฃผ์ ์ ๋ํด์๋ ์์ธ ํํฐ ์น์ ์ ๋ค์ ์ฐธ๊ณ ํ ์ ์์ต๋๋ค.
์คํ ์ปจํ ์คํธ
ExecutionContext๋ ArgumentsHost๋ฅผ ์์ํจ์ผ๋ก์จ ํ์ฌ ์คํ ๊ณผ์ ์ ๋ํ ์ถ๊ฐ์ ์ธ ์ธ๋ถ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ์ฌ๋ฌ ๋ณด์กฐ ๋ฉ์๋๋ค๋ ํจ๊ป ์ ๊ณตํฉ๋๋ค. ์ด๋ฌํ ์ ๋ณด๋ค์ ๋ณด๋ค ์ผ๋ฐ์ ์ธ ์ธํฐ์ ํฐ๋ฅผ ๊ตฌ์ถํ ๋ ์ ์ฉํ๋ฉฐ, ๋ค์ํ ์ปจํธ๋กค๋ฌ, ๋ฉ์๋, ์คํ ์ปจํ ์คํธ์์ ๋์ํ ์ ์๋๋ก ๋์์ค๋๋ค. ExecutionContext์ ๋ํด ๋ ์์ธํ ์์๋ณด๋ ค๋ฉด ์ฌ๊ธฐ๋ฅผ ์ฐธ๊ณ ํ์ธ์.
ํธ๋ค๋ฌ ํธ์ถ
๋ ๋ฒ์งธ ์ธ์๋ CallHandler์ ๋๋ค. CallHandler ์ธํฐํ์ด์ค๋ handle() ๋ฉ์๋๋ฅผ ๊ตฌํํ๊ณ ์์ผ๋ฉฐ, ์ด ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ธํฐ์ ํฐ ๋ด๋ถ์์ ๋ผ์ฐํธ ํธ๋ค๋ฌ ๋ฉ์๋๋ฅผ ํธ์ถํ ์ ์์ต๋๋ค. ๋ง์ฝ intercept() ๋ฉ์๋ ๊ตฌํ ๋ด์์ handle() ๋ฉ์๋๋ฅผ ํธ์ถํ์ง ์์ผ๋ฉด, ํด๋น ๋ผ์ฐํธ ํธ๋ค๋ฌ ๋ฉ์๋๋ ์ ํ ์คํ๋์ง ์์ต๋๋ค.
์ด๋ฌํ ๊ตฌ์กฐ ๋๋ถ์ intercept() ๋ฉ์๋๋ ์์ฒญ/์๋ต ์คํธ๋ฆผ์ ํจ๊ณผ์ ์ผ๋ก ๊ฐ์ธ๋ (wrapper) ์ญํ ์ ํ๊ฒ ๋ฉ๋๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก, ์ต์ข ๋ผ์ฐํธ ํธ๋ค๋ฌ๊ฐ ์คํ๋๊ธฐ ์ ๊ณผ ํ ๋ชจ๋์ ์ฌ์ฉ์ ์ ์ ๋ก์ง์ ๊ตฌํํ ์ ์์ต๋๋ค. handle() ํธ์ถ ์ ์ ์คํ๋๋ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ ๋ถ๋ช ํ์ง๋ง, ๊ทธ ์ดํ์ ์ด๋ค ์์ ์ ์ํํ ์ ์์๊น์? ์ด๋ handle()์ด Observable์ ๋ฐํํ๊ธฐ ๋๋ฌธ์, RxJS์ ๊ฐ๋ ฅํ ์ฐ์ฐ์๋ค์ ํ์ฉํ์ฌ ์๋ต์ ์ถ๊ฐ๋ก ์กฐ์ํ ์ ์์ต๋๋ค. AOP (๊ด์ ์งํฅ ํ๋ก๊ทธ๋๋ฐ) ์ฉ์ด๋ก, ๋ผ์ฐํธ ํธ๋ค๋ฌ๋ฅผ ํธ์ถํ๋ ์์ (์ฆ, handle() ํธ์ถ)์ ํฌ์ธํธ์ปท (Pointcut)์ด๋ผ๊ณ ํ๋ฉฐ, ์ด๋ ์ฐ๋ฆฌ๊ฐ ์ถ๊ฐ ๋ก์ง์ ์ฝ์ ํ ์ ์๋ ์ง์ ์ ๋ํ๋ ๋๋ค.
์๋ฅผ ๋ค์ด, POST /cats ์์ฒญ์ด ์๋ค๊ณ ๊ฐ์ ํฉ์๋ค. ์ด ์์ฒญ์ CatsController์ ์ ์๋ create() ํธ๋ค๋ฌ๋ฅผ ๋์์ผ๋ก ํฉ๋๋ค. ๋ง์ฝ ๋์ค์ handle()์ ํธ์ถํ์ง ์๋ ์ธํฐ์ ํฐ๊ฐ ์คํ๋๋ฉด, create() ๋ฉ์๋๋ ์ ํ ์คํ๋์ง ์์ต๋๋ค. ๊ทธ๋ฌ๋ handle()์ด ํธ์ถ๋๊ณ ๊ทธ Observable์ด ๋ฐํ๋๋ฉด, create() ํธ๋ค๋ฌ๊ฐ ์คํ๋๋ฉฐ, ๊ทธ ์๋ต ์คํธ๋ฆผ์ ํตํด ์ถ๊ฐ์ ์ธ ์ฒ๋ฆฌ๋ฅผ ์ํํ ํ ์ต์ข ๊ฒฐ๊ณผ๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐํํ ์ ์์ต๋๋ค.
๊ด์ ๊ฐ๋ก์ฑ๊ธฐ (Aspect interception)
์ฐ๋ฆฌ๊ฐ ์ดํด๋ณผ ์ฒซ ๋ฒ์งธ ์ฌ์ฉ ์ฌ๋ก๋ ์ธํฐ์ ํฐ๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์ ์ํธ์์ฉ์ ๋ก๊น ํ๋ ๊ฒ์ ๋๋ค (์: ์ฌ์ฉ์ ํธ์ถ ์ ์ฅ, ๋น๋๊ธฐ ์ด๋ฒคํธ ๋์คํจ์น, ํ์์คํฌํ ๊ณ์ฐ ๋ฑ). ์๋๋ ๊ฐ๋จํ LoggingInterceptor ์์์ ๋๋ค:
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`)),
);
}
}
ํํธ
NestInterceptor<T, R>๋ ์ ๋ค๋ฆญ ์ธํฐํ์ด์ค๋ก, ์ฌ๊ธฐ์ T๋ Observable<T>์ ํ์ (์๋ต ์คํธ๋ฆผ์ ์ง์ํจ)์ ๋ํ๋ด๋ฉฐ, R์ Observable<R>์ ๊ฐ์ธ์ง ๊ฐ์ ํ์ ์ ์๋ฏธํฉ๋๋ค.
์๋ฆผ
์ธํฐ์ ํฐ๋ ์ปจํธ๋กค๋ฌ, ํ๋ก๋ฐ์ด๋, ๊ฐ๋ ๋ฑ๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ์์ฑ์๋ฅผ ํตํด ์์กด์ฑ์ ์ฃผ์ ๋ฐ์ ์ ์์ต๋๋ค.
handle()์ RxJS Observable์ ๋ฐํํ๋ฏ๋ก, ์ฐ๋ฆฌ๋ ์คํธ๋ฆผ์ ์กฐ์ํ๊ธฐ ์ํด ๋ค์ํ ์ฐ์ฐ์๋ค์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ ์์ ์์๋ tap() ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ๋๋ฐ, ์ด ์ฐ์ฐ์๋ Observable ์คํธ๋ฆผ์ด ์ ์์ ์ผ๋ก ์ข ๋ฃ๋๊ฑฐ๋ ์์ธ์ ์ผ๋ก ์ข ๋ฃ๋ ๋ ์ต๋ช ๋ก๊น ํจ์๋ฅผ ํธ์ถํ์ง๋ง, ๊ทธ ์ธ์๋ ์๋ต ์ฌ์ดํด์ ๊ฐ์ญํ์ง ์์ต๋๋ค.
์ธํฐ์ ํฐ ์ฐ๊ฒฐํ๊ธฐ
์ธํฐ์ ํฐ๋ฅผ ์ค์ ํ๋ ค๋ฉด @nestjs/common ํจํค์ง์์ ๊ฐ์ ธ์จ @UseInterceptors() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํฉ๋๋ค. ํ์ดํ๋ ๊ฐ๋์ฒ๋ผ ์ธํฐ์ ํฐ๋ ์ปจํธ๋กค๋ฌ ๋ฒ์, ๋ฉ์๋ ๋ฒ์, ๋๋ ์ ์ญ ๋ฒ์๋ก ์ค์ ํ ์ ์์ต๋๋ค.
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
ํํธ
@UseInterceptors() ๋ฐ์ฝ๋ ์ดํฐ๋ @nestjs/common ํจํค์ง์์ ๊ฐ์ ธ์ต๋๋ค.
์์ ๊ฐ์ ๊ตฌ์ฑ ๋ฐฉ์์ผ๋ก ์ค์ ํ๋ฉด, CatsController์ ์ ์๋ ๊ฐ ๋ผ์ฐํธ ํธ๋ค๋ฌ๋ LoggingInterceptors๋ฅผ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค. ๋๊ตฐ๊ฐ GET /cats ์๋ํฌ์ธํธ๋ฅผ ํธ์ถํ๋ฉด, ํ์ค ์ถ๋ ฅ์ ๋ค์๊ณผ ๊ฐ์ ์ถ๋ ฅ์ด ๋ํ๋ฉ๋๋ค:
Before...
After... 1ms
์ฌ๊ธฐ์ ์ฐ๋ฆฌ๋ LoggingInterceptor ํด๋์ค๋ฅผ ์ ๋ฌํ์ต๋๋ค (์ธ์คํด์ค๊ฐ ์๋). ์ด๋ ๊ฒ ํ๋ฉด ์ธ์คํด์คํ ์ฑ ์์ ํ๋ ์์ํฌ์ ์์ํ๊ฒ ๋์ด ์์กด์ฑ ์ฃผ์ ์ด ๊ฐ๋ฅํด์ง๋๋ค. ํ์ดํ (pipes), ๊ฐ๋ (guards), ์์ธ ํํฐ (exception filters)์ ๋ง์ฐฌ๊ฐ์ง๋ก, ์ธ๋ผ์ธ (in-place) ์ธ์คํด์ค๋ฅผ ์ ๋ฌํ ์๋ ์์ต๋๋ค.
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
์์ ์ธ๊ธํ๋ฏ์ด, ์์ ๊ตฌ์ฑ์ ํด๋น ์ปจํธ๋กค๋ฌ์ ์ ์ธ๋ ๋ชจ๋ ํธ๋ค๋ฌ์ ์ธํฐ์ ํฐ๋ฅผ ์ ์ฉํฉ๋๋ค. ์ธํฐ์ ํฐ์ ๋ฒ์๋ฅผ ๋จ์ผ ๋ฉ์๋๋ก ์ ํํ๊ณ ์ถ๋ค๋ฉด, ๋จ์ํ ํด๋น ๋ฉ์๋์ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ ์ฉํ๋ฉด ๋ฉ๋๋ค.
์ ์ญ ์ธํฐ์ ํฐ (global interceptor)๋ฅผ ์ค์ ํ๋ ค๋ฉด, Nest ์ ํ๋ฆฌ์ผ์ด์ ์ธ์คํด์ค์ useGlobalInterceptors() ๋ฉ์๋๋ฅผ ์ฌ์ฉํฉ๋๋ค:
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
์ ์ญ ์ธํฐ์ ํฐ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฒด, ์ฆ ๋ชจ๋ ์ปจํธ๋กค๋ฌ์ ๋ชจ๋ ๋ผ์ฐํธ์ ๊ฑธ์ณ ์ฌ์ฉ๋ฉ๋๋ค. ์์กด์ฑ ์ฃผ์ ์ธก๋ฉด์์ ๋ณด๋ฉด, ์์ ์์์ฒ๋ผ useGlobalInterceptors()๋ฅผ ์ฌ์ฉํ์ฌ ๋ชจ๋ ์ธ๋ถ์์ ๋ฑ๋ก๋ ์ ์ญ ์ธํฐ์ ํฐ๋ ์์กด์ฑ์ ์ฃผ์ ๋ฐ์ ์ ์์ต๋๋ค. ์ด๋ ํด๋น ๋ฐ์ธ๋ฉ์ด ์ด๋ค ๋ชจ๋์ ์ปจํ ์คํธ ๋ฐ์์ ์ํ๋๊ธฐ ๋๋ฌธ์ ๋๋ค. ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ค๋ฉด, ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์ฑ ๋ฐฉ์์ผ๋ก ์ด๋ค ๋ชจ๋ ๋ด๋ถ์์ ์ธํฐ์ ํฐ๋ฅผ ์ง์ ์ค์ ํ ์ ์์ต๋๋ค:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
ํํธ
์ด ๋ฐฉ๋ฒ์ ์ฌ์ฉํ์ฌ ์ธํฐ์ ํฐ์ ์์กด์ฑ ์ฃผ์ ์ ์ํํ ๋, ์ด ๊ตฌ์ฑ์ด ์ฌ์ฉ๋ ๋ชจ๋์ด ์ด๋๋ ์ง ๊ฐ์ ํด๋น ์ธํฐ์ ํฐ๋ ์ค์ ๋ก ์ ์ญ (Global)์ผ๋ก ์ ์ฉ๋๋ค๋ ์ ์ ์ ์ํด์ผ ํฉ๋๋ค. ์ด ์์ ์ ์ธํฐ์ ํฐ (์ ์์์์๋ LoggingInterceptor)๊ฐ ์ ์๋ ๋ชจ๋์์ ์ํํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ๋ํ useClass๋ง์ด ์ปค์คํ ํ๋ก๋ฐ์ด๋๋ฅผ ๋ฑ๋กํ๋ ์ ์ผํ ๋ฐฉ๋ฒ์ ์๋๋๋ค. ๋ ๋ค์ํ ๋ฐฉ๋ฒ์ ์ฌ๊ธฐ์์ ํ์ธํ ์ ์์ต๋๋ค.
์๋ต ๋งคํ
handle() ๋ฉ์๋๋ Observable์ ๋ฐํํ๋ค๋ ๊ฒ์ ์ฐ๋ฆฌ๋ ์ด๋ฏธ ์๊ณ ์์ต๋๋ค. ์ด ์คํธ๋ฆผ์ ๋ผ์ฐํธ ํธ๋ค๋ฌ๊ฐ ๋ฐํํ ๊ฐ์ ํฌํจํ๋ฏ๋ก, RxJS์ map() ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ์ฌ ์ฝ๊ฒ ๋ณํํ ์ ์์ต๋๋ค.
์ฃผ์
์๋ต ๋งคํ ๊ธฐ๋ฅ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ณ ์ ์ ์๋ต ์ ๋ต๊ณผ ํจ๊ป ์ฌ์ฉํ ์ ์์ต๋๋ค. (@Res() ๊ฐ์ฒด๋ฅผ ์ง์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ธ์ง๋ฉ๋๋ค.)
TransformInterceptor๋ฅผ ์์ฑํด ๋ด ์๋ค. ์ด ์ธํฐ์ ํฐ๋ ๊ฐ ์๋ต์ ๋จ์ํ๊ฒ ์์ ํ์ฌ ๋์ ๊ณผ์ ์ ๋ณด์ฌ์ค ๊ฒ์ ๋๋ค. RxJS์ map() ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ์ฌ ์๋ต ๊ฐ์ฒด๋ฅผ ์๋ก ์์ฑ๋ ๊ฐ์ฒด์ data ์์ฑ์ ํ ๋นํ๊ณ , ์ด ์ ๊ฐ์ฒด๋ฅผ ํด๋ผ์ด์ธํธ์ ๋ฐํํฉ๋๋ค.
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 })));
}
}
ํํธ
Nest์ ์ธํฐ์ ํฐ๋ intercept() ๋ฉ์๋๊ฐ ๋๊ธฐ์์ด๋ ๋น๋๊ธฐ์์ด๋ ๋ชจ๋ ๋์ํฉ๋๋ค. ํ์ํ๋ค๋ฉด ๋ฉ์๋๋ฅผ async๋ก ๊ฐ๋จํ ๋ณ๊ฒฝํ์ฌ ์ฌ์ฉํ ์ ์์ต๋๋ค.
์์ ๊ฐ์ด ๊ตฌ์ฑํ๋ฉด, ๋๊ตฐ๊ฐ GET /cats ์๋ํฌ์ธํธ๋ฅผ ํธ์ถํ์ ๋์ ์๋ต์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค (ํด๋น ๋ผ์ฐํธ ํธ๋ค๋ฌ๊ฐ ๋น ๋ฐฐ์ด []์ ๋ฐํํ๋ค๊ณ ๊ฐ์ ํ ๊ฒฝ์ฐ):
{
"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 ));
}
}
์์ธ ๋งคํ
๋ ๋ค๋ฅธ ํฅ๋ฏธ๋ก์ด ์ฌ์ฉ ์ฌ๋ก๋ 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())),
);
}
}
์คํธ๋ฆผ ์ฌ์ ์
๊ฒฝ์ฐ์ ๋ฐ๋ผ ํธ๋ค๋ฌ์ ํธ์ถ ์์ฒด๋ฅผ ์์ ํ ๋ง๊ณ , ๋์ ๋ค๋ฅธ ๊ฐ์ ๋ฐํํ๊ณ ์ถ์ ๋๊ฐ ์์ต๋๋ค. ๊ทธ ๋ํ์ ์ธ ์๋ก๋ ์๋ต ์๊ฐ์ ๊ฐ์ ํ๊ธฐ ์ํ ์บ์ (cache) ๊ตฌํ์ด ์์ต๋๋ค. ์ด์ ์บ์์์ ์๋ต์ ๋ฐํํ๋ ๊ฐ๋จํ ์บ์ ์ธํฐ์ ํฐ ์์ ๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ์ค์ ์ํฉ์์๋ 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();
}
}
CacheInterceptor๋ ํ์ฌ isCached ๋ณ์์ ์๋ต [] ๋ชจ๋ ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. ์ฌ๊ธฐ์ ์ค์ํ ์ ์ RxJS์ of() ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ์ฌ ์๋ก์ด ์คํธ๋ฆผ์ ๋ฐํํ๋ค๋ ๊ฒ์ ๋๋ค. ๋ฐ๋ผ์ ๋ผ์ฐํธ ํธ๋ค๋ฌ๋ ์ ํ ํธ์ถ๋์ง ์์ต๋๋ค. CacheInterceptor๋ฅผ ์ฌ์ฉํ๋ ์๋ํฌ์ธํธ๊ฐ ํธ์ถ๋๋ฉด, ํ๋์ฝ๋ฉ๋ ๋น ๋ฐฐ์ด ์๋ต์ด ์ฆ์ ๋ฐํ๋ฉ๋๋ค. ๋ณด๋ค ๋ฒ์ฉ์ ์ธ ์๋ฃจ์ ์ ๋ง๋ค๊ธฐ ์ํด์๋ Reflector๋ฅผ ํ์ฉํ๊ณ ์ปค์คํ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์์ฑํ ์ ์์ต๋๋ค. Reflector์ ๋ํ ์ค๋ช ์ Guards ์ฑํฐ์์ ์ ๋ค๋ค์ง๊ณ ์์ต๋๋ค.
๋ค๋ฅธ ์ฐ์ฐ์๋ค
RxJS ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ์ฌ ์คํธ๋ฆผ์ ์กฐ์ํ ์ ์๋ค๋ ๊ฒ์ ๋ค์ํ ๊ธฐ๋ฅ์ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค. ๋ ํ๋์ ์ผ๋ฐ์ ์ธ ์ฌ์ฉ ์ฌ๋ก๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ๋ผ์ฐํธ ์์ฒญ์ ๋ํด ํ์์์ ์ฒ๋ฆฌ๋ฅผ ํ๊ณ ์ถ๋ค๊ณ ๊ฐ์ ํด ๋ด ์๋ค. ์ผ์ ์๊ฐ์ด ์ง๋๋๋ก ์๋ํฌ์ธํธ๊ฐ ์๋ฌด๊ฒ๋ ๋ฐํํ์ง ์์ผ๋ฉด, ์๋ฌ ์๋ต์ผ๋ก ์์ฒญ์ ์ข ๋ฃํ๊ณ ์ ํ ์ ์์ต๋๋ค. ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์ฑ์ผ๋ก ์ด๋ฅผ ๊ตฌํํ ์ ์์ต๋๋ค:
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);
}),
);
};
};
5์ด๊ฐ ์ง๋๋ฉด ์์ฒญ ์ฒ๋ฆฌ๊ฐ ์ทจ์๋ฉ๋๋ค. ๋ํ RequestTimeoutException์ ๋์ง๊ธฐ ์ ์ ์ฌ์ฉ์ ์ ์ ๋ก์ง์ ์ถ๊ฐํ์ฌ (์๋ฅผ ๋ค์ด) ์์์ ํด์ ํ๋ ๋ฑ์ ์ฒ๋ฆฌ๋ฅผ ํ ์๋ ์์ต๋๋ค.
์ปค์คํ ๋ฐ์ฝ๋ ์ดํฐ
Nest๋ ๋ฐ์ฝ๋ ์ดํฐ๋ผ๋ ์ธ์ด ๊ธฐ๋ฅ์ ์ค์ฌ์ผ๋ก ๊ตฌ์ถ๋์ด ์์ต๋๋ค. ๋ฐ์ฝ๋ ์ดํฐ๋ ๋ง์ ์ฃผ์ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์์ ์ ์๋ ค์ง ๊ฐ๋ ์ด์ง๋ง, JavaScript ์ธ๊ณ์์๋ ๋น๊ต์ ์๋ก์ด ๊ธฐ๋ฅ์ ๋๋ค. ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ์ด๋ป๊ฒ ์๋ํ๋์ง ๋ ์ ์ดํดํ๊ธฐ ์ํด์๋ ๊ด๋ จ ๋ฌธ์๋ฅผ ์ฝ์ด๋ณด๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค. ๋ค์์ ๋ฐ์ฝ๋ ์ดํฐ์ ๋ํ ๊ฐ๋จํ ์ ์์ ๋๋ค:
ES2016 ๋ฐ์ฝ๋ ์ดํฐ๋ ํจ์๋ฅผ ๋ฐํํ๋ ํํ์์ผ๋ก, ๋์ (target), ์ด๋ฆ (name), ์์ฑ ์ค๋ช ์ (property descriptor)๋ฅผ ์ธ์๋ก ๋ฐ์ ์ ์์ต๋๋ค. ๋ฐ์ฝ๋ ์ดํฐ๋ @ ๊ธฐํธ๋ฅผ ์์ ๋ถ์ฌ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ ์ฉํ ๋์์ ๊ฐ์ฅ ์์ ์์น์์ผ ์ฌ์ฉํฉ๋๋ค. ๋ฐ์ฝ๋ ์ดํฐ๋ ํด๋์ค, ๋ฉ์๋, ์์ฑ ์ค ์ด๋ ๊ณณ์๋ ์ ์๋ ์ ์์ต๋๋ค.
๋งค๊ฐ๋ณ์ ๋ฐ์ฝ๋ ์ดํฐ
Nest๋ HTTP ๋ผ์ฐํธ ํธ๋ค๋ฌ์ ํจ๊ป ์ฌ์ฉํ ์ ์๋ ์ ์ฉํ ๋งค๊ฐ๋ณ์ ๋ฐ์ฝ๋ ์ดํฐ๋ค์ ์ ๊ณตํฉ๋๋ค. ์๋๋ ์ ๊ณต๋๋ ๋ฐ์ฝ๋ ์ดํฐ ๋ชฉ๋ก๊ณผ ๊ทธ๊ฒ์ด ๋ํ๋ด๋ ์์ Express (๋๋ Fastify) ๊ฐ์ฒด๋ค์ ๋๋ค.
@Request(), @Req() | req |
@Response(), @Res() | res |
@Next() | next |
@Session() | req.session |
@Param(key?: string) | req.params / req.params[key] |
@Body(key?: string) | req.body / req.body[key] |
@Query(key?: string) | req.query / req.query[key] |
@Headers(key?: string) | req.headers / req.headers[name] |
@Ip() | req.ip |
@HostParam() | req.hosts |
๋ํ, ์์ ๋ง์ ์ปค์คํ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์์ฑํ ์๋ ์์ต๋๋ค. ์ด๊ฒ์ด ์ ์ ์ฉํ ๊น์?
Node.js ์ธ๊ณ์์๋ ์์ฒญ ๊ฐ์ฒด์ ์์ฑ์ ์ถ๊ฐํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ธ ๊ด๋ก์ ๋๋ค. ๊ทธ๋ฌ๊ณ ๋์ ๊ฐ ๋ผ์ฐํธ ํธ๋ค๋ฌ์์ ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ก ์ด๋ฅผ ์๋์ผ๋ก ์ถ์ถํฉ๋๋ค.
const user = req.user;
์ฝ๋๋ฅผ ๋ ์ฝ๊ธฐ ์ฝ๊ณ ๋ช ํํ๊ฒ ๋ง๋ค๊ธฐ ์ํด @User() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์์ฑํ๊ณ ์ด๋ฅผ ๋ชจ๋ ์ปจํธ๋กค๋ฌ์์ ์ฌ์ฌ์ฉํ ์ ์์ต๋๋ค.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
๊ทธ๋ฐ ๋ค์, ํ์์ ๋ฐ๋ผ ์ธ์ ๋ ์ง ๊ฐ๋จํ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค.
@Get()
async findOne(@User() user: UserEntity) {
console.log(user);
}
๋ฐ์ดํฐ ์ ๋ฌ
๋ฐ์ฝ๋ ์ดํฐ์ ๋์์ด ํน์ ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ฌ๋ผ์ ธ์ผ ํ ๊ฒฝ์ฐ, data ๋งค๊ฐ๋ณ์๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ฝ๋ ์ดํฐ ํฉํ ๋ฆฌ ํจ์์ ์ธ์๋ฅผ ์ ๋ฌํ ์ ์์ต๋๋ค. ํ ๊ฐ์ง ์ฌ์ฉ ์๋ก๋, ์์ฒญ ๊ฐ์ฒด์์ ํค๋ฅผ ๊ธฐ์ค์ผ๋ก ์์ฑ์ ์ถ์ถํ๋ ์ปค์คํ ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ์ธ์ฆ ๊ณ์ฒญ์ด ์์ฒญ์ ๊ฒ์ฆํ๊ณ ์ฌ์ฉ์ ์ํฐํฐ๋ฅผ ์์ฒญ ๊ฐ์ฒด์ ์ถ๊ฐํ๋ค๊ณ ๊ฐ์ ํด ๋ด ์๋ค. ์ธ์ฆ๋ ์์ฒญ์ ์ฌ์ฉ์ ์ํฐํฐ๋ ๋ค์๊ณผ ๊ฐ์ ์ ์์ต๋๋ค:
{
"id": 101,
"firstName": "Alan",
"lastName": "Turing",
"email": "alan@email.com",
"roles": ["admin"]
}
์์ฑ ์ด๋ฆ์ ํค๋ก ๋ฐ์ ํด๋น ๊ฐ์ด ์กด์ฌํ๋ฉด ๋ฐํํ๊ณ , ์กด์ฌํ์ง ์๊ฑฐ๋ user ๊ฐ์ฒด๊ฐ ์์ฑ๋์ง ์์ ๊ฒฝ์ฐ์๋ undefined๋ฅผ ๋ฐํํ๋ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ ์ํด ๋ด ์๋ค.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
๋ค์์ ์ปจํธ๋กค๋ฌ์์ @User() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ํตํด ํน์ ์์ฑ์ ์ ๊ทผํ๋ ๋ฐฉ๋ฒ์ ๋๋ค:
@Get()
async findOne(@User('firstName') firstName: string) {
console.log(`Hello ${firstName}`);
}
์ด ๋์ผํ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ค๋ฅธ ํค์ ํจ๊ป ์ฌ์ฉํ์ฌ ์๋ก ๋ค๋ฅธ ์์ฑ์ ์ ๊ทผํ ์ ์์ต๋๋ค. user ๊ฐ์ฒด๊ฐ ๊น๊ฑฐ๋ ๋ณต์กํ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๊ณ ์๋ ๊ฒฝ์ฐ, ์ด๋ฅผ ํตํด ์์ฒญ ํธ๋ค๋ฌ ๊ตฌํ์ ๋ ์ฝ๊ณ ์ฝ๊ธฐ ์ข๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.
ํํธ
TypeScript ์ฌ์ฉ์๋ผ๋ฉด createParamDecorator<T>()๋ ์ ๋ค๋ฆญ์ด๋ผ๋ ์ ์ ์ ์ํ์ธ์. ์๋ฅผ ๋ค์ด createParamDecorator<string>((data, ctx) => ...)์ ๊ฐ์ด ๋ช ์์ ์ผ๋ก ํ์ ์์ ์ฑ์ ๋ณด์ฅํ ์ ์์ต๋๋ค. ๋๋ ํฉํ ๋ฆฌ ํจ์ ๋ด๋ถ์์ createParamDecorator((data: string, ctx) => ...)์ฒ๋ผ ํ๋ผ๋ฏธํฐ ํ์ ์ ์ง์ ํ ์๋ ์์ต๋๋ค. ๋ ๋ค ์๋ตํ๋ฉด data์ ํ์ ์ any๊ฐ ๋ฉ๋๋ค.
ํ์ดํ์ ํจ๊ป ์ฌ์ฉํ๊ธฐ
Nest๋ ์ปค์คํ ๋งค๊ฐ๋ณ์ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๊ธฐ๋ณธ ์ ๊ณต ๋ฐ์ฝ๋ ์ดํฐ๋ค (@Body(), @Param(), @Query())๊ณผ ๋์ผํ๊ฒ ์ฒ๋ฆฌํฉ๋๋ค. ์ด๋ ์ปค์คํ ๋ฐ์ฝ๋ ์ดํฐ๋ก ์ฃผ์ ์ฒ๋ฆฌ๋ ํ๋ผ๋ฏธํฐ์๋ ํ์ดํ๊ฐ ์คํ๋๋ค๋ ๋ป์ ๋๋ค (์์ ์์๋ user ์ธ์). ๋ํ ํ์ดํ๋ฅผ ์ปค์คํ ๋ฐ์ฝ๋ ์ดํฐ์ ์ง์ ์ ์ฉํ ์๋ ์์ต๋๋ค.
@Get()
async findOne(
@User(new ValidationPipe({ validateCustomDecorators: true }))
user: UserEntity,
) {
console.log(user);
}
ํํธ
validateCustomDecorators ์ต์ ์ด true๋ก ์ค์ ๋์ด ์์ด์ผ ํฉ๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ValidationPipe๋ ์ปค์คํ ๋ฐ์ฝ๋ ์ดํฐ๋ก ์ฃผ์ ์ฒ๋ฆฌ๋ ์ธ์๋ฅผ ๊ฒ์ฆํ์ง ์์ต๋๋ค.
๋ฐ์ฝ๋ ์ดํฐ ํฉ์ฑ
Nest๋ ์ฌ๋ฌ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ํ๋๋ก ๊ฒฐํฉํ ์ ์๋ ํฌํผ ๋ฉ์๋๋ฅผ ์ ๊ณตํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ์ธ์ฆ๊ณผ ๊ด๋ จ๋ ๋ชจ๋ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ํ๋์ ๋ฐ์ฝ๋ ์ดํฐ๋ก ํตํฉํ๊ณ ์ถ๋ค๊ณ ๊ฐ์ ํด ๋ด ์๋ค. ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์ฑ์ผ๋ก ์ด๋ฅผ ๊ตฌํํ ์ ์์ต๋๋ค:
import { applyDecorators } from '@nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized' }),
);
}
๊ทธ๋ฐ ๋ค์, ์ด ์ปค์คํ @Auth() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉํ ์ ์์ต๋๋ค:
@Get('users')
@Auth('admin')
findAllUsers() {}
์ด๋ ๊ฒ ํ๋ฉด ๋ค ๊ฐ์ ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ํ๋์ ์ ์ธ์ผ๋ก ๋ชจ๋ ์ ์ฉ๋๋ ํจ๊ณผ๋ฅผ ์ป์ ์ ์์ต๋๋ค.
๊ฒฝ๊ณ
@nestjs/swagger ํจํค์ง์ @ApiHideProperty() ๋ฐ์ฝ๋ ์ดํฐ๋ ํฉ์ฑ์ด ๋ถ๊ฐ๋ฅํ๋ฉฐ, applyDecorators ํจ์์ ํจ๊ป ์ฌ์ฉํ ๊ฒฝ์ฐ ์ ๋๋ก ์๋ํ์ง ์์ต๋๋ค.
Reference
'๐ ๊ณต์ ๋ฌธ์ ๋ฒ์ญ > Nest.js' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Nest.js] Fundamentals - Dynamic modules, Injection scopes (0) | 2025.07.16 |
---|---|
[Nest.js] Fundamentals - Custom providers, Asynchronous providers (0) | 2025.07.15 |
[Nest.js] Overview - Pipes, Guards (0) | 2025.07.08 |
[Nest.js] Overview - Middleware, Exception filters (2) | 2025.07.07 |
[Nest.js] Overview - Providers, Modules (0) | 2025.07.05 |