NestJS 빠르게 배우기 05

NestJS 빠르게 배우기 05

지난 글에서는 Middleware에 대해서 알아봤다. 이번 글에서는 Exception filters에 대해서 알아보자.

Nest는 핸들링 되지 않은 모든 예외들에 대해서 반응하도록 하는 내장된 예외 레이어가 있다. 예외가 핸들링 되지 않는다면, 이 레이어에서 잡아서 자동으로 적절한 유저 친화적 응답을 보내준다.


공식 문서에서 제공해주는 이미지

이 동작은 HttpException 타입의 예외를 처리하는 내장된 글로벌 예외 처리기에 의해 동작하는 것이다. 예외가 인식되지 않는 경우 (HttpException이 아니거나, HttpException을 상속하지도 않는 경우) 내장된 예외 처리기가 아래 기본 응답을 만들어준다.

1
2
3
4
{
"statusCode": 500,
"message": "Internal server error"
}

표준 예외 스로잉하기

Nest는 내부장된 HttpException 클래스를 제공한다고 했다. 일반적인 HTTP REST/GraphQL API 기반의 앱을 위해서, 특정한 에러 상황이 발생했을 때 표준 HTTP 응답 객체를 보내는 가장 좋은 방법일 것이다. 예를 들어서, CatsController에서 findAll() 메서드를 가지고 있는 상황을 가정해보자. 이 라우트 핸들러가 몇 가지 이유로 예외를 스로잉 할 때를 가정해보면 아래와 같이 작성할 수 있다.

1
2
3
4
5
6
// cats.controller.ts

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

HttpStatus@nestjs/common 패키지 안에 있는 enum을 담고 있는 헬퍼이다.

이 엔드포인트를 요청한다면, 응답은 아래와 같이 갈 것이다.

1
2
3
4
{
"statusCode": 403,
"message": "Forbidden"
}

HttpException 생성자는 두 개의 인자 값을 요구하는데, 이것들로 응답 값을 결정한다.

  • response: JSON 응답의 바디르 결정하는데 쓰인다. 이 부분은 문자열이 될 수도 있고, 객체가 될 수도 있다. 아래에 무엇을 넘기는지에 따라 어떻게 달라지는지 나온다.
  • status: HTTP 상태 코드 (status code)를 결정하는데 사용된다.

기본 값으로 JSON 응답의 바디 부분은 두 가지 프로퍼티를 가지고 있는 객체이다.

  • statusCode: status 기본적으로는 인자 값으로 넘어온 상태 코드가 들어가게 된다.
  • message: status에 맞는 짧은 문장이 들어가게 된다.

message 부분을 덮어 씌우기 위해서는 response 인자 값에 문자열을 제공해야 한다. 전체 JSON 응답 값을 덮어 씌우려면 response 인자 값으로 객체를 넘기면 된다. Nest는 객체를 시리얼라이즈 하고 JSON의 응답 바디 값으로 넘긴다.

status 인자 값은 유효한 HTTP 상태 코드가 들어가야 한다. 가장 좋은 방법은 HttpStatus enum을 @nestjs/common에서 임포트해와서 사용하는 것이다.

아래 예시는 응답 바디 전체를 덮어 씌우는 것이다.

1
2
3
4
5
6
7
@Get()
async findAll() {
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: "Custom Message"
}, HttpStatus.FORBIDDEN)
}
1
2
3
4
{
"status": 403,
"error": "Custom Message"
}

내장된 HTTP 예외들

Nest는 일반적인 표준 예외들을 HttpException을 기본 베이스로 해서 제공하고 있다. 그리고 이것들은 모두 @nestjs/common 패키지에 속해있다.

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException

예외 필터들

내장된 기본 예외 필터들은 자동적으로 많은 경우를 커버하지만, 예외 레이어의 온전한 제어를 필요로 할 때도 있다. 예를 들어서 로그를 기록하는 툴을 달거나, 다양한 JSON 스키마를 동적인 요소들에 기반해서 사용하는 경우가 잇을 수 있다. 예외 필터(Exception filters)들은 이러한 목적에 맞게 사용할 수 있다. 이 필터들은 명확한 흐름의 통제와 응답 컨탠츠의 통제를 제공해준다.

HttpException 클래스의 인스턴스를 처리하는 예외 필터를 만들어보고, 커스텀한 응답 로직을 실행해보자. 이 작업을 위해서 RequestResponse 객체에 접근해야 한다. Request 오브젝트에서는 오리지날 url과 로깅 정보를 포함하고 있다. Response 객체는 직접적인 응답 컨트롤을 위해서 사용한다. (response.json 메서드를 사용하겠다는 의미이다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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();

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

모든 예외 필터들은 제네릭 ExceptionFilter<T> 인터페이스를 implements 해야 한다. implements 함으로써, catch(exception: T, host: ArgumentsHost) 메서드를 제공하도록 한다. T는 예외의 타입을 가리킨다.

Arguments host

catch() 메서드의 파라메타를 살펴보자. exception 파라메타는 현재 처리되고 있는 예외 객체이다. host 파라메타는 ArgumentsHost 객체이다. ArgumentsHost이 링크(Excution context)의 장에서 더 자세하게 다룰 예정이다. 이번 샘플에서는 RequestResponse 객체를 얻는 용도로 사용된 것만 알고 잇으면 된다. 코드에서 ArgumentsHost에서 헬퍼 메소드를 사용해서 Request Response를 얻을 수 있었다고 정도만 알자. 더 많은 ArgumentsHost에 대해서 보려면 이 링크 (위 링크와 같다)에서 확인해보자.

이 단계에서 추상적인 이유는 ArgumentsHost 함수들이 모든 context 안에 있기 때문이다. (즉, 지금은 HTTP 서버의 컨텍스트를 사용하고 있지만, MSA라든지, WebSockets에서도 사용된다.) 실행 맥락 챕터에서 어떻게 ArgumentsHost와 그것의 헬퍼 메소드들을 사용해서 어떤 실행 환경에서든 적절한 arguments에 접근하는지 볼 수 있다. 이러한 특징은 제네릭 예외 필터를 어떤 맥락에서든 동작하게 작성할 수 있게 해준다.

Binding filters

위에서 만든 HttpExceptionFilterCatsControllercreate() 메서드에 묶어보도록 하겠다.

1
2
3
4
5
6
7
// cats.controller.ts

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

@UseFilters() 데코레이터도 @nestjs/common에 있다.

적용할 때는 @UseFilters() 데코레이터를 사용했다. @Catch() 데코레이터와 유사하게, 하나의 필터 인스턴스나, 쉼표로 구분된 필터 인스턴스들을 넣을 수 있다. 위에서는 HttpExceptionFilter의 인스턴스를 넣어줬다. 근ㄷ 인스턴스 말고 클래스를 넣어줘도 된다.

클래스를 넣는 것을 선호하는게 좋다. 메모리 사용을 줄여준다. Nest가 쉽게 전체 모듈에서 같은 클래스의 인스턴스를 쉽게 사용할 수 있게 해준다.

위 예시에서 HttpExceptionFilter는 하나의 create() 라우터 핸들러 메서드에만 적용되어있다. 예외 필터는 컨트롤러 스코프, 글로벌 스코프 등으로도 사용될 수 있다 (위 예시에서는 메서드 스코프로 사용된 거임). 예를 들어서 컨트롤러 스코프로 만들어보자면, 아래와 같이 사용하면 된다.

1
2
@UseFilters(new HttpExceptionFilter())
export class CatsController {}

이 작업은 HttpExceptionFilterCatsController에서 정의한 라우터 핸들러 모두에게 적용하도록 한다.

글로벌 스코프의 필터를 만들려면 아래와 같이 사용하면 된다.

1
2
3
4
5
6
7
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}

bootstrap();

글로벌 스코프 필터는 앱 전반적으로 모두 사용된다. 의존성 주입 관점에서, 어떤 모듈의 외부로부터 등록된 글로벌 스코프 필터는 의존성을 주입할 수 없다. 왜냐하면 어떤 모듈의 맥락 밖에서 이미 실행된 것이기 때문이다 (?). 이러한 이슈를 해결하려면 글로벌 스코프 필터를 아래 처럼 생성된 모듈로부터 직접적으로 등록할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app.module.ts

import { Module } from "@nestjs/common";
import { APP_FILTER } from "@nestjs/core";

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

Catch everything

모든 핸들링되지 않은 예외를 캐치하려면, @Catch() 데코레이터의 파라메타 리스트를 빈 상태로 두면 된다. 아래 예시는 각 예외들이 던져질 때 클래스의 타입과 관계 없이 캐치하는 필터이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus
} from "@nestjs/common";

@Catch()
export class AllExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();

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

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

상속

일반적으로, 완전하게 커스터마이징 된 예외 필터를 앱의 요구 사항에 맞춰 만들 것이다. 하지만 내장된 기본 global exception filter를 확장할 일도 있을 거다. 기본 필터가 예외 처리하는 것을 대신하기 위해서 BaseExceptionFilter를 확장하고, catch() 메서드를 상속받으면 된다.

1
2
3
4
5
6
7
8
9
10
// all-exception.filter.ts
import { Catch, ArgumentsHost } from "@nestjs/common";
import { BaseExceptionFilter } from "@nestjs/core";

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

위의 실행은 쉘에서 접근을 보여주는 것이다. 확장된 예외 필터의 실행은 비지니스 로직을 포함하고 있어야 한다.

글로벌 필터는 베이스 필터를 확장할 수 있다. 두 가지 방법 모두 가능하다. 첫 번째 메서드는 HttpServer 참조를 주입하는 것이고, 두 번째 방법은 위에서 사용한대로 APP_FILTER 토큰을 사용하는 것이다. 아래는 HttpServer 참조를 주입하는 방식이다.

1
2
3
4
5
6
7
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { httpAdapter } = app.get(HttpAdapterHost); // HttpServer의 참조를 가져오는 방법인 것 같다.
app.useGlobalFilters(new AllExceptionFilter(httpAdapter));

await app.listen(300);
}

후기

사실 내용 중에 이해 못 한 부분도 몇 개 있다. 원하는 내용은 소수고 별로 안 쓰겠는데 싶은 내용은 많았다. 뒤에 내용은 FP를 위한 도구이거나, 권한과 관련해서 조금 더 Nest를 잘 사용하는 방법, Interceptor ?, 커스텀 데코레이터가 남아 있긴 한데, 지금까지 공부한 내용과 TypeORM 붙이는 방법만 확인해보면 간단한 TODO List API 서버를 만드는 데 문제가 없을 것 같다. Quicklearn 시리즈는 이쯤 정리하고, 간단한 데모를 만들어보고, Nest에서 GraphQL을 잘 사용하는 방법에 대해서 공부해봐야겠다.

Reference

Author

changhoi

Posted on

2020-03-26

Updated on

2020-03-26

Licensed under

댓글

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×