본문 바로가기
창고(2021년 이전)

[NodeJS] 에러, 그리고 예외처리(in Express)

by 측면삼각근 2020. 3. 5.
728x90
반응형

Java로 프로젝트를 진행 할 때 예외처리가 프로그램의 견고함을 위하여 필요하다고 배웠고. 알고는 있었다.
프로젝트에서 예외처리를 하고, express 에서 에러와 예외 처리에 대해 공부하고 적용하면서, 사실 내가 알고 있던 부분은 사실 내리 배움으로 예외처리에 대하여 예외사항 시 에러를 발생시키고 에러는 try catch이 필요한 정도만 두루뭉술히 알고있다는 것을 알게되었다. (분명, 더 디테일하게 배웠었겠지만, 내가 기억하지 못하는거겠지)

또.. 사실 여러 자료를 보면 볼수록 (내가 개발을 할 때 기반적으로 생각하는)JAVA에서의 개념이 더 명확히 잡혀있지 않아서 JS에서도 헷갈리고 있다는 생각이 들기 시작했다. 따라서 JAVA에서의 에러와 예외개념부터 시작하여, JS에서의 에러와 예외 개념, Express에서 프로젝트에서 구현했던 에러와 예외 예시정도로 간단히 프로젝트 리뷰를 하려고 한다.


error vs excpetion in java

자바에서는 실행 시 발생할 수 있는 오류를 에러(error)와 예외(exception) 두가지로 구분한다.

오류(Error)
오류는 시스템에 비정상적인 상황이 생겼을 때 발생한다.
이는 시스템 레벨에서 발생하기 때문에 심각한 수준의 오류이다. 발생 시 복구할 수 없는 심각한 오류이다.

예외(exception)
예외는 발생하더라도 수습 할 수 있을 정도의 비교적 덜 심각한 오류이다.

에러는 발생 시 막을 방도가 없지만, 예외는 프로그래머가 예외처리를 통해서 비정상 종료를 막을 수 있다.

예외는 발생할 상황을 미리 예측하여 처리 할 수 있다. 즉, 예외는 개발자가 처리 할 수 있기 때문에 예외를 구분하고 그에 따른 처리 방법을 명확히 알고 적용하는 것이 중요하다.

예외클래스

모든 예외 클래스는 Throwable클래스를 상속 받고 있으며, Throwable은 최상위 클래스 Objec의 자식 클래스이다.
Throwable을 상속받는 클래스는 Error와 Exception이 있다. Error은 시스템 레벨의 심각한 수준의 에러이기 때문에 시스템에 변화를 주어 문제를 처리해야 하는 경우가 일반적이다. 반면에 Excpetion은 개발자가 로직을 추가하여 처리 할 수 있다.

이 중 RuntimeExcpetion은 CheckedException과 UncheckedException을 구분하는 구분하는 기준이다. Excption의 자식 클래스 중 RuntimeExcption을 제외한 모든 클래스는 CheckedExecption이며, RuntimeException과 그 자식들은 Unchecked Exception이라 부른다.

Checked Exception과 Unchecked Exception의 가장 명확한 구분 기준은 '꼭 처리를 해야 하느냐'이다.

  Checked Exception Unchecked Exception
처리여부 반드시 예외를 처리해야함 명시적인 처리를 강제하지 않음
확인시점 컴파일 단계 실행단계
예외발생시 트랜잭션 처리 roll - back 하지 않음 roll-back 함
대표 예외 Exeption의 상속받는 하위 클래스 중 Runtime Exeption을 제외한 모든 예외 RuntimeException 하위 예외
- NullPointerExeption
- illegalArgumentException
- IndexOutOfBoundException
- SystemException

RuntimeException 클래스를 상속받는 자식 클래스들은 주로 치명적인 예외 상황을 발생시키지 않는 예외들로 구성된다. 하지만 그 외의 Exception 클래스에 속하는 자식들은 치명적인 예외 사항을 발생시키므로, try / catch문을 사용하여 예외를 처리해야만 한다.

Checked Exception이 발생할 가능성이 있는 메소드라면 반드시 로직을 try/catch로 감싸거나 throw로 던져서 처리해야 한다.
Unchecked Exception은 명시적인 예외처리를 하지 않아도 된다. 이 예외는 피할 수 있지만, 개발자가 부주의해서 발생하는 경우가 대부분이고, 미리 예측하지 못했던 상황에서 발생하는 예외가 아니기 때문에 굳이 로직으로 처리를 할 필요가 없도록 만들어져있다.

예외 처리 방법

  1. 예외 복구 -> 다른 작업 흐름으로 유도
  2. 예외 처리 회피 -> 처리하지 않고 호출한 쪽으로 throw
  3. 예외 전환 -> 명확한 의미의 예외로 전환 후 throw

*"가장 위험한 것은, 예외를 잡고 아무 처리도 하지 않는것이다. 오류는 나지 않겠지만, 예외가 발생했을 때 그 원인을 파악하기 어려워 개발은 물론 유지보수에 치명적 일 수 있다"*


Exception in JS, 유의할점!

기본적으로 JS에서도 예외와 에러의 개념적 차이는 Java에서와 동일하다.(좀 찾아본 바로는 exception에 대한 명확한 클라스는 없는것같다)
다만 한가지 유의할 점이있다면, 비동기 함수에서 에러 발생시 await이 없다면 아무리 try...catch문 안에 있다고 하더라도 작동하지 않는다.


Express에서의 효율적인 예외처리

프로젝트에서 에러 및 예외처리를 정리하고 획일화 하면서 여러가지 고민을 했던 것 같다.
주요한 고민 사항은 아래와 같았다.

  1. 모든 Controller 마다 try... catch문이 있어야 할까?
  2. 예외 발생시 res.response를 해주는 것 보다, 더 명확한 방법은 없을까?

두번째 res.response에 대한 응답은 당연히 throw new Error를 해주는 방법이 보편적이지만, 당시에는 채 생각하지 못하고 400번대 에러를 각각의 에러 발생지점에서 반환해주고 있었다. (지금 와서 생각해보니, 이러한 방식은 당연히 지양된다고 기억하고 있다)

java프로젝트 코드를 다시 열어보고 구글도 뒤적뒤적 공부를 하고 고민을 하다가 Error를 상속받아서 만든 CustomError를 _throw_해주고, _express 의 에러처리기_에서 처리를 해주는 방식으로 리펙토링 해주기로 했다.
예외 처리의 방법 중 3번째 예외 전환에 해당 할 것이다.

커스텀 에러 생성자 만들기

custonError.ts

export default class CustomError extends Error {
    type: string;

    status: number;

    constructor(type = "GENERIC", status = 500, message = "default error", ...params: any) {
        super(...params);

        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, CustomError);
        }

        this.type = type;
        this.status = status;
        this.message = message;
    }
}

자바스크립트에서 상속은 위험한 것이지만, 이 경우는 매우 유용하다. 네이티브 JS 에러 생성자를 확장하면 stack trace를 얻을 수 있다.
추가하고 확장 자를 통해 나중에 접근 할 수 있기 위함이다.

이후 에러처리 부분에서 res.send.status(400)...이러한 방법이 아니라, 아래와 같은 코드로, type과 status, message 를 보내는 것으로 규격화 하였다.

throw new CustomError("DAO_Exception", 409, "Update succeeded, but failed to retrieve comment information.");

error Wrapper function만들기

모든 controller 마다 모두 try...catch문이 있어야할까? 코드블럭이 모든 컨트롤러마다 중복적으로 들어가니, 가독성이나 코드의 효율의 측면에서 고민을조금은 오래 했었고, 적어도 내가 계속해서 찾았던 답들은 그렇다 였다.
음.. 계속해서 찾다보니 그렇지만, 그렇지 않다! 였다!
이것은 에러 처리를 위한 익스프레스 가이드 에서 방법을 찾아 적용한 것이다.

const helper = (fn:Function) => (req:Request, res:Response, next:NextFunction) => {
    fn(req, res, next).catch(next);
};

위와같은 error Wrapper함수를 만들고, 모든 Controller 마다 helper 함수를 넣어준다.
즉, Controller의 코드는 helper함수의 parameter Function인 것이다.
helper함수를 통해 모든 Controller는 에러가 나면 catch문에 의해 catch되어 nextfunction으로 향한다.
try catch 문이 있지만, 없는 것이다.
여태까지의 흐름에서 error Wrapper과 CustomError를 활용한 Controller코드는 다음과 같다

const catlist = helper(async (req:express.Request, res:express.Response) => {
    const { authorization } = req.headers;
    const userId = getUserIdbyAccessToken(authorization);

    const getCats:Array<object> = await UserService.getCatList(userId);
    if (!getCats) throw new CustomError("DAO_Exception", 409, "User's list not found");

    res.status(200).send(getCats);
});

nextfunction으로 처리된 Error는, express의 에러처리기로 향한다.
express의 에러처리기를 작성하는 방법은 여러가지가 있겠으나, 우리프로젝트에서 나는 index.ts파일의 단순화를 위해 에러처리기 또한 미들웨어처럼 작성하고 활용하였다.

에러처리기에 도달한 에러들은 각각의 특성에 따라 분류되고, 조금 더 체계화 되었으며 일괄적인 에러 및 메세지를 응답한다.

const typeORMError = (error:Error, req:Request, res:Response, next:NextFunction) => {
    if (error instanceof QueryFailedError) return res.status(400).json({ type: "QueryFailedError", message: error.message });
    next(error);
};

const jwtError = (error:Error, req:Request, res:Response, next:NextFunction) => {
    if (error instanceof TokenExpiredError) return res.status(401).json({ type: "TokenExpiredError", message: error.message });
    if (error instanceof JsonWebTokenError) return res.status(400).json({ type: "JsonWebTokenError", message: error.message });
    next(error);
};

const customErrorHandler = (error:any, req:Request, res:Response, next:NextFunction) => {
    if (error instanceof CustomError) {
        const { status, type, message } = error;
        return res.status(status).send({ type, message });
    }
    next(error);
};

const etcError = (error:Error, req:Request, res:Response, next:NextFunction) => {
    res.status(500).json({ type: "etcError", message: error.message });
};

에러 및 예외처리를 통한 리펙토링을 완료하고 일관성이 떨어지던 예외에 대한 응답을 통일 할 수 있었고, 클라이언트와의 소통이 원활해지고 디버깅시간이 확연히 줄어들어 유지보수가 용이해졌다고 확신한다.


번외 - JS의 에러의 유형

JS에서는 일반적인 Error생성자 외에도 7개의 중요 오류 생성자가 존재한다.

  • EvalError : 전역함수 eval()에서 발생하는 오류 인스턴스
    -> JS에서 더이상 발생하지 않지만, 하위 호환성을 위해 남아있다.
  • InternalError : JS엔진 내부에서 오류가 발생했음을 나타내는 오류
  • RangeError : 숫자 변수나 매개변수가 유효한 범위를 벗어났음을 나타내는 오류
  • ReferenceError : 잘못된 참조를 했음에 나타내는 오류
  • SyntaxError : eval()이 코드를 분석하는 중 잘못된 구문을 만났음을 나타내는 오류
  • TypeError : 변수나 매개변수가 유효한 자료형이 아님을 나타내는 오류

참조

MDN_JS_제어흐름과에러처리
JAVA 예외(Exception)처리에 대한 작은생각
제어 흐름과 에러처리
JS_예외처리
에러 처리를 위한 익스프레스 가이드
javascript 에러 처리 방법
MDN_ERROR
자바스크립트 에러 핸들링 : 신뢰할 만한 가이드

반응형