개발/프로젝트

에러처리 리팩토링 / 에러핸들링 예시

prpn97 2023. 10. 25. 23:39

이전 포스트에서 에러처리를 하면서 확인했듯.. 회원 / 비회원 유무를 조회하는데 POST로 하고 있어 자세히 살펴보니, 주민등록번호 앞자리를 받는데, 해쉬해서 저장하고 있었다. 그래서 이 부분은 body로 값을 넘기는 것에 대해 의도가 있기에 넘어가긴 하는데, 다른 부분에서 어차피 다듬어야 할 것들이 너무나도 많아서.. 당장 실질적으로 코드를 고칠 수 없어 포스팅만 하고자 한다. 

 

먼저 SQL인젝션 대비 안되어 있는 부분... 기존 포스팅에서 다뤘으니 넘어가자.. 

// * 회원/비회원 유무
const isUser = async (isUserDto: IsUserDto) => {
  const hashedResidentNumber = await cipherFuc(isUserDto.resident_number);
  const query: string = `
    SELECT u.id, ut.auth_token, u.uuid
    FROM orot.User u
    LEFT JOIN orot.UserToken ut ON u.id = ut.user_id 
    WHERE u.name = '${isUserDto.name}' AND u.resident_number = '${hashedResidentNumber}'
  `;
  const user: any = await sequelize.query(query, { type: QueryTypes.SELECT });
  if (user.length > 0) {
    returnResponse.status = 200;
    returnResponse.success = true;
    returnResponse.message = 'User already exist';
    returnResponse.data = { auth_token: user[0]['auth_token'], uuid: user[0]['uuid'] };
    return returnResponse;
  } else {
    returnResponse.status = 401;
    returnResponse.success = false;
    returnResponse.message = 'User signup possible';
    return returnResponse;
  }
};

 

이미 존재하는 것을 200, 가능한 것을 401을 반환했는데, 이 부분을 서비스에서 컨트롤러로 넘기고 있었다. 

 

이 의도는 회원가입할 때 api를 작동시켜서 특정 값을 반환하는 것이라서 예외적인 내용이지만, 일반적인 경우에서는 에러를 굳이 return하여 컨트롤러로 넘기기보다, 즉시 try-catch문으로 catch에서 에러를 캐치하여 미들웨어의 마지막 단인 에러처리 미들웨어로 보내주는 것이 다른 리소스를 소모하지 않고 바로 넘어갈 수 있겠다. 

 

그렇다면 미들웨어는 어떻게 구성하면 좋을까? 해당 서비스는 JS로 구현되어 있어 TS에서 쓸 경우 타입 지정만 잘 해주면 되겠다. 

1. 에러마다 별도로 인스턴스 설정

- 어떤 에러인지 구분을 두는 것이다. 기본적으로 일반적인지 그렇지 않은지 구분했다.

기준은 클라이언트에게 어떤 에러인지 노출시킬 것인지 말 것인지를 따졌고, 단순 클라이언트의 에러일 경우 보여주겠지만, DB관련된 오류나 에러 내용이 노출되었을 때 이용할 수 있는 위험이 조금이라도 있다면 서버 안에서만 디버깅할 수 있도록 처리한다. 

 

- 내가 처리한 에러 이외에 발견되는 외부 에러 ex) DB / 이미지 등에 대해서는 발견되는 대로 추가하고 있고, DB (Sequelize)관련 에러의 경우 현재는 개발 단계에 있어 db error로 단축해서 표현하고 있지만, 배포단계 이후에는 unexpected error처럼 에러를 파악할 수 없도록 메세지를 제한하도록 바꾸려 한다. 

// 인스턴스를 구분해준다. 
export class CommonError extends Error {
  constructor(status, message, detail) {
    super(message);
    this.status = status;
    this.detail = detail;
    Object.setPrototypeOf(this, CommonError.prototype);
  }
}
export class UncommonError extends Error {
  constructor(status, message, detail) {
    super(message);
    this.status = status;
    this.detail = detail;
    Object.setPrototypeOf(this, UncommonError.prototype);
  }
}


// 인스턴스에 따라 반환을 다르게 설정한다. 
export const errorHandler = (err, req, res, next) => {
  if (err instanceof CommonError) {
    const { status, message, detail } = err;
    const errorResponse = {
      status,
      success: false,
      message,
      detail,
    };
    res.status(status).json(errorResponse);
  } else if (err instanceof UncommonError) {
    const { status, message } = err;
    const errorResponse = {
      status,
      success: false,
      message,
      // detail,
    };
    res.status(status).json(errorResponse);
  } else if (err instanceof JsonWebTokenError) {
    const status = 401;
    const errorResponse = {
      status,
      success: false,
      message: "unauthorized error",
    };
    res.status(status).json(errorResponse);
  } else if (err.name.includes("Sequelize")) {
    const status = errorCode.DB_ERROR;
    console.error(err);
    const errorResponse = {
      status: status,
      success: false,
      message: "db error",
    };
    res.status(status).json(errorResponse);
  } else if (err.name.includes("SyntaxError")) {
    console.log(err);
    const status = errorCode.BAD_REQUEST;
    const errorResponse = {
      status: status,
      success: false,
      message: "올바르지 않은 형식입니다.",
    };
    res.status(status).json(errorResponse);
  } else if (err.code === "LIMIT_FILE_SIZE") {
    const status = errorCode.BAD_REQUEST;
    const errorResponse = {
      status: status,
      success: false,
      message: "파일 사이즈는 5MB 이하로 업로드 가능합니다.",
    };
    res.status(status).json(errorResponse);
  } else if (err.name.includes("MulterError")) {
    console.error(err);
    const status = errorCode.BAD_REQUEST;
    const errorResponse = {
      status: status,
      success: false,
      message: "image error",
    };
    res.status(status).json(errorResponse);
  } else {
    const errorResponse = isSuccess.fail(
      errorCode.INTERNAL_SERVER_ERROR,
      errorMessage.INTERNAL_SERVER_ERROR
    );
    res.status(errorResponse.status).json(errorResponse);
    console.error("non catched in error Handler: ", err);
  }
};

 

 

2. 실제 사용 방법

  맨 위 사례에서는 에러를 정상 응답값을 변경해서 보여주는 방식이였지만, 컨트롤러단에서는 try-catch 구문을 사용하여 try안에서 에러를 throw하여 catch에서 다음 미들웨어로 next(error)하여 던져주도록 하였다. 서비스단이라면 catch에서 throw error 로 넘긴다. 

  다른 내용이라 설명은 크게 안하겠지만, 컨트롤러 단에서는 클라이언트의 오류기 때문에 대다수를 CommonError 로, 서비스 단에서는 DB와 관련된 에러기 때문에 유효성을 검증하여 확인해야 하는 에러는 Common, entity나 별도의 에러는 UncommonError위주로 넘겨주었다. 

export const getUserHeatResult = async (req, res, next) => {
  try {
    const user_id = req.user_id;
    const diagnosis_id = 5;
    const year = req.params.year;
    const month = req.params.month;

    if (month > 12 || month < 1) {   // 생성한 인스턴스로 에러를 정의하여 throw한다. 
      throw new CommonError(
        errorCode.BAD_REQUEST,
        errorMessage.BAD_REQUEST,
        "1~12월만 입력할 수 있습니다."
      );
    }
    const data = await healthyService.getUserDiagnosis(
      user_id,
      parseInt(diagnosis_id),
      year,
      month
    );

    res
      .status(successCode.OK)
      .json(
        isSuccess.success(
          successCode.OK,
          successMessage.CREATE_POST_SUCCESS,
          data
        )
      );
  } catch (error) {
    next(error);   //throw한 에러를 다음 미들웨어로 전달한다. 
  }
};

 

 

코멘트

  이전 포스팅에서 에러처리의 성능에 대해서는 이미 파악했지만, 이번에 포스팅하면서 새삼 에러처리 및 에러를 구분하는 것은 성능적인 측면 외에도 MVC패턴을 지키면서 개발자의 시선에서 협업할 때 가독성 측면에서 크게 도움을 준다고 체감하고 있다. 협업이 아니더라도, 이후 코드를 재사용할 때 의도가 명확하기 때문에 비교적 적은 테스트로 바로 사용이 가능하다. 

 

  왜냐하면 이 것들이 안되어 있는 상태에서 API명세서에 http메서드마저 모호하게 되어있다면 로직의 의도를 한 눈에 파악하기 어렵고, 심지어 저 코드만 해도 네이밍컨벤션이 카멜케이스와 스네이크케이스가 섞여있어서 어지러웠다. (일단 완벽한 내 코드가 아니기 때문에 프론트와 소통해야 하는 req가 있다보니 일단 identation은 db관련된 부분은 스네이크케이스로 두었고, 내가 완벽히 맡는 현재의 프로젝트는 Entity에서 변환해주는 과정을 거치고 전부 카멜 케이스로 통일하고 있다. )

 

  

728x90