[TIL/Nest] 2025/04/21

원민관·2025년 4월 21일

[TIL]

목록 보기
169/201
post-thumbnail

1. Overview ✍️

Authentication과 Authorization에 대한 이해가 깊어짐에 따라, NestJS에서의 예외 처리에 대한 고민이 생겼습니다. 이번 글에서는 NestJS에서 예외를 어떻게 처리하는지 정리해보겠습니다.

2. Throwing standard exceptions ✍️

NestJS는 HttpException이라는 기본 예외 클래스를 제공하여 에러를 HTTP 형식으로 처리할 수 있도록 돕습니다. 해당 클래스는 @nestjs/common 패키지에 포함되어 있습니다. API 요청 중 오류가 발생하면 HttpException을 사용하여 클라이언트에게 상세한 오류 메시지를 제공할 수 있습니다.

다음은 GET 요청에 대해 예외를 던지는 예시입니다:

@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

여기서 첫 번째 인자인 Forbidden은 클라이언트에게 전달될 에러 메시지이고, 두 번째 인자인 HttpStatus.FORBIDDEN은 HTTP 상태 코드를 나타냅니다.

클라이언트의 응답은 다음과 같습니다:

{
  "statusCode": 403,
  "message": "Forbidden"
}

에러 메시지만 변경하고 싶다면 첫 번째 인자를 다른 문자열로 변경하면 됩니다. 만약 전체 응답 본문을 변경하고 싶다면, 객체를 전달할 수도 있습니다:

@Get()
async findAll() {
  try {
    await this.service.findAll();
  } catch (error) {
    throw new HttpException({
      status: HttpStatus.FORBIDDEN,
      error: 'This is a custom message',
    }, HttpStatus.FORBIDDEN, {
      cause: error
    });
  }
}

변경된 응답은 다음과 같습니다:

{
  "status": 403,
  "error": "This is a custom message"
}

3. Exceptions logging ✍️

NestJS는 기본적으로 HttpException과 같은 예외를 콘솔에 출력하지 않습니다. 이는 이러한 예외가 애플리케이션 흐름의 일부로 간주되기 때문입니다.

하지만 개발 중에 예외도 콘솔에 출력하고 싶다면, ExceptionFilter를 커스터마이징하여 로깅할 수 있습니다. 이 부분은 추후에 더 다루겠습니다.

또한, 사용자 정의 예외 클래스를 만들어서 예외를 더 명확하게 구분하고 재사용성을 높이는 것도 좋은 방법입니다.

4. Custom exceptions ✍️

NestJS는 기본 HttpException 클래스를 제공하지만, 프로젝트의 상황에 맞춰 의미 있는 이름을 가진 사용자 정의 예외 클래스를 만들 수 있습니다. 예를 들어, 접근이 금지된 경우를 명시적으로 처리하려면 ForbiddenException을 만들 수 있습니다:

export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

이제 이 사용자 정의 예외는 다음과 같이 사용할 수 있습니다:

@Get()
async findAll() {
  throw new ForbiddenException();
}

5. Built-in HTTP exceptions ✍️

NestJS는 기본 HttpException을 상속하는 여러 내장 예외 클래스를 제공합니다. 이러한 예외 클래스들은, 자주 발생하는 HTTP 오류 상황을 다루기 위해 만들어졌습니다. 예외 처리 시 필요에 따라 적절한 클래스를 활용할 수 있습니다.

옵션 파라미터를 사용하여 오류의 원인과 설명을 추가할 수 있습니다:

throw new BadRequestException('Something bad happened', {
  cause: new Error(),
  description: 'Some error description',
});

응답은 다음과 같습니다:

{
  "message": "Something bad happened",
  "error": "Some error description",
  "statusCode": 400
}

6. Exception filters ✍️

예외 필터는 기본 예외 처리 방식으로 처리하기 어려운 특별한 예외를 다루기 위해 사용됩니다. 예를 들어, 에러가 발생했을 때 단순히 에러 코드만 보내는 대신, 추가적인 정보를 남기거나 로그를 기록하고 싶을 때 사용할 수 있습니다.

다음은 HttpException 타입의 예외만을 처리하는 예외 필터 예시입니다:

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();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

7. Arguments Host ✍️

catch() 메서드의 매개변수인 exceptionhost를 살펴보겠습니다.

  1. exception: 현재 처리 중인 예외 객체입니다. HttpException 타입이므로, 상태 코드와 메시지를 포함한 예외 정보를 가져올 수 있습니다.

  2. host: ArgumentsHost 객체로, NestJS에서 제공하는 객체로 다양한 실행 컨텍스트의 인수에 접근할 수 있습니다. 예외 필터에서 ArgumentsHost를 사용하면 HTTP 요청/응답 객체를 손쉽게 다룰 수 있습니다. 주요 메서드로는 switchToHttp()가 있으며, 이를 통해 HTTP 요청/응답 객체에 접근할 수 있습니다.

8. Binding filters ✍️

@UseFilters() 데코레이터를 사용하면 예외 필터를 특정 컨트롤러나 메서드에 바인딩할 수 있습니다. 예를 들어, HttpExceptionFilter를 다음과 같이 사용할 수 있습니다:

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

인스턴스 대신 클래스에 필터를 적용하는 것이 좋습니다. 이렇게 하면 필터를 전역적으로 재사용할 수 있어 메모리 효율성을 높일 수 있습니다:

@Controller()
@UseFilters(new HttpExceptionFilter())
export class CatsController {}

전역 필터를 적용하려면 useGlobalFilters() 메서드를 사용하면 됩니다:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

전역 필터는 애플리케이션의 모든 컨트롤러와 라우트 핸들러에서 사용됩니다.

9. Catch everything ✍️

모든 예외를 처리하려면 @Catch() 데코레이터의 매개변수를 비워두면 됩니다. 이렇게 하면 HttpException 외의 예외도 처리할 수 있습니다:

import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';

@Catch()
export class CatchEverythingFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    const { httpAdapter } = this.httpAdapterHost;
    const ctx = host.switchToHttp();

    const httpStatus = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
  }
}

10. Inheritance ✍️

애플리케이션 요구사항을 충족시키기 위해 맞춤화된 예외 필터를 생성할 수 있지만, 기본 제공되는 예외 필터를 확장하여 사용하는 방법도 있습니다. BaseExceptionFilter를 상속받아 기본 필터의 동작을 확장할 수 있습니다:

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);
  }
}

전역 필터로 확장하려면 HttpAdapter 참조를 주입하거나 APP_FILTER를 사용하여 필터를 설정할 수 있습니다:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

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

11. Conclusion ✍️

NestJS에서의 예외 처리는 단순한 오류 메시지 전달을 넘어, 응답 일관성 유지와 로깅, 디버깅 효율성까지 책임지는 중요한 설계 포인트입니다.

단순한 에러 응답은 HttpException과 내장 예외 클래스 (BadRequestException, ForbiddenException 등)로 처리할 수 있고, 보다 구체적인 상황에서는 커스텀 예외 클래스를 만들어 재사용성과 가독성을 높일 수 있습니다. 복잡한 예외 흐름을 관리하려면 Exception Filter를 도입하여, 로깅, 포맷팅, 전역 처리 등 다양한 방식으로 예외를 다룰 수 있습니다.

NestJS는 예외를 ‘흐름 제어’의 한 형태로 보고 있으며, 필터나 상속을 통해 유연하게 커스터마이징할 수 있도록 설계되어 있습니다. 따라서, 예외 처리는 단순히 try-catch에 머무르지 않고, 애플리케이션 전반의 안정성과 UX를 향상시키는 도구로 활용되어야 할 것입니다.

reference: https://docs.nestjs.com/exception-filters#binding-filters

profile
Write a little every day, without hope, without despair ✍️

0개의 댓글