본문으로 건너뛰기

안동민 개발노트

본문 시작

예외 처리와 필터

빌트인 HTTP 예외와 사용자 정의 예외를 발생시키고 예외 필터로 오류 응답 형식과 처리 범위를 일관되게 만듭니다.

웹 애플리케이션을 개발하다 보면 다양한 예외(Exceptions) 상황을 만나게 됩니다.

예를 들어 존재하지 않는 리소스 접근, 데이터베이스 오류, 사용자 입력 유효성 검사 실패가 대표적입니다.

이런 예외를 적절히 처리하지 않으면 사용자에게 불친절한 오류 메시지가 노출되거나, 애플리케이션이 비정상 종료될 수 있습니다.

NestJS는 강력하고 유연한 예외 처리(Exception Handling) 메커니즘을 제공하여, 애플리케이션의 안정성과 사용자 경험을 향상시킬 수 있도록 돕습니다.

핵심적으로 예외 필터(Exception Filters)를 통해 이러한 예외들을 중앙 집중식으로 관리할 수 있습니다.

throw된 예외는 필터를 거쳐 일관된 응답이 된다

서비스나 컨트롤러에서 예외가 발생하면 NestJS는 이를 잡아 HTTP 상태와 응답 본문으로 변환한다. 필터는 이 변환 규칙을 커스터마이즈하는 지점이다.

  1. Throw

    컨트롤러 또는 서비스에서 HttpException이나 도메인 예외를 던진다.

  2. Catch

    예외 필터가 예외와 요청 문맥을 함께 받는다.

  3. Map

    statusCode, message, path, timestamp 같은 응답 필드를 만든다.

  4. Send

    클라이언트가 처리할 수 있는 JSON 응답으로 보낸다.

throw new NotFoundException()
{ statusCode, code, message, path }

NestJS의 빌트인 예외 처리

NestJS는 기본적으로 표준 HTTP 예외를 처리하기 위한 빌트인(built-in) 예외 계층을 제공합니다.

이는 @nestjs/common 패키지의 HttpException 클래스와 그 하위 클래스들을 통해 구현됩니다.

예를 들어, NotFoundException, BadRequestException, UnauthorizedException 등이 있습니다.

예시: 빌트인 예외 사용하기

컨트롤러나 서비스에서 특정 조건이 만족되지 않을 때, 이들 예외 클래스의 인스턴스를 throw하면 NestJS가 이를 자동으로 감지하여 적절한 HTTP 응답(상태 코드, 메시지 등)을 클라이언트에 반환합니다.

src/items/items.controller.ts
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
import { ItemsService } from './items.service';

@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}

  @Get(':id')
  findOne(@Param('id') id: string) {
    const item = this.itemsService.findItemById(id); // 예를 들어, 서비스에서 아이템을 찾는다고 가정
    if (!item) {
      // 아이템이 없는 경우 NotFoundException을 발생시킵니다.
      throw new NotFoundException(`Item with ID "${id}" not found.`);
    }
    return item;
  }
}

위 코드에서 findOne 메서드는 특정 id를 가진 아이템을 찾고, 만약 아이템이 존재하지 않으면 NotFoundExceptionthrow합니다.

NestJS는 이 예외를 가로채서 클라이언트에게 404 Not Found 상태 코드와 함께 지정된 메시지를 반환합니다.

이는 개발자가 일일이 res.status(404).json(...)과 같은 코드를 작성할 필요 없이, 선언적으로 예외를 처리할 수 있게 해줍니다.


예외 필터: 커스텀 예외 처리

예외 필터 응답 변환 흐름

@Catch()로 잡을 예외 범위를 정하고 catch()에서 요청 문맥과 예외를 조합해 응답 구조를 만든다.

  1. 1
    @Catch

    처리할 예외 타입을 선언한다.

  2. 2
    ArgumentsHost

    HTTP 요청과 응답 객체를 꺼낸다.

  3. 3
    Status

    예외 타입에 맞는 상태 코드를 계산한다.

  4. 4
    Body

    클라이언트가 이해할 JSON 필드를 만든다.

  5. 5
    Log

    서버 추적용 상세 정보를 남긴다.

필드의미주의점
statusCodeHTTP 상태 코드예외 타입과 맞아야 한다.
message사용자가 볼 오류 설명내부 스택을 노출하지 않는다.
path요청 경로디버깅과 고객 문의 연결에 유용하다.
timestamp발생 시각로그 검색 기준이 된다.

빌트인 예외 처리만으로 충분하지 않은 경우, 즉 특정 예외에 대해 커스텀된 응답 형식을 제공하거나, 추가적인 로깅을 수행하는 등 더 세밀한 제어가 필요할 때는 예외 필터를 사용할 수 있습니다.

예외 필터는 애플리케이션의 모든 계층에서 발생하는 처리되지 않은(uncaught) 예외들을 잡아내어 특정 로직을 수행할 수 있도록 해줍니다.

주요 특징 및 사용 사례
  • Catch() 데코레이터: 어떤 종류의 예외를 처리할지 @Catch() 데코레이터에 지정합니다. 특정 예외 클래스나 여러 예외 클래스를 배열로 지정할 수 있으며, 인자가 없으면 모든 종류의 예외를 처리합니다.
  • ExceptionFilter 인터페이스: ExceptionFilter 인터페이스를 구현하고 catch() 메서드를 오버라이드합니다. catch() 메서드는 발생한 예외 객체와 ExecutionContext 객체를 인자로 받습니다.
  • 전역, 컨트롤러, 메서드 레벨 적용: @UseFilters() 데코레이터를 사용하여 특정 컨트롤러나 메서드에 적용하거나, main.ts에서 전역으로 적용할 수 있습니다.
예시: 커스텀 예외 필터 작성

클라이언트에게 좀 더 상세하고 일관된 에러 응답 형식을 제공하는 예외 필터를 만들어보겠습니다.

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

@Catch(HttpException) // 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(); // 예외의 HTTP 상태 코드 가져오기

    // HttpException이 아닌 경우, 내부 서버 오류로 처리 (선택 사항)
    const errorResponse = exception.getResponse();
    const errorMessage = typeof errorResponse === 'string'
      ? errorResponse
      : (errorResponse as any).message || 'An unexpected error occurred.';

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: errorMessage, // 클라이언트에게 보여줄 커스텀 메시지
      // errorCode: 'CUSTOM_ERROR_CODE', // 필요에 따라 커스텀 에러 코드 추가
    });
  }
}

HttpExceptionFilter는 모든 HttpException을 잡아내어, 표준 HttpException이 반환하는 JSON 형태 외에 timestamppath 정보를 추가한 커스텀 응답을 생성합니다.

예외 필터 적용 방법

메서드 스코프: 특정 메서드에만 적용

src/items/items.controller.ts
import { Controller, Get, Param, NotFoundException, UseFilters } from '@nestjs/common';
import { ItemsService } from './items.service';
import { HttpExceptionFilter } from '../common/filters/http-exception.filter';

@Controller('items')
export class ItemsController {
  constructor(private readonly itemsService: ItemsService) {}

  @UseFilters(HttpExceptionFilter) // 이 메서드에서 발생하는 HttpException에만 적용
  @Get(':id')
  findOne(@Param('id') id: string) {
    const item = this.itemsService.findItemById(id);
    if (!item) {
      throw new NotFoundException(`Item with ID "${id}" not found.`);
    }
    return item;
  }
}

컨트롤러 스코프: 특정 컨트롤러의 모든 라우트에 적용

src/items/items.controller.ts
import { Controller, Get, Param, NotFoundException, UseFilters } from '@nestjs/common';
import { ItemsService } from './items.service';
import { HttpExceptionFilter } from '../common/filters/http-exception.filter';

@UseFilters(HttpExceptionFilter) // 이 컨트롤러의 모든 라우트에 적용
@Controller('items')
export class ItemsController {
  // ...
}

전역 스코프: 애플리케이션 전체에 적용

가장 일반적이고 권장되는 방법입니다.

main.ts 파일에서 NestJS 애플리케이션이 부트스트랩될 때 전역으로 등록합니다.

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter'; // 필터 임포트

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

  // 전역 예외 필터 등록
  // 주의: new 키워드를 사용하면 해당 필터는 의존성 주입을 받을 수 없습니다.
  // 의존성 주입이 필요한 경우, APP_FILTER 토큰을 사용하여 모듈에서 프로바이더로 등록해야 합니다.
  app.useGlobalFilters(new HttpExceptionFilter());

  await app.listen(3000);
}
bootstrap();

참고: app.useGlobalFilters(new HttpExceptionFilter()) 방식은 필터 내부에서 다른 서비스를 주입받아야 할 경우(예: 로깅 서비스) 문제가 될 수 있습니다.

이럴 때는 APP_FILTER 토큰을 사용하여 AppModule에 프로바이더로 등록하고, @UseFilters() 데코레이터에서 참조하는 것이 더 좋은 방법입니다.

src/app.module.ts (전역 필터로 등록 시 의존성 주입이 필요한 경우)
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
    // ... 다른 프로바이더들
  ],
  // ...
})
export class AppModule {}
예외 필터 적용 범위

예외 필터는 전역, 컨트롤러, 메서드 단위로 적용할 수 있다. 넓게 걸수록 일관성은 커지고, 좁게 걸수록 특수 응답을 만들기 쉽다.

  1. 전체 오류 계약

    서비스 전체가 공유할 응답 규격을 먼저 정한다.

  2. 리소스별 코드

    한 리소스 그룹만 다르면 컨트롤러 범위를 검토한다.

  3. 단일 엔드포인트

    한 라우트만 다르면 메서드 범위로 제한한다.

범위적용 방법적합한 상황
전역app.useGlobalFilters()모든 API 오류 응답 형식을 통일한다.
컨트롤러@UseFilters() on controller한 리소스 그룹의 오류 문맥이 다르다.
메서드@UseFilters() on handler특정 라우트만 특별한 오류 포맷이 필요하다.

사용자 정의 예외

NestJS의 HttpException을 상속받아 우리 애플리케이션의 특정 비즈니스 로직에 맞는 사용자 정의 예외를 생성할 수도 있습니다.

이는 코드의 가독성을 높이고, 특정 예외 상황을 명확하게 표현하는 데 도움을 줍니다.

예시: 사용자 정의 예외 클래스
src/common/exceptions/user-already-exists.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class UserAlreadyExistsException extends HttpException {
  constructor(username: string) {
    super(`User with username "${username}" already exists.`, HttpStatus.CONFLICT); // 409 Conflict
  }
}

이제 이 사용자 정의 예외를 컨트롤러나 서비스에서 throw할 수 있으며, HttpExceptionFilter는 이를 자동으로 처리할 것입니다.

src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { UserAlreadyExistsException } from '../common/exceptions/user-already-exists.exception';

@Injectable()
export class UsersService {
  private users: string[] = ['testuser'];

  createUser(username: string): string {
    if (this.users.includes(username)) {
      throw new UserAlreadyExistsException(username);
    }
    this.users.push(username);
    return `User ${username} created successfully.`;
  }
}

사용자 정의 예외를 쓰면 서비스 코드에는 도메인 의미가 남고, 필터는 이를 클라이언트 응답 규격으로 변환합니다.

사용자 정의 예외 응답

도메인 예외는 서비스 코드에 업무 의미를 남기고, 필터는 이를 클라이언트 응답 규격으로 바꾸는 번역기 역할을 한다.

  1. Service

    중복 사용자 같은 업무 조건을 발견한다.

  2. Exception

    UserAlreadyExistsException처럼 의미 있는 예외를 던진다.

  3. Filter

    예외를 code, status, message로 매핑한다.

  4. Client

    프론트엔드는 code를 기준으로 안내나 재시도를 결정한다.

  5. code

    프론트엔드가 분기할 안정적인 문자열을 제공한다.

  6. message

    사용자가 이해할 문장으로 내부 구현을 감춘다.

  7. details

    입력 필드 오류처럼 복구에 필요한 최소 정보만 담는다.

예외 필터를 설계할 때는 예외를 잡는 것에서 끝내지 말고, 클라이언트가 재시도·로그인 이동·입력 수정 같은 복구 경로를 선택할 수 있도록 응답 형식을 정규화해야 합니다.

예외 필터 계약

필터는 오류를 잡는 것에서 끝나지 않는다. 클라이언트가 다음 행동을 고를 수 있도록 상태, 코드, 메시지, 복구 힌트를 일관되게 제공해야 한다.

  1. 입력 오류

    필드별 메시지를 보여 주고 사용자가 바로 수정한다.

  2. 인증 오류

    토큰 만료를 감지해 로그인 갱신으로 보낸다.

  3. 시스템 오류

    내부 상세 대신 추적 번호와 일반 안내를 표시한다.

들어갈 값클라이언트 행동
status400, 401, 403, 404, 409, 500화면 상태와 재시도 여부를 결정한다.
codeUSER_EXISTS, TOKEN_EXPIRED언어와 무관하게 분기한다.
message사용자에게 보여 줄 설명입력 수정이나 로그인 이동을 안내한다.
tracepath, timestamp, request id운영 로그와 문의를 연결한다.

예외 처리와 예외 필터는 NestJS 애플리케이션의 오류 응답 형식과 복구 전략을 정하는 요소입니다.

도메인 오류, 인증 오류, 시스템 오류를 구분해 일관된 응답 구조로 처리해야 합니다.

Nest 예외 응답 계약

도메인 오류, 인증 오류, 시스템 오류가 제각각 응답하면 프론트엔드는 매번 다른 분기를 가져야 한다. 예외 응답 계약은 이 차이를 하나의 틀로 맞춘다.

  1. 도메인 오류

    중복, 재고 부족, 상태 전이 실패는 409 또는 400 계열로 명확히 표현한다.

  2. 인증 오류

    토큰 없음과 만료를 구분해 로그인 이동 또는 갱신을 선택하게 한다.

  3. 권한 오류

    인증은 됐지만 접근할 수 없는 상황은 403으로 분리한다.

  4. 시스템 오류

    DB 장애와 외부 API 실패는 내부 상세를 숨기고 추적 키를 남긴다.

필드예시이유
codeORDER_STOCK_EMPTY화면 분기 기준
message재고가 부족합니다.사용자 안내
recover수량 조정 또는 장바구니 이동다음 행동 제시