NestJS Exception Filter 예외 설계

NestJS 심화: Exception Filters와 예외 경계 설계 요약 이미지
요약 이미지(직접 생성). 무단 사용 금지.

NestJS 심화: Exception Filters와 예외 경계 설계

1) 버전 기준

  • NestJS 릴리즈 기준: v11.1.14 (2026-02-17, GitHub Releases)
  • 공식 문서 기준: Exception Filters

2) 핵심 개념

NestJS 공식 문서에서 Exception Filter는 예외를 가로채고 응답 형태를 일관되게 만드는 메커니즘으로 설명됩니다. 기본적으로는 프레임워크 내장 예외 계층을 사용하고, 커스텀 필터를 통해 HTTP 응답 바디/코드/로깅 정책을 통제할 수 있습니다.

  • @Catch()로 특정 예외 타입을 대상으로 필터를 선언할 수 있습니다.
  • 메서드/컨트롤러/글로벌 범위로 적용 범위를 선택할 수 있습니다.
  • 플랫폼(Express/Fastify)에 따라 응답 객체 접근은 ArgumentsHost를 통해 분기합니다.

3) 트레이드오프

  • 글로벌 필터 1개 집중: 일관성은 높지만, 도메인별 세밀한 오류 표현이 어려워질 수 있습니다.
  • 컨트롤러별 필터 분리: 표현력은 높지만 정책 중복과 관리 비용이 커질 수 있습니다.
  • 예외를 광범위하게 200 응답으로 변환: 클라이언트 호환성은 좋아질 수 있으나, 모니터링/알람 정확도가 떨어질 수 있습니다.

4) 장애 재현-해결

재현 시나리오

  1. 서비스 계층에서 일반 Error를 throw.
  2. HTTP 계층에서 필터가 없거나, HttpException만 처리하도록 잘못 제한.
  3. 클라이언트가 엔드포인트별로 다른 에러 포맷을 받음.

해결 절차

  1. 공식 문서 패턴대로 커스텀 Exception Filter를 작성합니다.
  2. ArgumentsHost에서 HTTP 컨텍스트를 꺼내 공통 에러 스키마를 강제합니다.
  3. 글로벌 적용(app.useGlobalFilters()) 후, 도메인 특수 케이스만 로컬 필터로 보강합니다.
  4. HttpException과 일반 Error를 분리 처리하여 상태코드/메시지 손실을 방지합니다.

5) 체크리스트

  • [ ] 전역 에러 응답 스키마(코드/메시지/타임스탬프/경로)가 고정되어 있는가?
  • [ ] HttpException과 일반 Error가 구분 처리되는가?
  • [ ] 글로벌 필터와 로컬 필터의 책임 경계가 문서화되어 있는가?
  • [ ] Fastify/Express 중 실제 어댑터 기준으로 테스트했는가?
  • [ ] 예외 응답이 로깅/모니터링 태그와 연계되는가?

6) 공식 링크

  • NestJS Exception Filters: https://docs.nestjs.com/exception-filters
  • NestJS Releases: https://github.com/nestjs/nest/releases

7) 실전 코드: 커스텀 Exception Filter 구현

가장 많이 쓰이는 패턴은 모든 예외를 잡는 글로벌 필터입니다. HttpException과 일반 Error를 분기 처리하여 일관된 에러 응답을 만듭니다.

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';

interface ErrorResponse {
  statusCode: number;
  message: string;
  error: string;
  timestamp: string;
  path: string;
}

@Catch() // 모든 예외를 잡음
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger(AllExceptionsFilter.name);

  catch(exception: unknown, host: ArgumentsHost): void {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = 'Internal server error';
    let error = 'Internal Server Error';

    if (exception instanceof HttpException) {
      statusCode = exception.getStatus();
      const exceptionResponse = exception.getResponse();
      message = typeof exceptionResponse === 'string'
        ? exceptionResponse
        : (exceptionResponse as any).message || message;
      error = exception.name;
    } else if (exception instanceof Error) {
      message = exception.message;
      error = exception.name;
    }

    const errorResponse: ErrorResponse = {
      statusCode,
      message,
      error,
      timestamp: new Date().toISOString(),
      path: request.url,
    };

    // 5xx 에러만 스택 트레이스 로깅
    if (statusCode >= 500) {
      this.logger.error(
        `${request.method} ${request.url} ${statusCode}`,
        exception instanceof Error ? exception.stack : String(exception),
      );
    } else {
      this.logger.warn(`${request.method} ${request.url} ${statusCode}: ${message}`);
    }

    response.status(statusCode).json(errorResponse);
  }
}

글로벌 필터 등록 (main.ts)

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './filters/all-exceptions.filter';

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

DI를 통한 등록이 필요한 경우(다른 서비스 주입 등) APP_FILTER 토큰을 사용합니다:

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { AllExceptionsFilter } from './filters/all-exceptions.filter';

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

8) 도메인별 예외 계층 설계

실무에서는 비즈니스 로직별로 커스텀 예외 클래스를 만들어 에러 코드와 메시지를 표준화합니다. 이렇게 하면 프론트엔드에서 에러 코드 기반으로 분기 처리가 가능해집니다.

// 비즈니스 예외 베이스 클래스
export class BusinessException extends HttpException {
  constructor(
    public readonly errorCode: string,
    message: string,
    statusCode: HttpStatus = HttpStatus.BAD_REQUEST,
  ) {
    super({ errorCode, message, statusCode }, statusCode);
  }
}

// 도메인별 예외
export class UserNotFoundException extends BusinessException {
  constructor(userId: string) {
    super('USER_NOT_FOUND', `사용자를 찾을 수 없습니다: ${userId}`, HttpStatus.NOT_FOUND);
  }
}

export class InsufficientBalanceException extends BusinessException {
  constructor(required: number, current: number) {
    super(
      'INSUFFICIENT_BALANCE',
      `잔액이 부족합니다. 필요: ${required}, 현재: ${current}`,
      HttpStatus.UNPROCESSABLE_ENTITY,
    );
  }
}

9) 마이크로서비스 예외 처리 (RPC/WebSocket)

HTTP가 아닌 전송 계층에서는 RpcException이나 WsException을 사용해야 합니다. ArgumentsHost.getType()으로 컨텍스트 타입을 판별하면 하이브리드 앱에서도 하나의 필터로 처리 가능합니다.

@Catch()
export class HybridExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const type = host.getType(); // 'http' | 'rpc' | 'ws'

    switch (type) {
      case 'http':
        return this.handleHttp(exception, host);
      case 'rpc':
        return this.handleRpc(exception, host);
      case 'ws':
        return this.handleWs(exception, host);
    }
  }

  private handleHttp(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;
    response.status(status).json({ error: String(exception) });
  }

  private handleRpc(exception: unknown, host: ArgumentsHost) {
    return throwError(() => exception);
  }

  private handleWs(exception: unknown, host: ArgumentsHost) {
    const client = host.switchToWs().getClient();
    client.emit('error', { message: String(exception) });
  }
}

10) 관련 글

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux