ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • NestJS 꼭 알아야 되는 Exception filter 주의점
    Nest.js 2024. 8. 14. 17:31
    728x90
    NestJS 여러개의 exception filter와 모든 exception catch filter 사용시 주의할점에 대해서 알아보자.

    예시 Exception filter 종류

    • BadRequestFilter : 400 bad request 를 catch 하는 filter
    • ServiceHttpExceptionFilter : service 에러 catch 하는 filter
    • AllExceptionFilter : 모든 에러를 catch 하는 filter
    // exception
    app.useGlobalFilters(
        new BadRequestServiceExceptionFilter()
        new ServiceHttpExceptionFilter(),
        new AllExceptionFilter(httpAdapterHost),
    );

     

    Interceptor나 guard를 생각 했을때 BadRequestServiceException 먼저 실행되고 ServiceHttpExceptionFilter가 실행되고 AllExceptionFilter가 실행 되겠지라고 생각 했지만 실제로 코드 결과는 전혀 달랐다. 예측 할 수 없는 결과가 나왔다.

     

    결과적으로 
    @Catch() 데코레이터에 인자값에 들어가 있는 exception에 해당하는 에러를 catch 하는 것이다.
    // ServiceHttpExceptionFilter
    @Catch(ServiceException)
    export class ServiceHttpExceptionFilter implements ExceptionFilter {
    	catch(exception: ServiceException, host: ArgumentsHost) {
        ...
        
    // BadRequestServiceExceptionFilter
    @Catch(BadRequestException)
    export class BadRequestServiceExceptionFilter implements ExceptionFilter {
        constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
        ...
    
    // AllExceptionFilter
    @Catch()
    export class AllExceptionFilter implements ExceptionFilter {
    	constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
    	catch(exception: unknown, host: ArgumentsHost) {
    BadRequestServiceException는 BadRequestException에 해당하는 에러들을 catch하고
    ServiceHttpExceptionFilter는 ServiceException에 해당하는 에러들을 캐치하고
    AllExceptionFilter 는 모든 에러를 캐치 한다.

     

    1. AllExceptionFilter에서 모든 에러를 catch 하는것이 아니다.

    • 여기서 중요한것은 AllExceptionFilter가 모든 에러를 다 catch 한다고 했으니까 BadRequestException과 ServiceException을 체크하는 로직을 넣어주어야 될것 같지만 전혀 그렇지 않았다. 이미 BadRequestServiceExceptionFilter와 ServiceHttpExceptionFilter에서 catch하고 남은 모든 에러들을 AllExceptionFilter에서 catch 한다. (실제로 코드를 구현하여 출력을 해보았을때 BadRequestException의 에러가 AllExceptionFilter에서 catch 되지 않았다.)
    @Catch()
    export class AllExceptionFilter implements ExceptionFilter {
      catch(exception: unknown, host: ArgumentsHost) {
      	// 이짓을 안해도 된다는 소리다.
        if (exception instanceof ServiceException) {
          throw exception;
        }
        
        ...

     

     

    2. every catch filter를 함께 사용 한다면 꼭 제일 먼저 사용 해야한다.

    WarningWhen combining an exception filter that catches everything with a filter that is bound to a specific type, the "Catch anything" filter should be declared first to allow the specific filter to correctly handle the bound type.

     

    " 모든 것을 캐치하는 예외 필터와 특정 유형에 바인딩된 필터를 결합하는 경우, 특정 필터가 바인딩된 유형을 올바르게 처리할 수 있도록 '무엇이든 캐치' 필터를 먼저 선언해야 합니다." 
    위의 내용은 NestJS 공식문서에서 가져온 내용으로 꼭 기억 해두어야 하는 중요 내용이다. 

     

    // (X)
    app.useGlobalFilters(
        new BadRequestServiceExceptionFilter()
        new ServiceHttpExceptionFilter(),
        new AllExceptionFilter(httpAdapterHost),
    );
    
    // (O)
    app.useGlobalFilters(
    	new AllExceptionFilter(httpAdapterHost), // 고정
        new BadRequestServiceExceptionFilter()
        new ServiceHttpExceptionFilter(),
    );

     

    모든 에러를 catch하는 AllExceptionFilter를 만들어서 사용하게 된다면 꼭 제일 처음 선언 해주어야 한다.
    그렇지 않으면 다른 filter들이 정상적으로 에러들을 catch 하지 못한다. 

    3. throw를 통해서 다른 exception filter로 전달 할 수 없다.

    @Catch()
    export class AllExceptionFilter implements ExceptionFilter {
      catch(exception: unknown, host: ArgumentsHost) {
        if (exception instanceof ServiceException) {
          throw exception;
        }
    allExceptionFilter에서 에러를 가져온 후에 무언가 특정한 상황일때 해당하는 filter에게 넘겨주고 싶을때가 있다.
    결론부터 말하자면 그런식으로 사용 할 수 없다. throw 하는 방법 따윈 존재 하지않는다.
    allExceptionFilter에서 throw -> BadRequestFilter catch (x) 이런식으로 사용 불가능하다.
    allExceptionFIlter가 가장 먼저 실행 하니까 throw 하면 될것 같다는 생각이 들지만 위의 설명대로 allExceptionFilter에서는 BadRequestException 에러와 ServiceException 에러들을 캐치하지 못한다는것을 기억 해야한다.

     

    4. httpAdpator를 이용하여 reply를 통해 response 전달

    공식문서를 참고하여 httpAdaptor와 reply를 통해 response를 전달한다.
    // main.ts
    const httpAdapterHost = app.get(HttpAdapterHost);
    
    // exception
    app.useGlobalFilters(
        new AllExceptionFilter(httpAdapterHost),
        ...
    )
    // AllExceptionFilter.ts
    
    @Catch()
    export class AllExceptionFilter implements ExceptionFilter {
    
    	constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
        
    	catch(exception: unknown, host: ArgumentsHost) {
    		
    		const ctx = host.switchToHttp();
    		const { httpAdapter } = this.httpAdapterHost;
    		const request = ctx.getRequest<Request>();
    
    		const { ip, method, originalUrl } = request;
    		const userAgent = request.get('user-agent');
    
    		const statusCode =
    			exception instanceof HttpException
    				? exception.getStatus()
    				: HttpStatus.INTERNAL_SERVER_ERROR;
                    
            if (exception instanceof QueryFailedError) {
            
                const response = InternalServerErrorException(
                    ERROR_INTERNAL_SERVER_ERROR,
                );
    
                const responseBody = response.toHttpExceptionResponse(
                    httpAdapter.getRequestUrl(ctx.getRequest()),
                );
    
                httpAdapter.reply(ctx.getResponse(), responseBody, statusCode);
    
        	}
    예를들어 TypeORM에서 에러가 발생 한다면 
    httpAdapterHost를 의존성 주입 받아 QueryFailedError 즉 TypeORM에서 발생한 에러라면 httpAdapter.reply를 통해서 response를 보낸다.
    TypeORM에서 발생한 에러는 치명적인 에러기 때문에 log를 수집하는 로직을 추가하면 좋을것이다.
    결론적으로 특정 에러 exception에 맞는 Response가 있다면 그것을 만들어서 httpAdapter.reply를 통해서 에러를 response 해주면 된다.

     

    5. 예시 exception filter를 도식화 한 그림

     

     

    Reference

    - ( https://docs.nestjs.com/exception-filters )

    728x90
Designed by Tistory.