📂 Backend Engineering

NestJS Exception Filters 완벽 정리 (라이프사이클부터 내부 동작까지)

foodev 2025. 2. 12. 10:36
728x90
반응형

NestJS Exception Filters 정리

NestJS에는 애플리케이션 전역에서 처리되지 않은 모든 예외를 담당하는 예외 계층(Exception Layer) 이 내장되어 있습니다.
애플리케이션 코드에서 예외가 처리되지 않고 발생하면, 이 계층이 이를 포착하여 사용자 친화적인 HTTP 응답을 자동으로 반환합니다.

공식 문서: https://docs.nestjs.com/exception-filters


🌠 목차

  • NestJS 요청 라이프사이클
  • Exception Filters의 주요 목적
  • Exception Filters가 에러를 잡을 수 있는 이유
  • 예외 처리 방법들
  • Exception Filters를 활용한 커스텀
  • Filters vs Pipes
  • Interceptor 개념 정리
  • Guard 개념 정리
  • Pipes와 Query / Body 처리 방식

NestJS 요청 라이프사이클

클라이언트 요청이 컨트롤러에 도달하기까지의 흐름은 다음과 같습니다.

  1. 클라이언트 요청
  2. 미들웨어 실행 (로깅, 인증 등)
  3. 가드 실행 (권한 체크: AuthGuard, AdminGuard 등)
  4. 인터셉터 시작
  5. 파이프 실행 (데이터 변환 / 유효성 검사)
  6. 컨트롤러 메서드 실행
  7. 서비스 로직 실행
  8. 인터셉터 종료
  9. 응답 반환

⚠️ 에러가 발생하면?

위 과정 중 어디에서든 예외가 발생하면

  • Exception Filter가 에러를 캐치
  • 적절한 에러 응답을 클라이언트에 반환

Exception Filters의 주요 목적

1️⃣ 에러 캐치

  • 요청 라이프사이클 전역에서 발생한 예외를 처리

2️⃣ 에러 커스텀

  • 에러 응답 포맷을 프로젝트 기준에 맞게 통일
  • 메시지, timestamp, path 등 추가 가능

Exception Filters가 에러를 잡을 수 있는 이유

핵심은 @Catch() 데코레이터입니다.

export declare function Catch(
  ...exceptions: Array<Type<any> | Abstract<any>>
): ClassDecorator;

의미 정리

  • ...exceptions
    • 여러 개의 예외 타입을 Rest Parameter로 전달 가능
  • Type
    • 구체적인 클래스
  • Abstract
    • 추상 클래스 또는 인터페이스
  • 반환값
    • ClassDecorator
// Type 예시
class HttpException {}

// Abstract 예시
abstract class BaseException {}
interface IException {}

👉 즉, 특정 예외 타입만 골라서 필터링 가능합니다.


Node Modules 내부 동작 구조

BaseExceptionFilter

기본 예외 처리 로직을 담당하는 추상 필터 클래스입니다.

export declare class BaseExceptionFilter<T = any>
  implements ExceptionFilter<T> {
  catch(exception: T, host: ArgumentsHost): void;
}
  • 기본 catch() 구현 제공
  • 알 수 없는 에러 처리 로직 포함

BaseExceptionFilterContext

  • 필터 인스턴스 생성 및 관리
  • DI 컨테이너와 연동

ExceptionsHandler

예외 처리 시스템의 중심

  • 발생한 예외를 적절한 필터로 라우팅
export declare class ExceptionsHandler extends BaseExceptionFilter {
  next(exception: any, ctx: ArgumentsHost): void;
}

전체 흐름 요약

예외 발생
  ↓
Router / Middleware (최초 캐치)
  ↓
ExceptionsHandler
  ↓
BaseExceptionFilterContext
  ↓
BaseExceptionFilter or ExternalExceptionsFilter

예외 처리 방법들

기본 제공 방식

throw new HttpException('접근 금지!', HttpStatus.FORBIDDEN);
{
  "statusCode": 403,
  "message": "접근 금지!"
}

하지만 응답을 더 커스터마이징하고 싶다면?

➡️ Exception Filter를 사용합니다.


Exception Filters를 활용한 커스텀

@Catch(HttpException)
export class MyExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse();

    response.status(403).json({
      statusCode: 403,
      timestamp: new Date(),
      message: '죄송합니다. 접근 권한이 없습니다.',
      path: '/admin',
    });
  }
}

언제 쓰면 좋을까?

  • 에러 로깅을 하고 싶을 때
  • 에러 응답 포맷을 통일하고 싶을 때
  • 특정 예외만 다르게 처리하고 싶을 때

여러 예외 처리

@Catch(HttpException, ValidationError)
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {}
}
@Catch()
export class CatchAllFilter implements ExceptionFilter {}

Exception Filters vs Pipes

Exception Filters

  • 발생한 예외를 처리
  • 클라이언트에 반환할 응답을 결정

Pipes

  • 컨트롤러 진입 이전에 실행
  • 데이터 변환 / 유효성 검사

❗ Pipe에서 발생한 예외도 결국 Exception Filter에서 처리됩니다.


Interceptor 정리

역할

  • 메서드 실행 전/후 로직 추가 (AOP)
  • 응답 변환
  • 예외 변환
  • 캐싱, 로깅, 타임아웃 처리

구조

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    console.log('Before...');

    return next.handle().pipe(
      tap(() => console.log(`After... ${Date.now() - now}ms`)),
    );
  }
}

Guard 정리

Guard란?

  • 라우트 핸들러 실행 여부를 결정
  • 권한, 인증, 역할 기반 제어
export interface CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean>;
}

Guard vs Middleware

구분GuardMiddleware

실행 시점 핸들러 직전 라우트 이전
역할 접근 허용 여부 결정 요청/응답 객체 수정
Context ExecutionContext 접근 가능 핸들러 정보 접근 불가

Pipes와 Query / Body 처리

Query String

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {}
{
  "statusCode": 400,
  "message": "Validation failed (numeric string is expected)",
  "error": "Bad Request"
}

👉 잘못된 값이면 컨트롤러 진입 자체를 막음

Body 처리

  • Pipe로 직접 검증 ❌
  • class-validator 사용 ⭕️
export class CreateEventBody {
  @IsUUID()
  uuid?: string;
}

마무리

  • Exception Filter는 에러 처리의 마지막 보루
  • Pipe / Guard / Interceptor와 함께 사용할 때 진짜 위력이 나옵니다

👍 글이 도움이 됐다면 좋아요 한 번 눌러주세요

관심 받는 거 좋아합니다 😄

728x90
반응형