예외 처리와 필터
웹 애플리케이션을 개발하다 보면 다양한 종류의 예외(Exceptions) 상황에 직면하게 됩니다. 예를 들어, 존재하지 않는 리소스에 접근하려 하거나, 데이터베이스 오류가 발생하거나, 사용자 입력이 유효성 검사를 통과하지 못하는 경우가 그렇습니다. 이러한 예외들을 적절히 처리하지 않으면 사용자에게 불친절한 에러 메시지가 노출되거나, 심지어 애플리케이션이 비정상적으로 종료될 수도 있습니다.
NestJS는 강력하고 유연한 예외 처리(Exception Handling) 메커니즘을 제공하여, 애플리케이션의 안정성과 사용자 경험을 향상시킬 수 있도록 돕습니다. 핵심적으로 예외 필터(Exception Filters) 를 통해 이러한 예외들을 중앙 집중식으로 관리할 수 있습니다.
NestJS의 빌트인 예외 처리
NestJS는 기본적으로 표준 HTTP 예외를 처리하기 위한 빌트인(built-in) 예외 계층을 제공합니다. 이는 @nestjs/common
패키지의 HttpException
클래스와 그 하위 클래스들을 통해 구현됩니다. 예를 들어, NotFoundException
, BadRequestException
, UnauthorizedException
등이 있습니다.
예시: 빌트인 예외 사용하기
컨트롤러나 서비스에서 특정 조건이 만족되지 않을 때, 이들 예외 클래스의 인스턴스를 throw
하면 NestJS가 이를 자동으로 감지하여 적절한 HTTP 응답(상태 코드, 메시지 등)을 클라이언트에 반환합니다.
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
를 가진 아이템을 찾고, 만약 아이템이 존재하지 않으면 NotFoundException
을 throw
합니다. NestJS는 이 예외를 가로채서 클라이언트에게 404 Not Found 상태 코드와 함께 지정된 메시지를 반환합니다.
이는 개발자가 일일이 res.status(404).json(...)
과 같은 코드를 작성할 필요 없이, 선언적으로 예외를 처리할 수 있게 해줍니다.
예외 필터: 커스텀 예외 처리
빌트인 예외 처리만으로 충분하지 않은 경우, 즉 특정 예외에 대해 커스텀된 응답 형식을 제공하거나, 추가적인 로깅을 수행하는 등 더 세밀한 제어가 필요할 때는 예외 필터를 사용할 수 있습니다. 예외 필터는 애플리케이션의 모든 계층에서 발생하는 처리되지 않은(uncaught) 예외들을 잡아내어 특정 로직을 수행할 수 있도록 해줍니다.
주요 특징 및 사용 사례
Catch()
데코레이터: 어떤 종류의 예외를 처리할지@Catch()
데코레이터에 지정합니다. 특정 예외 클래스나 여러 예외 클래스를 배열로 지정할 수 있으며, 인자가 없으면 모든 종류의 예외를 처리합니다.ExceptionFilter
인터페이스:ExceptionFilter
인터페이스를 구현하고catch()
메서드를 오버라이드합니다.catch()
메서드는 발생한 예외 객체와ExecutionContext
객체를 인자로 받습니다.- 전역, 컨트롤러, 메서드 레벨 적용:
@UseFilters()
데코레이터를 사용하여 특정 컨트롤러나 메서드에 적용하거나,main.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 형태 외에 timestamp
와 path
정보를 추가한 커스텀 응답을 생성합니다.
예외 필터 적용 방법
메서드 스코프: 특정 메서드에만 적용
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;
}
}
컨트롤러 스코프: 특정 컨트롤러의 모든 라우트에 적용
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 애플리케이션이 부트스트랩될 때 전역으로 등록합니다.
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()
데코레이터에서 참조하는 것이 더 좋은 방법입니다.
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 {}
사용자 정의 예외
NestJS의 HttpException
을 상속받아 우리 애플리케이션의 특정 비즈니스 로직에 맞는 사용자 정의 예외를 생성할 수도 있습니다. 이는 코드의 가독성을 높이고, 특정 예외 상황을 명확하게 표현하는 데 도움을 줍니다.
예시: 사용자 정의 예외 클래스
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
는 이를 자동으로 처리할 것입니다.
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.`;
}
}
예외 처리와 예외 필터는 NestJS 애플리케이션의 견고함과 사용자 친화성을 결정하는 중요한 요소입니다. 적절한 예외 전략을 수립하고 예외 필터를 활용하여 예상치 못한 상황에도 유연하게 대처하는 애플리케이션을 만들어나가시길 바랍니다.