본문 바로가기
TIL

TIL 240612 - 레이어를 분리하면 서비스랑 미들웨어에서 에러처리를 어떻게 하는가

by lemonpie611 2024. 6. 12.

3-Layered Architecture를 분리하고나서 마주한 두가지 문제점

 

1) 에러처리는 Controller에서 다 하는데, 미들웨어에서도 에러처리를 해도 되는건가..?

2) Service 계층에서 모든 에러를 throw new Error 로 처리하니까 콘솔창에만 에러메시지가 뜨고 insomnia에서는 안뜬다..

 

근데 이걸 모두 한번에 해결해주는 방법이 있었음. 에러를 담을 클래스를 생성하여, 에러가 발생하는 위치에 throw new (클래스이름) 형식으로 객체를 만들어 next로 던져주면 된다.

 

그러니까,

// ./error/http-error.js

import { HTTP_STATUS } from '../const/http-status.const.js';

class BadRequest {
  constructor(message = BadRequest.name) {
    this.message = message;
    this.status = HTTP_STATUS.BAD_REQUEST;
  }
}

class Unauthorized {
  constructor(message = Unauthorized.name) {
    this.message = message;
    this.status = HTTP_STATUS.UNAUTHORIZED;
  }
}

.
.

export const HttpError = {
  BadRequest,
  Unauthorized,
  Forbidden,
  NotFound,
  Conflict,
  InternalServerError,
};

이런식으로 에러 status별 클래스를 만들어 export 해주고,

// ./service/auth.service.js
import { AuthRepository } from '../repositories/auth.repository.js';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { MESSAGES } from '../const/messages.const.js';
import { HttpError } from '../error/http.error.js';

export class AuthService {
  authRepository = new AuthRepository();
  .
  .
  //로그인
  signIn = async (email, password) => {
    const user = await this.authRepository.findUserInfoByEmail(email);
    if (!user) {
      throw new HttpError.NotFound(MESSAGES.AUTH.SIGN_IN.IS_NOT_EXIST);
    }
    if (!(await bcrypt.compare(password, user.password))) {
      throw new HttpError.Unauthorized(MESSAGES.AUTH.SIGN_IN.PW_NOT_MATCHED);
    }
    const { accessToken, refreshToken } = await this.refreshToken(user.userId);
    return { accessToken, refreshToken };
  };
  .
  .
}

throw new Error 대신 throw new HttpError.NotFound('에러메시지') 라고 작성하면 status와 에러메시지가 모두 에러처리 미들웨어로 전달된다.

// ./middleware/access-token.middleware.js

import jwt from 'jsonwebtoken';
import { AuthRepository } from '../repositories/auth.repository.js';
import { MESSAGES } from '../const/messages.const.js';
import { HttpError } from '../error/http.error.js';

export default async function (req, res, next) {
  try {
    const authRepository = new AuthRepository();
    const authorization = req.headers.authorization;
    if (!authorization) {
      throw new HttpError.BadRequest(MESSAGES.JWT.NONE);
    }

    const [tokenType, accessToken] = authorization.split(' ');

    if (tokenType !== 'Bearer') {
      throw new HttpError.Unauthorized(MESSAGES.JWT.NOT_TYPE);
    }

    if (!accessToken) {
      throw new HttpError.Unauthorized(MESSAGES.JWT.NONE);
    }

    const decodedToken = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET_KEY);
    const userId = decodedToken.id;

    const user = await authRepository.findUserInfoById(userId);
    if (!user) {
      throw new HttpError.Unauthorized(MESSAGES.JWT.NO_MATCH);
    }

    req.user = user;
    next();
  } catch (err) {
    next(err);
  }
}

미들웨어도 마찬가지로 이런식으로 하면 된다. next(err)로 넘어간 나머지 에러들은 에러처리 미들웨어에서 다룬다.

// ./middlewares/error-hander.middleware.js

import { MESSAGES } from '../const/messages.const.js';
import { HTTP_STATUS } from '../const/http-status.const.js';

export default function (err, req, res, next) {
  console.error(err);
  if (err.name === 'ValidationError') {
    return res.status(HTTP_STATUS.BAD_REQUEST).json({
      status: HTTP_STATUS.BAD_REQUEST,
      message: err.message,
    });
  } else if (err.name == 'TokenExpiredError') {
    return res.status(HTTP_STATUS.UNAUTHORIZED).json({
      status: HTTP_STATUS.UNAUTHORIZED,
      message: MESSAGES.JWT.EXPIRED,
    });
  } else if (err.name == 'JsonWebTokenError') {
    return res.status(HTTP_STATUS.UNAUTHORIZED).json({
      status: HTTP_STATUS.UNAUTHORIZED,
      message: MESSAGES.JWT.NOT_AVAILABLE,
    });
  }

  return res.status(err.status).json({
    status: err.status,
    message: err.message,
  });
}

에러핸들러 미들웨어에 jwt 관련 에러를 if로 처리한 결과.