[해보고자 하는 것]
1. 3 Layered Architecture 의 서비스 계층에서 발생한 유효성 검사의 처리를 에러 처리 미들웨어에서 진행한다.
2. 기존 swtich문으로 모든 에러 case를 기재하였던 것을 새로운 에러 클래스를 이용하여 효율적으로 관리한다.
#1. 에러 처리 미들웨어
이번 3 계층형 아키텍쳐(3 Layered Architecture)로 리팩토링을 진행하면서 Service 계층에서 모든 유효성 검사를 진행했다.
다만, 서비스 계층에서는 res를 내보내지 않기로 생각했기 때문에 별도로 에러를 처리하는 미들웨어가 필요했다.
에러처리 미들웨어를 사용하게 된다면 발생한 에러를 처리할 때(클라이언트에게 응답할 때)
응답하는 방식 혹은 구성을 다르게 할 경우 에러처리 미들웨어에서만 변경하면 되는 장점이 있다.
에러처리 미들웨어가 작동되는 로직은 아래와 같다.
1. 컨트롤러에서 받은 데이터로 실질적인 비즈니스 로직을 실행하는 서비스 계층에서 유효성 검사 진행
2. 유효성 검사를 통과하지 못할 경우 throw Error로 에러를 발생
3. 서비스 계층에서 에러를 받게될 경우 try...catch 문에 의하여 catch 문 안에 있는 next 메서드 실행
4. next를 통해 에러 처리 미들웨어로 넘어온 후 각 에러에 맞는 응답(Response) 전달
# 컨트롤러 파일 예시
/* Controller */
import { UsersService } from "../services/users.service.js";
export class UsersController {
usersService = new UsersService();
signUp = async (req, res, next) => {
try {
const { email, userName, password, confirmPassword } = req.body;
const signupUser = await this.usersService.signUpUser(
email,
userName,
password,
confirmPassword
);
return res.status(201).json({ data: signupUser });
} catch (error) { // try 문 안에서 에러가 발생할 경우 error와 함께 next 메서드 실행
next(error);
};
};
... 이하 코드 생략
};
# 서비스 파일 예시
/* Service */
import { UsersRepository } from '../repositories/users.repository.js'
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
export class UsersService {
usersRepository = new UsersRepository();
signUpUser = async (email, userName, password, confirmPassword) => {
// 이메일 형식이 아닐 경우 Error 메시지 발송
const emailForm = /^([0-9a-zA-Z_\.-]+)@([0-9a-zA-Z_-]+)(\.[0-9a-zA-Z_-]+){1,2}$/;
if (!emailForm.test(email)) {
throw new Error("이메일 형식이 올바르지 않습니다.");
};
// 입력 받는 데이터가 없는 경우 !
if (!email || !userName || !password) {
throw new Error("가입 정보를 모두 입력해주세요.");
};
const existUser = await this.usersRepository.findUserByEmail(email);
// 기존 유저가 있을 경우
if (existUser) {
throw new Error("이미 가입된 이메일 입니다.");
};
// 입력된 password 길이가 6자 미만일 경우 Error 메시지 발송
if (password.length < 6) {
throw new Error("비밀번호는 최소 6자 이상 입력되어야 합니다.");
};
// password와 confirmPassword 값이 일치하지 않을 경우 Error 메시지 발송
if (password !== confirmPassword) {
throw new Error("비밀번호를 확인해주세요.");
};
// 해쉬된 비밀번호를 이용하여 데이터베이스에 저장한다.
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = await this.usersRepository.signupUser(
email,
userName,
hashedPassword);
return {
userId: newUser.userId,
userName: newUser.userName,
email: newUser.email,
createdAt: newUser.createdAt,
updatedAt: newUser.updatedAt
};
};
... 이하 코드 생략
};
# 에러처리 미들웨어 파일 예시 (Switch 문)
/* ErrorHandling */
const errorHandling = async (err, req, res, next) => {
console.error(err);
let statusCode = 0
switch (err.message) {
/* 회원가입 유효성 검사 */
case "이메일 형식이 올바르지 않습니다.":
statusCode = 400;
break;
case "가입 정보를 모두 입력해주세요.":
statusCode = 400;
break;
case "이미 가입된 이메일 입니다.":
statusCode = 400;
break;
case "비밀번호는 최소 6자 이상 입력되어야 합니다.":
statusCode = 400;
break;
case "비밀번호를 확인해주세요.":
statusCode = 401;
break;
default:
statusCode = 500;
err.message = "알 수 없는 오류가 발생했습니다. 관리자에게 문의하세요."
break;
};
return res.status(statusCode).json({ message: err.message });
};
export default errorHandling;
맨 처음 위와 같은 코드로 에러처리 미들웨어를 구상했다.
하지만 코딩을 하면서도 한편으로는 아래 의문점이 들었다.
1. 에러 메시지에 맞는 케이스를 확인하기 위해 굳이 에러 메시지를 다시 가져와야하는 번거로움을 없앨 수는 없는가?
2. 기존 에러는 statusCode를 가지지 못 하는데 가지게 할 수는 없는가?
고민에 대한 해답은 아래와 같이 새로운 에러 클래스를 만들면서 해결이 되었다.
#2. 에러 핸들링 파일 리팩토링
앞서 언급한 바와 같이 각 에러별로 내가 원하는 상태코드를 반환해주고자 할 경우
switch 문에서 각 에러별 케이스를 구분해줘야한다. (나는 에러 메시지 값으로 구분했다.)
사실 기존의 에러는 message 값 외의 다른 값을 가질수 없기 때문이기도 했다.
찾아보니 Error는 클래스이고, 클래스는 빵틀의 역할로 인스턴스를 생성할 때의 기준이 된다.
위 스위치 문에서 보다시피 내가 만드는 에러들은 statusCode와 message를 공통적으로 가지고 있기 때문에 새로운 에러 클래스를 만들수 있을 것이라고 생각했다.
# 에러 처리 미들웨어 (수정)
/* ErrorHandling using Class */
export class ValidationError extends Error {
constructor(message, statusCode) {
super(message)
this.name = "ValidationError"
this.statusCode = statusCode
};
};
const errorHandling = async (err, req, res, next) => {
console.error(err);
/* 예상치 못한 에러가 발생했을 경우 500코드와 에러 메시지 반환 */
if (!err.statusCode) {
return res.status(500).json({ message: "알 수 없는 에러가 발생했습니다. 관리자에게 문의하세요." });
}
/* 그 외 상황에 맞는 상태코드와 에러 메세지 반환 */
return res.status(err.statusCode).json({ message: err.message });
앞선 코드에서는 유효성 검사가 추가될 때마다 에러 처리 미들웨어에서도 case 를 추가해줘야하는 단점이 있었다.
하지만 위 리팩토링한 코드는 유효성 검사가 추가되더라도 서비스 계층에서만 추가하면 되는 장점이 있다.
해당 미들웨어를 사용하기 위해 서비스 계층에서의 throw Error 부분도 아래와 같이 수정했다.
# 서비스 파일 예시 (수정)
/* Serivece */
import { UsersRepository } from '../repositories/users.repository.js'
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { ValidationError } from '../middlewares/error.middleware.js';
export class UsersService {
usersRepository = new UsersRepository();
@@ -10,29 +11,29 @@ export class UsersService {
// 이메일 형식이 아닐 경우 Error 메시지 발송
const emailForm = /^([0-9a-zA-Z_\.-]+)@([0-9a-zA-Z_-]+)(\.[0-9a-zA-Z_-]+){1,2}$/;
if (!emailForm.test(email)) {
throw new ValidationError("이메일 형식이 올바르지 않습니다.", 400);
};
// 입력 받는 데이터가 없는 경우 !
if (!email || !userName || !password) {
throw new ValidationError("가입 정보를 모두 입력해주세요.", 400);
};
const existUser = await this.usersRepository.findUserByEmail(email);
// 기존 유저가 있을 경우
if (existUser) {
throw new ValidationError("이미 가입된 이메일 입니다.", 400);
};
// 입력된 password 길이가 6자 미만일 경우 Error 메시지 발송
if (password.length < 6) {
throw new ValidationError("비밀번호는 최소 6자 이상 입력되어야 합니다.", 400);
};
// password와 confirmPassword 값이 일치하지 않을 경우 Error 메시지 발송
if (password !== confirmPassword) {
throw new ValidationError("비밀번호를 확인해주세요.", 401);
};
... 아래 코드 생략
기존 Error는 message만 가지고 있었다면,
새롭게 만든 클래스(ValidationError) 의 경우 statusCode 값을 가질수 있어 메시지 뒤에 2번째 인자로 넣어줄 수 있다.
이 에러처리 미들웨어에서 respose를 하는 것에는 변함이 없지만 케이스를 추가하는 번거로움이 없어진다.
개인적인 판단이기는 하나 훨씬 효율적이고 가독성도 더 좋게 변경된 듯 하다.
(에러처리하기 까지의 비즈니스 로직이 변경된 부분은 없고 코드구조만 변경되었다.)
물론 이것도 정답이 아닐 수 있지만
현재 내가 찾은 상황에서는 최선이라 할 수 있겠다.
혹시라도 추가 혹은 수정이 필요하다면 수정을 꼭 해보리라 ~
'Node.js 도전기' 카테고리의 다른 글
| Node.js_3 Layerd Architecture 로 리팩토링하기 ! (2) (2) | 2023.12.08 |
|---|---|
| Node.js_3 Layerd Architecture 로 리팩토링하기 ! (1) (1) | 2023.12.08 |
| Node.js_객체 지향 설계 5원칙(SOLID) (1) | 2023.12.06 |
| Node.js_객체 지향 프로그래밍 (OOP) (1) | 2023.12.05 |
| Node.js_트랜잭션 (Transaction) (2) | 2023.12.04 |