공식 문서: https://docs.nestjs.com
NestJS는 Decorator, DI(Dependency Injection), IoC(Inversion of Control), AOP(Aspect Oriented Programming) 를 중심으로 설계된 Node.js 백엔드 프레임워크입니다.
| 개념 | Nest에서의 역할 |
|---|---|
| Decorator | 클래스·메서드·파라미터에 메타데이터를 붙여 프레임워크가 구조를 읽게 합니다 (@Controller, @Injectable, @Roles 등) |
| IoC | 객체 생성·연결을 개발자가 아니라 Nest 컨테이너가 담당합니다 (new Service() 직접 호출 ❌) |
| DI | IoC 컨테이너가 constructor에 필요한 의존성을 주입합니다 |
| AOP | 로깅·인증·검증·에러 처리 같은 횡단 관심사를 handler 밖(Guard, Interceptor, Pipe, Filter)으로 분리합니다 |
| Module | DI 범위와 캡슐화 단위 — 어떤 Provider를 어디서 쓸 수 있는지 경계를 설정합니다 |
Nest는 HTTP 요청 하나를 아래 pipeline으로 처리합니다. Middleware~Pipe까지가 AOP layer이며, handler 본연의 비즈니스 로직과 분리된 횡단 관심사입니다.
요청
→ Middleware ← Express chain (전역 전처리)
→ Guard ← 진입 허가 (authorization)
→ Interceptor ← handler 앞뒤 감싸기 (AOP 핵심)
→ Pipe ← handler 인자 변환·검증
→ Controller ← HTTP handler (얇게 유지)
→ (에러 시) Exception Filter
| Layer | Interface | 핵심 메서드 | 등록 |
|---|---|---|---|
| Middleware | NestMiddleware (Class) | use(req, res, next) | NestModule.configure() |
| Guard | CanActivate | canActivate() | @UseGuards() |
| Interceptor | NestInterceptor | intercept() → next.handle() | @UseInterceptors() |
| Pipe | PipeTransform | transform() | @Param() / @Body() 옆, @UsePipes() |
| Exception Filter | ExceptionFilter | catch() | @UseFilters() |
| Controller | - (@Controller()) | handler return | @Module({ controllers }) |
순서가 중요한 이유
@Injectable()은 "이 클래스를 Nest IoC 컨테이너가 관리하는 Provider로 등록한다"는 메타데이터입니다.
// ❌ 직접 생성 — IoC 우회
const service = new CatsService();
// ✅ DI — Nest가 생성·주입
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
}
부트스트랩 시점
@Injectable() / providers 등록 / constructor 선언은 등록일 뿐입니다. 실제 인스턴스 생성·주입은 NestFactory.create(AppModule) 부트스트랩 시점에 일어납니다.
1. NestFactory.create(AppModule)
2. @Module 메타데이터 읽기 → 의존성 그래프 구성
3. CatsService 인스턴스 생성 (기본 Singleton)
4. CatsController 생성 → constructor에 CatsService 주입
5. 라우트 등록 완료
@UseGuards(), @UseInterceptors(), @UseFilters() 등록 시 Class를 넘기면 Nest가 인스턴스를 생성하고 constructor 의존성을 resolve합니다.
@UseGuards(RolesGuard) // ✅ Nest가 생성 + AuthService 등 DI
@UseGuards(new RolesGuard()) // ❌ Nest DI 컨테이너 밖에서 생성 → DI 불가
Global 등록 + DI가 필요하면 main.ts의 useGlobalX() 대신 APP_* token으로 Module에 등록합니다.
| Global token | 대상 |
|---|---|
| APP_GUARD | Guard |
| APP_INTERCEPTOR | Interceptor |
| APP_PIPE | Pipe |
| APP_FILTER | Exception Filter |
@Module({
providers: [
{ provide: APP_PIPE, useClass: ValidationPipe },
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
],
})
export class AppModule {}
Pipe/ValidationPipe는 runtime에 DTO의 metatype(class 타입 정보)을 읽어 class-validator 데코레이터를 적용합니다. TypeScript interface는 컴파일 후 사라지므로 ❌, class만 ✅입니다.
이것은 Nest Decorator + Reflection 패턴의 대표적 예시입니다 — compile-time 데코레이터가 runtime 검증으로 이어집니다.
https://docs.nestjs.com/first-steps
npm i -g @nestjs/cli
nest new project-name
# 더 엄격한 TS: nest new project-name --strict
npm run start # 일반 실행
npm run start:dev # watch — 파일 변경 시 자동 재컴파일·재시작
npm run lint # ESLint — await 누락, any 남용 등
npm run format # Prettier — 들여쓰기, 줄바꿈, 따옴표
main.ts — 앱 부트스트랩
async function bootstrap() {
const app = await NestFactory.create(AppModule); // IoC 컨테이너 초기화
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
NestFactory.create()가 Module graph를 읽고, Provider·Controller·Guard 등을 한 번에 wiring합니다.
https://docs.nestjs.com/controllers
Controller는 HTTP 요청을 받고 응답을 보내는 진입점입니다. 비즈니스 로직은 Service(Provider)에 위임하고, Controller는 얇게 유지하는 것이 DI·관심사 분리 원칙에 맞습니다.
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Get()
findAll() {
return this.catsService.findAll();
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
@Post()
create(@Body() dto: CreateCatDto) {
return this.catsService.create(dto);
}
}
@Controller('cats') + @Get() → GET /cats입니다. @Get('breed') → GET /cats/breed입니다.
값만 return하면 Nest가 응답을 처리합니다. Interceptor, Exception Filter 등 AOP layer와 정상 연동됩니다.
@Post()
@HttpCode(201)
@Header('Cache-Control', 'no-store')
create(@Body() dto: CreateCatDto) {
return this.catsService.create(dto);
}
| 항목 | 내용 |
|---|---|
| body | return 값입니다 (객체/배열 → JSON, primitive → 그대로) |
| 상태 코드 | GET → 200, POST → 201 (기본값) |
| 커스텀 | @HttpCode(), @Header(), @Redirect() |
| Nest 연동 | Interceptor, Exception Filter 등이 정상 동작합니다 |
쿠키 설정용 Nest decorator는 없습니다 → @Res({ passthrough: true })로 res.cookie()를 사용합니다.
@Res()로 Express response 객체를 직접 사용합니다. 해당 route에서 Nest Standard pipeline이 꺼집니다.
@Get()
findAll(@Res() res: Response) {
res.status(200).json([]); // API JSON
}
@Get('hello')
hello(@Res() res: Response) {
res.status(200).send('Hello'); // 단순 메시지
}
| 항목 | 내용 |
|---|---|
| body | res.json(), res.send() 등을 직접 호출합니다 |
| 상태 코드 | res.status()를 직접 설정합니다 |
| 주의 | return은 무시됩니다 — 반드시 res로 응답을 종료해야 하며, 그렇지 않으면 hang 상태가 됩니다 |
| Nest 연동 | Interceptor, @HttpCode() 등 비활성화됩니다 |
| 단점 | Express에 종속되며, 테스트·이식성이 떨어집니다 |
return으로 body는 Nest에 맡기고, res로 쿠키·헤더만 직접 설정합니다. AOP pipeline을 유지하면서 Express API를 일부 사용합니다.
@Post()
login(@Res({ passthrough: true }) res: Response) {
res.cookie('token', 'abc', { httpOnly: true });
return { ok: true };
}
| Standard | Express | passthrough | |
|---|---|---|---|
| body | return | res.send/json() | return |
| Nest AOP | ✅ | ❌ | ✅ |
| res 직접 조작 | ❌ | ✅ | 일부만 ✅ |
https://docs.nestjs.com/providers
Provider(Service, Repository 등)는 비즈니스 로직을 담습니다. Nest DI의 핵심 단위입니다.
Controller는 HTTP 진입점, Provider는 로직, Module은 DI 범위입니다.
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
findOne(id: number): Cat | undefined {
return this.cats.find((cat) => cat.id === id);
}
}
| 항목 | 내용 |
|---|---|
| @Injectable() | IoC 컨테이너 Provider 등록 메타데이터입니다 |
| Scope | 기본 Singleton — 앱 전체에서 인스턴스 1개를 공유합니다 |
| 역할 | 데이터 저장·조회, 비즈니스 로직을 담당합니다 |
nest g service cats
Node.js는 요청마다 스레드를 분리하지 않으므로, Singleton Provider는 요청 간 공유됩니다. CatsService의 cats 배열은 모든 요청이 같은 배열을 봅니다.
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
// ↑ TypeScript shorthand: 선언 + this.catsService 초기화
@Post()
create(@Body() dto: CreateCatDto) {
return this.catsService.create(dto);
}
}
Nest IoC가 CatsService 타입을 보고 인스턴스를 resolve → constructor에 주입합니다. Dependency Inversion — Controller는 CatsService 구현에 직접 의존하지 않고, Nest가 연결해 줍니다.
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
providers에 없으면 Nest가 CatsService를 모르므로 주입이 실패합니다.
https://docs.nestjs.com/modules
Module은 Nest DI 컨테이너의 캡슐화 단위입니다. 어떤 Provider를 누가 주입할 수 있는지 경계를 정합니다.
| 속성 | 역할 |
|---|---|
| providers | Nest가 생성·관리할 Provider입니다 |
| controllers | 이 Module의 Controller입니다 |
| imports | 다른 Module을 가져옵니다 (export된 Provider 사용) |
| exports | 다른 Module에 공개 API로 내보낼 Provider입니다 |
Provider는 Module 내부에서만 주입할 수 있습니다 (캡슐화). 외부 Module에서 쓰려면 exports + imports가 필요합니다.
관련 Controller + Service를 기능(domain) 단위로 묶습니다 — SRP, 응집도를 높입니다.
// cats/cats.module.ts
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
// app.module.ts
@Module({
imports: [CatsModule],
})
export class AppModule {}
src/
cats/
dto/create-cat.dto.ts
cats.controller.ts
cats.service.ts
cats.module.ts
app.module.ts
main.ts
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService], // 다른 Module에 공개
})
export class CatsModule {}
@Module({
imports: [CatsModule], // export된 CatsService 사용 가능
providers: [OrdersService],
})
export class OrdersModule {
constructor(private catsService: CatsService) {} // 같은 Singleton
}
| 방식 | 결과 |
|---|---|
| exports + imports | 같은 Singleton 인스턴스를 공유합니다 |
| 각 Module에 providers: [CatsService] | Module마다 별도 인스턴스 — 상태 불일치 위험이 있습니다 |
AppModule (Root)
└── imports: [CatsModule]
├── controllers: [CatsController]
├── providers: [CatsService]
└── exports: [CatsService]
https://docs.nestjs.com/middleware
Middleware는 Controller 실행 전 Express chain에서 동작합니다. req, res, next에 접근하며, next() 미호출 시 hang됩니다.
Middleware는 어떤 handler가 실행될지 모릅니다 — route context 밖의 전역 전처리에 적합합니다 (로깅, body parser, req.user 붙이기 등).
요청 → [Middleware] → [Guard → Interceptor → Pipe → Controller]
Functional — DI가 불필요합니다
export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`${req.method} ${req.url}`);
next();
}
Class — DI가 필요합니다 (@Injectable())
Middleware 자신이 Service를 주입받을 때 사용합니다.
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(private authService: AuthService) {}
use(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization;
if (this.authService.validate(token)) {
req.user = this.authService.decode(token); // 이후 Guard/Controller에서 사용
next();
} else {
next(); // 또는 res.status(401).send()
}
}
}
| Functional | Class | |
|---|---|---|
| DI | ❌ | ✅ |
| 용도 | 로깅 등 단순 전처리 | AuthService 등 Service가 필요할 때 사용합니다 |
Middleware는 @Module({ providers })가 아니라 configure(consumer)로 route에 연결합니다.
@Module({ imports: [CatsModule] })
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware) // Class → 요청마다 use() 실행
.forRoutes(CatsController); // Controller 단위 적용
// .forRoutes('cats'); // 경로 prefix
// .forRoutes({ path: 'cats', method: RequestMethod.GET });
}
}
// main.ts — DI 불가
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
Class middleware + DI가 필요하면 Module에서 forRoutes('*')를 사용합니다.
| Middleware | Guard | |
|---|---|---|
| context | handler를 알 수 없습니다 | ExecutionContext로 handler를 알 수 있습니다 |
| 등록 | configure(consumer) | @UseGuards() |
| 역할 | 공통 전처리 | route handler 실행을 허가합니다 |
https://docs.nestjs.com/guards
Guard는 route handler 실행 허용/거부를 결정합니다 (주로 authorization).
Middleware와 달리 Guard는 ExecutionContext로 어떤 handler·decorator metadata가 붙었는지 알 수 있습니다 — route별 조건 분기가 가능합니다 (AOP + Reflection).
요청 → Middleware → Guard → Interceptor → Pipe → Controller
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return !!request.user; // Middleware/Guard가 미리 붙인 user
}
}
| Middleware | Guard | |
|---|---|---|
| 반환 | next() 호출 | boolean (true/false) |
| 용도 | authentication (req.user 설정) | authorization (route별 권한 검사) |
| false 시 | — | 403 Forbidden, handler가 실행되지 않습니다 |
@Controller('cats')
@UseGuards(RolesGuard) // Controller 전체
export class CatsController {
@Get()
findAll() {} // RolesGuard 적용
@Post()
@UseGuards(AuthGuard) // method 추가 적용
create() {}
}
@Module({
providers: [{ provide: APP_GUARD, useClass: RolesGuard }],
})
export class AppModule {}
Decorator로 metadata를 저장 → Guard에서 Reflector로 읽습니다 — handler마다 if/else 없이 선언적으로 권한을 설정합니다 (AOP + Reflection).
// roles.decorator.ts
export const Roles = Reflector.createDecorator<string[]>();
// cats.controller.ts
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {
@Get()
findAll() {} // @Roles 없음 → public
@Post()
@Roles(['admin'])
create(@Body() dto: CreateCatDto) {
return this.catsService.create(dto);
}
}
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) return true; // metadata 없으면 역할 검사 생략
const { user } = context.switchToHttp().getRequest();
return roles.some((role) => user?.roles?.includes(role));
}
}
| 역할 | |
|---|---|
| @Roles(['admin']) | handler에 필요 역할 metadata를 저장합니다 |
| reflector.get(Roles, handler) | Guard에서 metadata를 읽습니다 |
| if (!roles) return true | @Roles 없는 route = public입니다 |
| request.user | Middleware/AuthGuard가 미리 붙인 user입니다 (관례) |
https://docs.nestjs.com/interceptors
Interceptor는 Nest AOP의 핵심 구현체입니다. handler를 next.handle()로 감싸서 실행 전·후에 로직을 끼워 넣습니다 (around advice).
요청 → ... → Interceptor ──→ next.handle() ──→ Pipe → Controller
↑ Before ↑ Pointcut ↑ handler 실행
└──── After ← Observable stream ←──┘
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
console.log(`[Before] ${req.method} ${req.url}`);
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`[After] ${Date.now() - now}ms`)),
);
}
}
| 내용 | |
|---|---|
| handle() 전 | 로깅, 전처리를 수행합니다 |
| handle() | Controller method 실행 → Observable stream |
| .pipe(tap/map/...) | stream 변환 — After 로직, 응답 mapping |
| handle() 미호출 | handler가 생략됩니다 (캐시 hit 등) |
Response mapping — Controller return [] → { data: [] }로 전역 통일합니다 (DRY, AOP).
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
intercept(context: ExecutionContext, next: CallHandler) {
return next.handle().pipe(map((data) => ({ data })));
}
}
Standard(return) 전용입니다 — @Res() 사용 route에서는 동작하지 않습니다.
Handler 생략 (캐시)
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
if (this.cache.has(key)) {
return of(cachedValue); // handle() 호출 안 함 → Controller skip
}
return next.handle();
}
}
@Controller('cats')
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
@Module({
providers: [{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor }],
})
export class AppModule {}
| Guard | Interceptor | |
|---|---|---|
| 역할 | 들어갈 수 있는지 판단합니다 (gate) | 들어간 뒤 무엇을 할지 감쌉니다 (wrap) |
| 반환 | boolean | Observable |
| handler | 허용/거부 | next.handle()로 감쌉니다 |
Pipe는 Controller method 실행 직전, handler 인자에 대해 변환·검증합니다. 시스템 경계(boundary)에서 입력을 정제하는 AOP layer입니다.
Middleware는 req 전체를 봅니다. Pipe는 @Param, @Body, @Query로 선언된 특정 인자를 알 수 있습니다 — handler signature와 연결된 검증입니다.
Pipe에서 exception이 throw되면 Controller가 실행되지 않으며, Exception Filter가 400 응답을 반환합니다.
URL/query/body는 항상 string입니다. Pipe가 runtime 타입 변환을 수행합니다.
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
// GET /cats/abc → 400
// GET /cats/1 → id: 1 (number)
| Pipe | 역할 |
|---|---|
| ParseIntPipe | string → number로 변환합니다 |
| ParseFloatPipe | string → float로 변환합니다 |
| ParseBoolPipe | string → boolean으로 변환합니다 |
| ParseUUIDPipe | UUID를 검증합니다 |
| ParseEnumPipe | enum을 검증합니다 |
| DefaultValuePipe | 값이 없을 때 기본값을 적용합니다 |
@Get()
findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}
Declarative validation — DTO class에 @IsString() 등 decorator를 붙이면 ValidationPipe가 runtime에 검증합니다.
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
@Min(0)
age: number;
@IsString()
breed: string;
}
@Post()
create(@Body() dto: CreateCatDto) {
return this.catsService.create(dto);
}
Global ValidationPipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
@Module({
providers: [{ provide: APP_PIPE, useClass: ValidationPipe }],
})
export class AppModule {}
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
// metadata.type: 'body' | 'query' | 'param' | 'custom'
// metadata.metatype: CreateCatDto (class) — interface면 undefined
return value;
}
}
| Middleware | Pipe | |
|---|---|---|
| 대상 | req, res (전체) | handler 인자 (특정 param) |
| context | handler/param을 알 수 없습니다 | @Body, @Param을 알 수 있습니다 |
| AOP 관점 | chain-level 전처리 | boundary validation/transformation |
https://docs.nestjs.com/exception-filters
Exception Filter는 에러 응답을 처리하는 AOP layer입니다. Pipe·Guard·Controller 어디서든 throw된 exception을 잡아 일관된 JSON으로 변환합니다.
| Exception | 응답 |
|---|---|
| HttpException subclass | { statusCode, message, ... } |
| unrecognized | { statusCode: 500, message: "Internal server error" } |
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
const cat = this.catsService.findOne(id);
if (!cat) {
throw new NotFoundException(`Cat #${id} not found`);
}
return cat;
}
throw new BadRequestException('Invalid input');
throw new UnauthorizedException();
throw new ForbiddenException();
Custom exception은 HttpException을 extends하면 built-in filter가 자동 처리합니다.
export class CatNotFoundException extends HttpException {
constructor(id: number) {
super(`Cat #${id} not found`, HttpStatus.NOT_FOUND);
}
}
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
| 내용 | |
|---|---|
| @Catch(HttpException) | 잡을 exception 타입입니다 (비우면 all) |
| ArgumentsHost | HTTP/Microservice/WS 등 context를 추상화합니다 |
| host.switchToHttp() | Express req, res를 반환합니다 |
@Post()
@UseFilters(HttpExceptionFilter)
create() {
throw new ForbiddenException();
}
@Module({
providers: [{ provide: APP_FILTER, useClass: HttpExceptionFilter }],
})
export class AppModule {}
| Built-in global filter | Custom filter | |
|---|---|---|
| 설정 | 자동 적용 | @UseFilters() / APP_FILTER로 등록 |
| 용도 | 기본 JSON 에러 응답 | timestamp, path, 로깅, 커스텀 schema 추가 |
https://docs.nestjs.com/custom-decorators
Nest는 Decorator + Metadata + Reflection으로 declarative programming을 구현합니다. @Body(), @Roles(), @User() 모두 같은 패턴입니다.
| Decorator | Express |
|---|---|
| @Req() | req |
| @Res() | res |
| @Param('id') | req.params.id |
| @Body() | req.body |
| @Query('age') | req.query.age |
| @Headers('authorization') | req.headers.authorization |
| @Ip() | req.ip |
export const User = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const user = ctx.switchToHttp().getRequest().user;
return data ? user?.[data] : user;
},
);
@Get('profile')
getProfile(@User() user: UserEntity) {
return user;
}
@Get('greeting')
greet(@User('firstName') firstName: string) {
return `Hello ${firstName}`;
}
Custom decorator에도 Pipe를 적용할 수 있습니다 — validateCustomDecorators: true가 필요합니다.
@Get('profile')
getProfile(
@User(new ValidationPipe({ validateCustomDecorators: true }))
user: UserEntity,
) {
return user;
}
여러 decorator를 하나로 합칩니다 (DRY).
export function Auth(...roles: string[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
);
}
@Get('users')
@Auth('admin')
findAllUsers() {}
| 패턴 | Nest 구현 |
|---|---|
| IoC / DI | @Injectable() + providers + constructor injection + NestFactory.create()로 구현합니다 |
| Module 캡슐화 | imports / exports로 Provider 가시성·Singleton 공유를 관리합니다 |
| AOP | Guard (before gate) → Interceptor (around) → Pipe (before args) → Filter (after error)로 구현합니다 |
| Decorator + Metadata | @Roles(), @Body(), DTO @IsString() — 선언적으로 설정합니다 |
| Reflection | Reflector.get(), ValidationPipe metatype — runtime에 metadata를 소비합니다 |
| 관심사 분리 | Controller (HTTP) / Service (logic) / Pipe (validation) / Filter (error)로 분리합니다 |
| Declarative > Imperative | handler마다 if/else 대신 decorator + pipeline layer에 위임합니다 |