NestJS

7. NestJS 예외 필터(Exception Filters): 오류 처리 전략

오늘의하늘은 2025. 3. 30. 23:51

애플리케이션을 개발하다 보면 오류 상황은 불가피하게 발생합니다. 데이터베이스 연결 실패, 유효하지 않은 사용자 입력, 인증 오류 등 다양한 예외 상황에 대처하는 것은 안정적인 애플리케이션을 구축하는 데 필수적입니다. NestJS는 이러한 예외 상황을 우아하게 처리할 수 있는 '예외 필터(Exception Filters)'라는 강력한 메커니즘을 제공합니다.

이 글에서는 NestJS의 예외 처리 시스템의 내부 작동 방식과 사용자 정의 예외 필터를 만들어 애플리케이션의 오류 처리를 세밀하게 제어하는 방법을 자세히 살펴보겠습니다.

목차

  1. NestJS의 기본 예외 처리 방식
  2. 내장 HTTP 예외
  3. 예외 필터의 개념과 역할
  4. 기본 예외 필터 구현하기
  5. 전역 예외 필터 등록하기
  6. 여러 예외 필터 결합하기
  7. 데이터베이스 오류 처리 전략
  8. 비동기 예외 처리
  9. 로깅과 모니터링 통합
  10. 모범 사례와 패턴

NestJS의 기본 예외 처리 방식

NestJS는 처리되지 않은 모든 예외를 포착하는 전역 예외 필터 레이어를 제공합니다. 이 레이어는 클라이언트에게 적절한 사용자 친화적인 응답을 반환하는 역할을 합니다.

기본적으로 NestJS가 인식하지 못하는 예외가 발생하면(예: Error, TypeError 또는 사용자 정의 예외), 다음과 같은 JSON 응답을 자동으로 생성합니다:

{
  "statusCode": 500,
  "message": "Internal server error"
}

이 응답은 구체적인 오류 정보를 숨기고 일반적인 '서버 오류' 메시지만 노출하므로 보안 측면에서는 좋지만, 디버깅 목적으로는 충분한 정보를 제공하지 않습니다.

내장 HTTP 예외

NestJS는 @nestjs/common 패키지에서 다양한 내장 HTTP 예외 클래스를 제공합니다. 이러한 예외들은 적절한 HTTP 상태 코드와 함께 표준화된 응답을 생성합니다.

주요 내장 예외 클래스:

import {
  BadRequestException,
  UnauthorizedException,
  ForbiddenException,
  NotFoundException,
  ConflictException,
  GoneException,
  HttpVersionNotSupportedException,
  PayloadTooLargeException,
  UnsupportedMediaTypeException,
  UnprocessableEntityException,
  InternalServerErrorException,
  NotImplementedException,
  ImATeapotException,
  MethodNotAllowedException,
  BadGatewayException,
  ServiceUnavailableException,
  GatewayTimeoutException,
  PreconditionFailedException,
} from '@nestjs/common';

이러한 예외 클래스는 다음과 같이 사용할 수 있습니다:

import { Injectable, NotFoundException } from '@nestjs/common';

@Injectable()
export class UsersService {
  private users = [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
  ];

  findOne(id: number) {
    const user = this.users.find(user => user.id === id);
    
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
    
    return user;
  }
}

위 코드에서 findOne 메서드는 사용자를 찾지 못할 경우 NotFoundException을 발생시킵니다. 이 예외는 자동으로 HTTP 404 상태 코드와 함께 다음과 같은 응답을 생성합니다:

{
  "statusCode": 404,
  "message": "User with ID 3 not found",
  "error": "Not Found"
}

커스텀 예외 만들기

표준 HTTP 예외 외에도 비즈니스 로직에 특화된 커스텀 예외를 만들 수 있습니다. 기본 HttpException 클래스를 확장하여 자신만의 예외 클래스를 정의할 수 있습니다:

import { HttpException, HttpStatus } from '@nestjs/common';

export class InsufficientCreditsException extends HttpException {
  constructor(userId: number, requiredCredits: number) {
    super(
      {
        statusCode: HttpStatus.PAYMENT_REQUIRED,
        message: `User ${userId} needs ${requiredCredits} more credits to perform this action`,
        errorCode: 'INSUFFICIENT_CREDITS',
      },
      HttpStatus.PAYMENT_REQUIRED
    );
  }
}

이제 서비스나 컨트롤러에서 이 커스텀 예외를 사용할 수 있습니다:

import { Injectable } from '@nestjs/common';
import { InsufficientCreditsException } from './exceptions/insufficient-credits.exception';

@Injectable()
export class PaymentService {
  processPayment(userId: number, amount: number) {
    const userCredits = this.getUserCredits(userId);
    
    if (userCredits < amount) {
      throw new InsufficientCreditsException(userId, amount - userCredits);
    }
    
    // 결제 처리 로직
    return { success: true };
  }
  
  private getUserCredits(userId: number): number {
    // 사용자 크레딧 조회 로직 (간략화)
    return 10;
  }
}

예외 필터의 개념과 역할

NestJS의 예외 필터는 애플리케이션에서 발생하는 예외를 가로채고 사용자 정의 로직을 적용한 후 적절한 응답을 클라이언트에게 반환하는 메커니즘입니다. 예외 필터를 사용하면 다음과 같은 작업을 수행할 수 있습니다:

  1. 예외 응답의 형식을 사용자 정의
  2. 특정 유형의 예외에 대한 특별한 처리 로직 추가
  3. 예외 로깅 및 모니터링
  4. 다양한 클라이언트 유형(웹, 모바일, API)에 맞는 응답 형식 제공

기본 예외 필터 구현하기

예외 필터를 만들기 위해서는 ExceptionFilter 인터페이스를 구현하고 @Catch() 데코레이터를 사용해야 합니다. 다음은 모든 HTTP 예외를 처리하는 기본 예외 필터의 예입니다:

// http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@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();
    const exceptionResponse = exception.getResponse();
    
    let errorMessage: string;
    let errorCode: string;
    
    if (typeof exceptionResponse === 'object' && exceptionResponse !== null) {
      errorMessage = (exceptionResponse as any).message || 'Internal error';
      errorCode = (exceptionResponse as any).errorCode;
    } else {
      errorMessage = exceptionResponse as string;
    }
    
    const responseBody = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      message: errorMessage,
      ...(errorCode && { errorCode }),
    };
    
    response.status(status).json(responseBody);
  }
}

이 필터는 다음과 같은 개선된 응답 형식을 제공합니다:

{
  "statusCode": 404,
  "timestamp": "2023-07-01T12:00:00.000Z",
  "path": "/users/999",
  "method": "GET",
  "message": "User with ID 999 not found",
  "errorCode": "USER_NOT_FOUND"
}

특정 컨트롤러에 예외 필터 적용하기

예외 필터를 특정 컨트롤러나 라우트 핸들러에 적용하려면 @UseFilters() 데코레이터를 사용합니다:

// users.controller.ts
import { Controller, Get, Param, UseFilters, ParseIntPipe } from '@nestjs/common';
import { UsersService } from './users.service';
import { HttpExceptionFilter } from '../filters/http-exception.filter';

@Controller('users')
@UseFilters(HttpExceptionFilter)  // 컨트롤러 전체에 필터 적용
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':id')
  // 특정 라우트에만 필터 적용도 가능
  // @UseFilters(HttpExceptionFilter)
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id);
  }
}

전역 예외 필터 등록하기

모든 컨트롤러와 라우트 핸들러에 예외 필터를 적용하려면 전역으로 등록할 수 있습니다. 이는 main.ts 파일에서 수행합니다:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 전역 필터 등록
  app.useGlobalFilters(new HttpExceptionFilter());
  
  await app.listen(3000);
}
bootstrap();

또는 의존성 주입을 활용하여 전역 필터를 등록할 수도 있습니다:

// app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { UsersModule } from './users/users.module';

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

이 방법은 필터가 다른 의존성(예: 로거 서비스, 구성 서비스 등)을 주입받아야 할 때 특히 유용합니다.

여러 예외 필터 결합하기

NestJS에서는 여러 예외 필터를 결합하여 다양한 유형의 예외를 처리할 수 있습니다. 각 필터는 특정 유형의 예외만 처리하도록 설계할 수 있으며, 필터의 실행 순서는 선언된 순서의 역순입니다.

예를 들어, 데이터베이스 관련 예외와 HTTP 예외를 별도로 처리하는 필터를 만들 수 있습니다:

// database-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { QueryFailedError, EntityNotFoundError } from 'typeorm';
import { Response } from 'express';

@Catch(QueryFailedError, EntityNotFoundError)
export class DatabaseExceptionFilter implements ExceptionFilter {
  catch(exception: QueryFailedError | EntityNotFoundError, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    
    let status = 500;
    let message = 'Database error occurred';
    
    if (exception instanceof EntityNotFoundError) {
      status = 404;
      message = 'Entity not found';
    } else if (exception instanceof QueryFailedError) {
      const sqlError = exception as any;
      
      // 중복 키 오류(PostgreSQL) 처리
      if (sqlError.code === '23505') {
        status = 409;
        message = 'Duplicate entry';
      }
    }
    
    response.status(status).json({
      statusCode: status,
      message,
      timestamp: new Date().toISOString(),
    });
  }
}

그런 다음 여러 필터를 결합하여 사용할 수 있습니다:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { DatabaseExceptionFilter } from './filters/database-exception.filter';

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

이 설정에서는 먼저 HttpExceptionFilter가 실행되고, 그 다음 DatabaseExceptionFilter가 실행됩니다. DatabaseExceptionFilter가 예외를 처리하지 않으면 HttpExceptionFilter가 처리할 기회를 갖게 됩니다.

데이터베이스 오류 처리 전략

데이터베이스 작업은 다양한 오류를 발생시킬 수 있으며, 이를 적절히 처리하는 것은 견고한 애플리케이션을 구축하는 데 중요합니다. 다음은 TypeORM과 함께 NestJS를 사용할 때 일반적인 데이터베이스 오류를 처리하는 전략입니다:

// database-exception.filter.ts (확장 버전)
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus, Logger } from '@nestjs/common';
import { QueryFailedError, EntityNotFoundError, TypeORMError } from 'typeorm';
import { Response, Request } from 'express';

@Catch(TypeORMError)
export class TypeOrmExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(TypeOrmExceptionFilter.name);

  catch(exception: TypeORMError, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    
    let status: HttpStatus;
    let message: string;
    let errorCode: string;
    
    // 자세한 로깅
    this.logger.error(`TypeORM Error: ${exception.message}`, exception.stack);
    
    if (exception instanceof EntityNotFoundError) {
      status = HttpStatus.NOT_FOUND;
      message = 'Entity not found';
      errorCode = 'ENTITY_NOT_FOUND';
    } else if (exception instanceof QueryFailedError) {
      const err = exception as any;
      
      // PostgreSQL 오류 코드에 따른 처리
      switch (err.code) {
        case '23505': // 중복 키 오류
          status = HttpStatus.CONFLICT;
          message = 'Duplicate entry';
          errorCode = 'DUPLICATE_ENTRY';
          
          // 어떤 필드가 중복되었는지 식별 (정규식 사용)
          const detail = err.detail;
          const keyMatch = detail.match(/\((.*?)\)=/);
          if (keyMatch) {
            const key = keyMatch[1];
            message = `The ${key} already exists`;
          }
          break;
          
        case '23503': // 외래 키 제약 조건 위반
          status = HttpStatus.BAD_REQUEST;
          message = 'Related entity does not exist';
          errorCode = 'FOREIGN_KEY_VIOLATION';
          break;
          
        case '22P02': // 잘못된 텍스트 표현
          status = HttpStatus.BAD_REQUEST;
          message = 'Invalid input syntax';
          errorCode = 'INVALID_TEXT_REPRESENTATION';
          break;
          
        default:
          status = HttpStatus.INTERNAL_SERVER_ERROR;
          message = 'Database error occurred';
          errorCode = 'DATABASE_ERROR';
      }
    } else {
      // 기타 TypeORM 오류 처리
      status = HttpStatus.INTERNAL_SERVER_ERROR;
      message = 'Database operation failed';
      errorCode = 'DATABASE_ERROR';
    }
    
    response.status(status).json({
      statusCode: status,
      message,
      errorCode,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

이 필터는 다양한 PostgreSQL 오류 코드를 처리하고 사용자 친화적인 오류 메시지를 반환합니다. 다른 데이터베이스 시스템(MySQL, MS SQL 등)을 사용하는 경우 해당 시스템의 오류 코드에 맞게 필터를 조정해야 합니다.

비동기 예외 처리

NestJS에서는 비동기 작업 중에 발생하는 예외도 예외 필터에서 자동으로 처리됩니다. Promise가 거부되거나 async 함수에서 예외가 발생하면 NestJS의 예외 필터 체인으로 전달됩니다.

다음은 비동기 서비스 메서드에서 예외를 발생시키는 예입니다:

// users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { User } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async findOne(id: number): Promise<User> {
    const user = await this.usersRepository.findOneBy({ id });
    
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
    
    return user;
  }
  
  async create(userData: Partial<User>): Promise<User> {
    try {
      const newUser = this.usersRepository.create(userData);
      return await this.usersRepository.save(newUser);
    } catch (error) {
      // 오류를 다시 던져서 예외 필터가 처리하도록 함
      throw error;
    }
  }
}

비동기 예외 처리와 관련된 몇 가지 모범 사례:

  1. 예외 래핑: 외부 라이브러리나 서비스에서 발생한 예외를 NestJS 예외로 래핑하여 일관된 오류 응답을 제공합니다.
  2. 선별적 다시 던지기: 일부 예외는 처리하고 다른 예외는 상위 계층에서 처리하도록 다시 던질 수 있습니다.
  3. 비동기 오류 처리 미들웨어: 비동기 라우트 핸들러의 예외를 잡아 처리하는 미들웨어를 사용할 수 있습니다.

로깅과 모니터링 통합

효과적인 오류 처리 전략의 중요한 부분은 적절한 로깅입니다. NestJS의 내장 로거 또는 Winston과 같은 외부 로깅 라이브러리를 예외 필터에 통합할 수 있습니다:

// global-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(GlobalExceptionFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;
    
    let message: string;
    let stack: string;
    
    if (exception instanceof HttpException) {
      const exceptionResponse = exception.getResponse();
      message = typeof exceptionResponse === 'object' && 'message' in exceptionResponse
        ? (exceptionResponse as any).message
        : exception.message;
      
      stack = exception.stack;
    } else if (exception instanceof Error) {
      message = exception.message;
      stack = exception.stack;
    } else {
      message = 'Internal server error';
      stack = '';
    }
    
    // 오류 상세 정보 로깅
    this.logger.error(
      `[${request.method}] ${request.url} - ${status} - ${message}`,
      stack,
    );
    
    // 운영 환경에서는 스택 트레이스를 클라이언트에게 노출하지 않습니다
    const isProduction = process.env.NODE_ENV === 'production';
    
    const responseBody = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message,
      ...(!isProduction && stack ? { stack } : {}),
    };
    
    response.status(status).json(responseBody);
  }
}

이 필터는 모든 예외를 잡아서 로깅하고, 개발 환경에서는 스택 트레이스를 클라이언트에게 제공하지만 운영 환경에서는 제공하지 않습니다.

Sentry 같은 오류 모니터링 서비스 통합

애플리케이션을 운영할 때는 Sentry와 같은 오류 모니터링 서비스를 통합하는 것이 유용합니다:

// sentry-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';

@Catch()
export class SentryExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    
    // Sentry에 예외 보고
    Sentry.captureException(exception);
    
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;
    
    let message: string;
    
    if (exception instanceof HttpException) {
      const exceptionResponse = exception.getResponse();
      message = typeof exceptionResponse === 'object' && 'message' in exceptionResponse
        ? (exceptionResponse as any).message
        : exception.message;
    } else if (exception instanceof Error) {
      message = exception.message;
    } else {
      message = 'Internal server error';
    }
    
    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message,
    });
  }
}

이 필터를 사용하기 전에 Sentry를 초기화해야 합니다:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SentryExceptionFilter } from './filters/sentry-exception.filter';
import * as Sentry from '@sentry/node';

async function bootstrap() {
  // Sentry 초기화
  Sentry.init({
    dsn: 'YOUR_SENTRY_DSN',
    environment: process.env.NODE_ENV,
  });
  
  const app = await NestFactory.create(AppModule);
  
  // 전역 필터 등록
  app.useGlobalFilters(new SentryExceptionFilter());
  
  await app.listen(3000);
}
bootstrap();

모범 사례와 패턴

NestJS에서 예외 처리를 위한 몇 가지 모범 사례와 패턴을 살펴보겠습니다:

1. 계층화된 예외 처리

애플리케이션의 각 계층에 적합한 예외 처리 전략을 채택합니다:

  • 컨트롤러 계층: HTTP 예외를 던지고 예외 필터로 처리합니다.
  • 서비스 계층: 도메인별 예외를 던지고 필요한 경우 하위 계층의 예외를 래핑합니다.
  • 리포지토리 계층: 데이터베이스 예외를 처리하거나 도메인별 예외로 변환합니다.

2. 도메인별 예외 계층 구조 만들기

비즈니스 로직에 특화된 예외 계층 구조를 만들어 예외 처리를 더 세밀하게 제어할 수 있습니다:

// exceptions/base.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class BaseException extends HttpException {
  constructor(
    message: string,
    statusCode: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR,
    readonly errorCode?: string,
  ) {
    super(
      {
        statusCode,
        message,
        errorCode,
      },
      statusCode,
    );
  }
}

// exceptions/user.exceptions.ts
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';

export class UserNotFoundException extends BaseException {
  constructor(userId: number) {
    super(
      `User with ID ${userId} not found`,
      HttpStatus.NOT_FOUND,
      'USER_NOT_FOUND',
    );
  }
}

export class UserAlreadyExistsException extends BaseException {
  constructor(email: string) {
    super(
      `User with email ${email} already exists`,
      HttpStatus.CONFLICT,
      'USER_ALREADY_EXISTS',
    );
  }
}

// exceptions/payment.exceptions.ts
import { HttpStatus } from '@nestjs/common';
import { BaseException } from './base.exception';

export class InsufficientCreditsException extends BaseException {
  constructor(userId: number, requiredCredits: number) {
    super(
      `User ${userId} needs ${requiredCredits} more credits`,
      HttpStatus.PAYMENT_REQUIRED,
      'INSUFFICIENT_CREDITS',
    );
  }
}

export class PaymentFailedException extends BaseException {
  constructor(transactionId: string, reason: string) {
    super(
      `Payment ${transactionId} failed: ${reason}`,
      HttpStatus.BAD_REQUEST,
      'PAYMENT_FAILED',
    );
  }
}

이러한 도메인별 예외는 서비스 계층에서 다음과 같이 사용할 수 있습니다:

// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { UserNotFoundException, UserAlreadyExistsException } from '../exceptions/user.exceptions';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async findOne(id: number): Promise<User> {
    const user = await this.usersRepository.findOneBy({ id });
    
    if (!user) {
      throw new UserNotFoundException(id);
    }
    
    return user;
  }
  
  async create(userData: Partial<User>): Promise<User> {
    // 이메일 중복 체크
    const existingUser = await this.usersRepository.findOneBy({ 
      email: userData.email 
    });
    
    if (existingUser) {
      throw new UserAlreadyExistsException(userData.email);
    }
    
    const newUser = this.usersRepository.create(userData);
    return await this.usersRepository.save(newUser);
  }
}

3. 예외 필터 조합하기

여러 예외 필터를 결합하여 서로 다른 유형의 예외를 처리할 수 있습니다:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { TypeOrmExceptionFilter } from './filters/typeorm-exception.filter';
import { SentryExceptionFilter } from './filters/sentry-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 필터는 역순으로 적용됩니다:
  // 1. SentryExceptionFilter (모든 예외를 Sentry로 보냄)
  // 2. TypeOrmExceptionFilter (TypeORM 관련 예외 처리)
  // 3. HttpExceptionFilter (Http 예외 처리)
  app.useGlobalFilters(
    new SentryExceptionFilter(),
    new TypeOrmExceptionFilter(),
    new HttpExceptionFilter(),
  );
  
  await app.listen(3000);
}
bootstrap();

4. 환경별 예외 처리 전략

개발 환경과 운영 환경에 맞는 예외 처리 전략을 사용합니다:

// config/exception-filter.config.ts
import { HttpExceptionFilter } from '../filters/http-exception.filter';
import { DetailedHttpExceptionFilter } from '../filters/detailed-http-exception.filter';
import { SentryExceptionFilter } from '../filters/sentry-exception.filter';

export function getExceptionFilters() {
  const isDevelopment = process.env.NODE_ENV === 'development';
  
  if (isDevelopment) {
    // 개발 환경: 자세한 예외 정보 제공
    return [new DetailedHttpExceptionFilter()];
  } else {
    // 운영 환경: Sentry 로깅 및 최소한의 정보만 제공
    return [
      new SentryExceptionFilter(),
      new HttpExceptionFilter(),
    ];
  }
}

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { getExceptionFilters } from './config/exception-filter.config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 환경에 따른 예외 필터 적용
  const exceptionFilters = getExceptionFilters();
  app.useGlobalFilters(...exceptionFilters);
  
  await app.listen(3000);
}
bootstrap();

5. 예외 응답 형식 표준화

애플리케이션 전체에서 일관된 예외 응답 형식을 유지하는 것이 중요합니다:

// interfaces/api-response.interface.ts
export interface ApiErrorResponse {
  statusCode: number;
  message: string | string[];
  errorCode?: string;
  timestamp: string;
  path: string;
  details?: Record<string, any>;
}

// filters/base-exception.filter.ts
import { ArgumentsHost, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';
import { ApiErrorResponse } from '../interfaces/api-response.interface';

export abstract class BaseExceptionFilter {
  protected createErrorResponse(
    exception: any,
    host: ArgumentsHost,
    status: HttpStatus,
    message: string | string[],
    errorCode?: string,
    details?: Record<string, any>,
  ): void {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const errorResponse: ApiErrorResponse = {
      statusCode: status,
      message,
      errorCode,
      timestamp: new Date().toISOString(),
      path: request.url,
      ...(details && { details }),
    };

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

이제 모든 예외 필터가 이 기본 클래스를 확장하여 일관된 오류 응답 형식을 유지할 수 있습니다.

결론

NestJS의 예외 필터는 애플리케이션의 오류 처리를 세밀하게 제어할 수 있는 강력한 메커니즘입니다. 이 기능을 이용하면 일관된 오류 응답 형식을 제공하고, 다양한 유형의 예외를 적절히 처리하며, 로깅 및 모니터링 시스템과 통합할 수 있습니다.

효과적인 예외 처리 전략을 통해 사용자 경험을 향상시키고, 디버깅을 용이하게 하며, 애플리케이션의 안정성을 높일 수 있습니다. 특히 대규모 엔터프라이즈 애플리케이션에서는 체계적인 예외 처리가 유지보수성과 확장성에 큰 영향을 미칩니다.

NestJS에서 예외 필터를 활용하여 강력하고 견고한 오류 처리 시스템을 구축함으로써, 예상치 못한 상황에도 우아하게 대응하는 애플리케이션을 개발할 수 있습니다.

다음 글에서는 NestJS의 파이프(Pipes)에 대해 알아보고, 입력 데이터의 유효성 검사와 변환을 처리하는 방법에 대해 살펴보겠습니다.

반응형