모든 에러 처리하는 Exception Filter 생성기

영슈·2023년 9월 18일
0

인턴십-NestStreaming

목록 보기
2/2
post-thumbnail

created: 2023-09-16


깃허브 주소

Nest-Streaming

진행 일지

1.[nest-streaming:feat] Follow 기능 생성 & 삭제 완성
2.[nest-streaming:convention] Error Handling 하는 Exception
https://github.com/node9137/nest-streaming/pull/8

기록할 점

비즈니스 로직 단에서 발생하는 커스텀 에러 구조 와 커스텀 에러 처리 및 전역적 발생 에러 처리 를 위한 고민
=> Class 단위 Custom Error 와 nest 에서 제공하는 Filter 단위 구현

기존의 Error 처리


export interface CommonBaseError {
    status: false;
    businessCode: number;
    message: string;
}

export interface NotExistedUser extends CommonBaseError{
    status : false;
    businessCode : 1002,
    message:"정보에 일치하는 회원이 없습니다."
}

------------------------------------------------

if(isExistedUser)
	throw typia.random<AlreadyExistedUser>();

  • interface 로 common 생성후 , 항상 상수값인 interface 를 그냥 생성하여 throw
    - Business Logic 내 발생한 에러인지 다른 에러인지 구분하기 힘듬.
    - 기본적인 Nest 에서 발생하는 Error 들은 Class 단위로 인한 불일치 발생
    - 어디서 에러가 발생했는지 , 에러마다 추가적인 로직 처리시 매우 힘듬
    => Class 단위 에러 생성 및 처리 단위도 Class 에서 처리하자!

Exception Filter in Nest

  • 응용 프로그램 내 처리되지 않은 모든 예외 처리 위한 예외 처리 레이어 제공
{ "statusCode": 500, "message": "Internal server error" }

=> HTTP exception 및 상속된 Class 가 아닌 경우 기본 JSON Response

  • 기본 제공 HttpException Layer
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
export declare class HttpException extends Error {
	constructor(response: string | Record<string, any>, status: number, options?: HttpExceptionOptions);
}

=> options 에는 cause(발생 원인) 와 description (설명) 존재

  • BadRequest , Forbidden , NotFound , Conflict 등 일반적인 Error 들 존재

ExceptionBase

export abstract class ExceptionBase extends Error {
	
	abstract code: string;
	public readonly correlationId: string;
	
	constructor(
	readonly message: string,
	readonly businessCode:number,
	readonly cause?: Error
	) {
	super(message);
	Error.captureStackTrace(this, this.constructor);
	}
	toJSON(): SerializedException {
		return {
			status:false,
			message: this.message,
			code: this.code,
			businessCode : this.businessCode,
			cause : this.cause !== undefined ? JSON.stringify(this.cause) : undefined,
		};
	}
}
  • message 와 businessCode 와 Error 발생 원인을 담는 cause
  • Error.captureStackTrace 를 통해 , 발생한 전체 경로 담는 함수
    => 백엔드 단에서 에러가 발생한 함수 및 경로 추적 가능후 차후 처리 가능 +
    프론트 엔드에서 에러 이해 및 비즈니스 코드 통한 로직 분기 처리 가능

Exception Filter

import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
BadRequestException,
} from "@nestjs/common";

import { HttpAdapterHost } from "@nestjs/core";
import { BadRequestException as BadRequest } from "src/errors/bad-request-error";
import { DatabaseFailedException } from "src/errors/database-failed.error";
import { ExceptionBase } from "src/errors/exception.base";
import { ServerFailedException } from "src/errors/server-failed.error";
import { TypeORMError } from "typeorm";

  

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
static readonly dbErrorMessage = "데이터베이스 오류입니다.";
constructor(
private readonly httpAdapterHost: HttpAdapterHost,
) {}

	catch(exception: unknown, host: ArgumentsHost): void {
		const { httpAdapter } = this.httpAdapterHost;
		const ctx = host.switchToHttp();
		if(exception instanceof ExceptionBase){
			const data = exception.toJSON();
			httpAdapter.reply(ctx.getResponse(),data,400);
		}
		else if(exception instanceof BadRequestException){
			httpAdapter.reply(ctx.getResponse(),new BadRequest(),400)
		}
		
		else if(exception instanceof TypeORMError){
			httpAdapter.reply(ctx.getResponse(),new DatabaseFailedException,400);
		}
		
		else if(exception instanceof HttpException){
			const status = exception.getStatus();
			httpAdapter.reply(ctx.getResponse(),exception,status);
		}
		else{
			httpAdapter.reply(ctx.getResponse(),new ServerFailedException,500);
		}
	}
}
  • Catch 를 통한 모든 Error Interceptor
  • if 문 과 else if 문 을 통한 분기 처리 ( swtich 문을 통해 할 시 , exception 이 boolean 타입으로 변환 ! )
  • EceptionBase 의 Instance 일 시 -> Custom Error
  • else if 문 을 통해 , 추가적인 Error 검증 가능
  • 모든 Error 를 Catch

=> 서버가 의도치 않은 부분에서 중단 및 종료 방지 + Logging 이나 다른 방향으로 Handling 가능

Sample

export class NotExistedUser extends ExceptionBase{
	static readonly message = "정보에 일치하는 회원이 없습니다."
	public readonly code = 'USER.NOT_EXIST'
	static readonly businessCode = 1002
	
	constructor(cause?:Error,metadata?:unknown){
		super(NotExistedUser.message,NotExistedUser.businessCode,cause)
	}
}

=>

{ 
	"status": false, 
	"message": "정보에 일치하는 회원이 없습니다.", 
	"code": "USER.NOT_EXIST", 
	"businessCode": 1002 
}
  • response 는 toJSON 을 통해 JSON 화

  • error.stack 출력시 , 이렇게 발생 경로 출력
    ( 이때 주의해야 할 점은 이 stack 은 string type)

심화 포인트

중복 Catch 하는 두개의 Interceptor 가 있을시?

사용할 이유?

  • Catch () 로 모든 Exception 을 catch 하나 , 다른 Class 에 로직 분리하고 싶을 시
  • Module 마다 다른 Exception Filter 를 사용하고 싶을 시

First Case

@Module({
	imports: [ConfigModule.forRoot({}), AlarmModule, TestMoudle],
	controllers: [],
	providers: [
	{
	provide:APP_FILTER,
	useClass:BadRequestExceptionFilter
	},
	{
	provide:APP_FILTER,
	useClass:AllExceptionsFilter
	},
	],
})
  • 이런 중복 Catch 하는 Filter 가 있을시 => 하단 Filter 가 적용됨. ( Decorator 때문이라 생각 )

Second Case

app.module.ts
@Module({
	imports: [AlarmModule],
	providers: [
	{
	provide:APP_FILTER,
	useClass:AllExceptionsFilter
	},
	],
})
alram.module.ts
@Module({
	imports: [AlarmModule],
	providers: [
	{
	provide:APP_FILTER,
	useClass:BadRequestExceptionFilter
	},
	],
})
  • 내부 Module 에 있는 Interceptor 로 적용

장점

  • 차후 , Redis 나 Logger 를 활용해 에러 핸들링 시 , Injection 용이
  • 각기 다른 Filter 적용 용이
  • 현재 Context 를 받는 ctx 를 통해 다양한 추가 작업 가능!
    ( 어떤 Request 가 Error 를 발생시켰는지 or 어떤 data 가 Error 를 발생 시켰는지 )

참고

https://github.com/Sairyss/domain-driven-hexagon/blob/master/src/libs/application/interceptors/exception.interceptor.ts
https://docs.nestjs.com/exception-filters

Writed By Obisidan
profile
https://youngsu5582.life/ 로 블로그 이동중입니다~

0개의 댓글