[‘NestJS로 배우는 백엔드 프로그래밍’ 정리] Pipe, Middleware, Guard, Interceptor

2025. 10. 9. 18:21·Study/NestJS

저번에는 NestJS의 Controller, Provider, Module에 대해서 정리했었다.

https://quickchabun.tistory.com/188

 

[‘NestJS로 배우는 백엔드 프로그래밍’ 정리] Controller, Provider, Module

사실 백엔드를 공부하고자 하는 의지는 예전부터 있었다. 그래서 백엔드 강의도 구매해서 70% 넘게 들었었는데, 다른 일과 겹쳐서 완강을 못했던 기억이 난다. 그리고 다가온 추석 연휴, 교보문

quickchabun.tistory.com

 

 

이번에는 요청이 제대로 전달되었는지 유효성 검사를 하는 파이프, 요청 처리 전에 부가 기능을 수행하는 미들웨어, 권환 확인을 위한 가드, 요청과 응답을 수정하는하는 인터셉터, 예외를 처리하는 예외 필터, 그리고 이 요소들의 생명 주기에 대해 정리해보려 한다.

(한용재님의 책 ‘NestJS로 배우는 백엔드 프로그래밍’을 읽고 정리한 글입니다)


1. Pipe

파이프는 요청이 라우트 핸들러로 전달되기 전에 요청 객체를 변환할 수 있게 해준다.

(route handler: 사용자의 요청을 처리하는 엔드포인트마다 동작을 수행하는 컴포넌트)

 

ParseIntPipe, ParseBoolPipe, ParseArrayPipe, ParseUUIDPipe: 전달된 인수 타입 검사 용도

 

@Param 데코레이터의 두 번째 인수로 파이프를 넘겨 현재 실행 컨텍스트에 바인딩할 수 있다.

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
	return this.users.Service.findOne(id);
}

 

파이프 객체를 직접 생성하여 전달도 가능하다.

@Get(':id')
findOne(@Param('id', new ParseIntPipe({ errorHTTPStatusCode: 
HttpStatus.NOT_ACCEPTABLE})) id: number) {
	return this.usersService.findOne(id);
}

 

DefaultValuePipe: 인수의 값에 기본값 설정 시 사용

@Get()
findAll(
	@Query('offset' , new DefaultValuePipe(0), ParseIntPipe) offset: number,
	@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
} {
	console.log(offset, limit);
	
	(...)
}

커스텀 파이프

PipeTransform 인터페이스를 상속받은 클래스에 @Injectable 데코레이터를 붙여주면 된다.

@Injectable()
export class ValidationPipe implements PipeTransform {
	transform(value: any, metadata: ArgumentMetadata) {
		console.log(metadata);
		return value;
	}
}

 

 

PipeTransform과 ArgumentMetadata에 대해서 더 살펴보자

// value: 현재 파이프에 전달된 인수, metadata: 현재 파이프에 전달된 인수의 메타데이터
export interface PipeTransform<T = any, R = any> {
	transform(value : T, metadata: ArgumentMetadata): R;
}

// type: 파이프에 전달된 인수가 본문인지, 쿼리 매개변수인지, 경로 매개변수인지, 커스텀 매개변수인지
// metatype: 라우트 핸들러에 정의된 인수의 타입
// data: 데코레이터에 전달된 문자열 (매개변수의 이름)

export interface ArgumentMetadata {
	readonly type: Paramtype;
	readonly metatype?: Type<any> | undefined;
	readonly data?: string | undefined;
}

export declare type Paramtype = 'body' | 'query' | 'param' | 'custom';

 

 

class-validator, class-transformer 라이브러리를 활용하여 유효성 검사 파이프를 만들 수 있다. (joi보다 간편)

import { IsString, MinLength, MaxLenght, IsEmail } from 'class-validator';

export class CreateUserDto {
	@IsString()
	@MinLength(1)
	@MaxLength(20)
	name: string;
	
	@IsEmail()
	email: string;
}
	

 

 

위의 dto 객체를 받아서 유효성 검사를 하는 파이프(ValidataionPipe)를 구현한다면

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
	async transform(value: any, { metatype }: ArgumentMetadata) {
		if (!metatype || !this.toValidate(metatype)) {
			return value;
		}
		
		// class-transformer의 plainToClass 함수 -> 순수 자바스크립트 객체를 클래스 객체로 변환
		const object = plainToClass(metatype, value);
		const errors = await validate(object);
		if (errors.length > 0) {
			throw new BadRequestException('Validation  failed');
		}
		return value;
	}
	
	private toValidate(metatype: Function): boolean {
		const types: Function[] = [String, Boolean, Number, Array, Object];
		return !types.includes(metatype);
	}
}

 

 

이 ValidationPipe를 적용한다면

@Post()
create(@Body(ValidationPipe) createUserDto: CreateUserDto) {
	return this.usersService.create(createUserDto);
}

 

 

전역으로 설정하려면 부트스트랩 과정에서 적용하면 된다.

async function bootstrap() {
	const app = await NestFactory.create(AppModule);
	app.useGlobalPipes(new ValidationPIpe())
	await app.listen(3000);
}
bootstrap();

2. Middleware

라우트 핸들러가 클라이언트의 요청을 처리하기 전에 수행되는 컴포넌트

(파이프와 다르게 미들웨어는 애플리케이션의 모든 컨텍스트에서 사용 불가. 현재 요청이 어떤 핸들러에서 수행되는지, 어떤 매개변수를 가지고 있는지에 대한 실행 컨텍스트를 알지 못하기 때문에)

 

Nest의 미들웨어 = Express의 미들웨어

  • 어떤 형태의 코드라도 수행 가능
  • 요청과 응답에 변형을 가할 수 있음
  • 요청/응답 주기를 끝낼 수 있음
  • 여러 개의 미들웨어를 사용한다면 next()로 호출 스택 상 다음 미들웨어에 제어권 전달

미들웨어를 활용하여 쿠키 파싱, 세션 관리, 인증/인가, 본문 파싱을 할 수 있다. (단, 인가는 가드 사용 권장)

 

Nest에서는 미들웨어를 함수로 작성하거나 NestMiddleware 인터페이스를 구현한 클래스로 작성 가능

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
	use(req: Request, res: Response, next: NextFunction) {
		console.log('Request');
		next();
	}
}

미들웨어를 모듈에 포함시키려면 NestModule 인터페이스를 구현해야한다. NestModule에 선언된 configure 함수를 통해 미들웨어를 설정한다.

 

app.module.ts에서

@Module({
	imports: [UsersModule],
})
export class AppModule implements NestModule {
	configure(consumer: MiddlewareConsumer): any {
		consumer
			.apply(LoggerMiddleware)
			.forRoutes('/users');
		
	}
}
	

 

만약 미들웨어 2개를 적용한다면 apply에 콤마로 나열하면 된다.

→ .apply(LoggerMiddleware, Logger2Middleware)

 

미들웨어를 전역으로 적용하려면 아까 파이프에서 그랬듯이 부트스트랩 과정에서 적용하면 된다.

→ app.use(logger);


3. Guard

인증: 요청자가 자신이 누구인지 증명하는 과정

인가: 인증을 통과한 유저가 요청한 기능을 사용할 권한이 있는지 판별하는 것

 

미들웨어는 실행 컨텍스트에 접근하지 못하고, 가드는 실행 컨텍스트 인스턴스에 접근할 수 있다.

→ 가드를 활용해 인가를 구현하자

 

가드는 CanActivate 인터페이스를 구현해야

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
	canActivate(
		context: ExecutionContext,
	): boolean | Promise<boolean> | Observable<boolean> {
		const request = context.switchToHttp().getRequest();
		return this.validateRequest(request);
	}
	
	private validateRequest(request: any) {
		return true;
	}
}

 

canActivate 함수는 ExecutionContext 인스턴스를 인수로 받는다.

ExecutionContext는 요청과 응답에 대한 정보를 가지고 있는 ArgumentHost를 상속 받는다.

→ HTTP로 기능을 제공하고 있으므로 switchToHttp() 함수를 활용하여 원하는 정보를 가져올 수 있다.

 

가드를 사용하려면 @UseGuards(AuthGuard)와 같이 사용하면 된다.

@UseGuards(AuthGuard)
@Controller()
export class AppController {
	constructor(private readonly appService: AppService) { }
	
	@UseGuards(AuthGuard)
	@Get()
	getHello(): string {
		return this.appService.getHello();
	}
}

// 전역으로 적용하려면 부트스트랩 과정에서 useGlobalGuards 적용
async function bootstrap() {
	const app = await NestFactory.create(AppModule);
	app.useGlobalGuards(new AuthGuard());
	await app.listen(3000);
}
bootstrap();

 

 

가드에 종속성 주입을 사용해서 다른 프로바이더를 주입해서 사용하고 싶다면 커스텀 프로바이더로 선언해야 한다.

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
	providers: [
		{
			provide: APP_GUARD,
			useClass: AuthGuard,
		},
	],
})
export class AppModule {}

회원 가입

회원 가입 요청에 포함된 이메일에 다음 링크가 포함되어 있다고 가정하자

const url = ${baseUrl}/users/email-varify?signupVerifyToken=${signupVerifyToken};

async verifyEmail(signupVerifyToken: string): Promise<string> {
	const user = await this.usersRepository.findOne({
		where: { signupVerifyToken }
	});
	
	if (!user) {
		throw new NotFoundException('User is no exist');
	}
	
	return this.authService.login({
		id: user.id,
		name: user.name,
		email: user.email,
	});
}
		

 

 

AuthService에서 로그인 처리를 하고, 응답으로 JWT 토큰을 생성해서 돌려준다

import * as jwt from 'jsonwebtoken';
import { Inject, Injectable } from '@nestjs/common';
import authConfig from 'src/config/authConfig';
import { ConfigType } from '@nestjs/config';

interface User {
	id: string;
	name: string;
	email: string;
}

@Injectable()
export class AuthService {
	constructor(
		@Inject(authConfig.KEY) private config: ConfigType<typeof authConfig>,
	) { }
	
	login(user: User) {
		const payload = { ...user };
		
		return jwt.sign(payload, this.config.jwtSecret, {
			expiresIn: '1d',
			audience: 'example.com',
			issuer: 'example.com',
		});
	}
}

로그인

이메일과 패스워드로 유저를 찾고, 유저가 있다면 JWT를 발급하면 된다.

async login(email: string, password: string): Promise<string> {
	const user = await this.usersRepository.findOne({
		where: { email, password }
	});
	
	if (!user) {
		throw new NotFoundException('User is not Exist');
	}
	
	return this.authService.login({
		id: user.id,
		name: user.name,
		email: user.email,
	});
}

JWT 인증

로그인한 유저 본인의 정보를 조회하는 API를 만들어보자. (GET /users/:id, Authorization: Bearer <token>)

Bearer 방식 인증을 사용하기 위해 헤더에 키를 Authorization, 값을 Bearer <token>으로 구성한다.

 import { Headers } from '@nestjs/common';
 
 @Controller('users')
 export class UsersController {
	 constructor(
		 private usersService: UsersService,
		 private authService: AuthService,
	) { }
	
	(...)
	
	@Get(':id')
	async getUserInfo(@Headers() headers: any, @Param('id') userId: string): 
	Promise<UserInfo> {
		const jwtString = headers.authorization.split('Bearer ')[1];
		this.authService.verify(jwtString);
		return this.usersService.getUserInfo(userId)
	}
}
		

 

 

AuthService에서는 JWT 토큰을 검증하는 로직을 작성한다.

export class AuthService {
	
	(...)
	verify(jwtString: string) {
		try {
			const payload = jwt.verify(jwtString, this.config.jwtSecret) as 
			(jwt.JwtPayload | string) & User;
			
			const { id, email } = payload;
			
			return {
				userId: id,
				email,
			}
			
		} catch (e) {
			throw new UnauthorizedException()
		}
	}
}

 

 

UsersService에서는 데이터베이스에서 정보를 가져온다.

export class UsersService {
	
	(...)
	async getUserInfo(userId: string): Promise<UserInfo> {
		const user = await this.usersRepository.findOne({
			where: { id: userId }
		});
		
		if (!user) {
			throw new NotFoundException('User is not exist');
		}
		
		return {
			id: user.id,
			name: user.name,
			email: user.email,
		};
	}
}

 

 

현재 구현 방식은 헤더에 포함된 JWT 토큰의 유효성을 검사하는 로직을 모든 엔드포인트에 중복 구현해야 하는데, 비효율적이고 DRY 원칙에도 위배된다.

→ 가드를 활용하여 핸들러 코드에서 분리하자

@Injectable()
export class AuthGuard implements CanActivate {
	constructor(private authService: AuthService) { }
	
	canActivate(
		context: ExecutionContext,
	): boolean | Promise<boolean> | Observable<boolean> {
		const request = context.switchToHttp().getRequest();
		return this.validateRequest(request);
	}
	
	private validateRequest(request: Request) {
		const jwtString = request.headers.authorization.split('Bearer ')[1];
		this.authService.verify(jwtString);
		return true;
	}
}

 

 

그리고 회원 조회 엔드포인트에만 AuthGuard를 적용해보자

@UseGuards(AuthGuard)
@Get(':id')
async getUserInfo(@Headers() headers: any, @Param('id') userId: string):
Promsie<UserInfo> {
	return this.usersService.getUserInfo(userId);
}

4. Interceptor

요청과 응답을 가로채서 변형을 가할 수 있는 컴포넌트

  • 메서드 실행 전/후 추가 로직을 바인딩
  • 함수에서 반환된 결과를 변환
  • 함수에서 던져진 예외를 변환
  • 기본 기능의 동작을 확장
  • 특정 조건에 따라 기능을 완전히 재정의(캐싱 등)

 

미들웨어는 요청이 라우트 핸들러로 전달되기 전에 동작하지만, 인터셉터는 요청에 대한 라우트 핸들러의 처리 전과 후에 호출되어 요청과 응답을 다룰 수 있다.

(또한 미들웨어는 여러개의 미들웨어 조합도 가능)

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 => {
				return { data }
			}));
	}
}

 

 

NestInterceptor 구조

export interface NestInterceptor<T = any, R = any> {
	intercept(context: ExecutionContext, next: CallHandler<T>): Observable<R> |
Promise<Observable<R>>;
}

export interface CallHandler<T = any> {
	handle(): Observable<T>;
}

 

두 번째 인수인 CallHandler는 handle() 메서드를 구현해야 한다.

→ 라우트 핸들러에서 전달된 응답 스트림을 돌려주고 RxJS의 Observable로 구현되어 있다.

→ handle()을 호출하고 Observable을 수신한 후 응답 스트림에 추가 작업을 수행할 수 있다.

 

 

예시로 LoggingInterceptor를 활용하여 요청과 응답을 로그로 남긴다면

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
	constructor(private logger: Logger) { }
	
	intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
		const { method, url, body } = context.getArgByIndex(0);
		this.logger.log(`Request to ${method} ${url}`);
		
		return next
			.handle()
			.pipe(
				tap(data => this.logger.log(`Response from ${method} ${url} \\n
				response: ${JSON.stringify(data)}`))
			);
	}
}

5. 예외 필터

Nest에서 제공하는 전역 예외 필터 외에 직접 예외 필터 레이어를 둬서 원하는 대로 예외를 다룰 수 있다.

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
	catch(exception: Error, host: ArgumentsHost) {
		const ctx = host.switchToHttp();
		const res = ctx.getResponse<Response>();
		const req = ctx.getRequest<Request>();
		
		// HttpException이 아닌 예외는 InternalServerErrorException 처리
		if(!(exception instance HttpException)) {
			exception = new InternalServerErrorException();
		}
		
		const response = (exception as HttpException).getResponse();
		
		const log = {
			timestamp: new Date(),
			url: req.url,
			response,
		}
		
		console.log(log);
		
		res
		.status((exception as HttpException).getStatus())
		.json(response);
	}
}

예외 필터는 @UseFilter 데코레이터로 컨트롤러에 직접 적용하거나 전역으로 적용 가능

(예외 필터는 전역 필터를 하나만 가지도록 하는 것이 일반적)

부트스트랩 과정에서 전역 필터를 적용하는 방식은 필터에 의존성을 주입할 수 없다는 제약이 있다 (예외 필터의 수행이 예외가 발생한 모듈 외부에서 이루어지기 때문)

 

→ 의존성 주입을 받으려면 예외 필터를 커스텀 프로바이더로 등록하자

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
	providers: [
		{
			provide: APP_FILTER,
			useClass: HttpExceptionFilter,
		},
	],
})
export class AppModule {}

 

 

이제 HttpExceptionFilter는 다른 프로바이더를 주입받아 사용 가능

export class HttpExceptionFilter implements ExceptionFilter {
	constructor(private logger: Logger) {}

	(...)
}

6. 요청 생명주기

미들웨어

전역으로 바인딩된 미들웨어 실행, 이후에는 모듈에 바인딩되는 순서대로 실행

(다른 모듈에 바인딩되어 있는 미들웨어들이 있으면 먼저 루트 모듈에 바인딩된 미들웨어 실행 후 imports에 정의된 순서대로 실행)


가드

전역으로 바인딩된 가드 시작 → 컨트롤러에 정의된 순서대로 실행


인터셉터

인터셉터는 RxJS의 Observable 객체를 반환하는데 요청의 실행 순서와 반대 순서로 동작한다.

요청: 전역 → 컨트롤러 → 라우터 순

응답: 라우터 → 컨트롤러 → 전역 순


파이프

파이프가 여러 레벨에 적용되어 있다면 순서대로 적용

파이프가 적용된 라우터의 매개변수가 여러 개 있을 때는 정의한 순서의 역순으로 적용


예외 필터

라우터 → 컨트롤러 → 전역으로 바인딩된 순서대로 동작

(필터가 예외를 잡으면 다른 필터가 동일한 예외를 잡을 수 없음)

 

 

순서를 정리하자면,

  1. 요청
  2. 미들웨어(전역 미들웨어 → 모듈 미들웨어)
  3. 가드(전역 가드 → 컨트롤러 가드 → 라우터 가드)
  4. 인터셉터(전역 인터셉터 → 컨트롤러 인터셉터 → 라우터 인터셉터)
  5. 파이프(전역 파이프 → 컨트롤러 파이프 → 라우터 파이프)
  6. 컨트롤러
  7. 인터셉터(전역 인터셉터 → 컨트롤러 인터셉터 → 라우터 인터셉터)
  8. 예외 필터(전역 인터셉터 → 컨트롤러 인터셉터 → 라우터 인터셉터)
  9. 응답
저작자표시 비영리 변경금지 (새창열림)

'Study > NestJS' 카테고리의 다른 글

[‘NestJS로 배우는 백엔드 프로그래밍’ 정리] Controller, Provider, Module  (0) 2025.10.09
'Study/NestJS' 카테고리의 다른 글
  • [‘NestJS로 배우는 백엔드 프로그래밍’ 정리] Controller, Provider, Module
퀵차분
퀵차분
Web Developer 🥐
  • 퀵차분
    QC's Devlog
    퀵차분
  • 전체
    오늘
    어제
    • 분류 전체보기 (177)
      • Frontend (31)
      • Fedify (4)
      • Study (42)
        • NestJS (2)
        • Node.js (3)
        • Modern JS Deep Dive (13)
        • SQL (1)
        • Network (1)
        • 프롬프트 엔지니어링 (4)
        • 인공지능 (9)
        • 시스템프로그래밍 (11)
        • 선형대수학 (1)
      • Intern (4)
      • KUIT (21)
      • Algorithm (48)
        • Baekjoon(C++) (26)
        • Programmers(JavaScript) (22)
      • 우아한테크코스(프리코스) (4)
      • Project (9)
        • crohasang_page (2)
        • PROlog (4)
        • Nomadcoder (2)
      • 생각 (4)
      • Event (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    자바스크립트
    알고리즘
    리액트
    프론트엔드
    티스토리챌린지
    프로그래머스
    next.js
    KUIT
    인공지능
    javascript
    react
    프로그래머스 자바스크립트
    fedify
    오블완
    HTML
    typescript
    타입스크립트
    백준
    음악추천
    시스템프로그래밍
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
퀵차분
[‘NestJS로 배우는 백엔드 프로그래밍’ 정리] Pipe, Middleware, Guard, Interceptor
상단으로

티스토리툴바