티스토리 뷰

728x90
반응형

[참고블로그]

 

[Node Express] 서버에 3 Layer Architecture 적용하기

🍔 Intro. 아래 코드는 3 Layer Architecture를 적용하지 않고 모두 route 폴더에 작성한 예시입니다. 이렇게 작성하면 기능이 복잡해질 때, 한 파일의 코드가 너무 길어지고, 가독성도 떨어집니다. 또, la

velog.io

 

Controller, Service, Repository 가 무엇일까?

찾아본 결과 Controller가 무엇인지 알기 전에 MVC 패턴에 대하여 먼저 아는 것이 중요합니다!MVC패턴은 Model-View-Controller의 약자로서 개발을 할 때 3가지 형태로 역학을 나누어 개발하는 방법론입니

velog.io

 

Spring Service Layer에 대한 고찰

Spring Service Layer에 대한 고찰

velog.io

 

node.js의 module.exports와 exports

lazlojuly의 글 Node.js module.exports vs. exports을 번역했다. node.js의 module.exports와 exports (노트: 이 글은 Node.js 6.1.0 릴리즈 이후에 작성되었습니다.) 요약 는 함수를 사용했을 때 반환 받는 변수라고 생…

edykim.com

 

 

자바스크립트 ES6 모듈 내보내기/불러오기 (import)

Engineering Blog by Dale Seo

www.daleseo.com

 

[깃헙 래포지토리]

제 프로젝트의 코드를 살펴보시고, 더 추가로 보완해야될 부분이 있으면 자유롭게 issue에 기재해주시면 감사하겠습니다. 🙇🏻‍♀️

https://github.com/loveAlakazam/review-share-be-test

 

GitHub - loveAlakazam/review-share-be-test

Contribute to loveAlakazam/review-share-be-test development by creating an account on GitHub.

github.com


[배경]

배경은 코딩테스트 에서 과제물 형식으로 제출했지만 불합격 받았습니다.

기업으로부터 받은 리뷰에서는 Controller 로직안에 서비스와 데이터베이스 처리 로직이 포함되어

방대해진 Controller 로직 때문입니다.

 

이부분은 제가 제출 기간에 단순히 빨리 끝내기 위해서, 아키텍쳐를 신경쓰지 못한 부족함이 있었습니다.

코딩테스트에서 불합격을 받았지만, 불합격에 멈추지 않고

리뷰를 발판삼아서 리팩토링을 하기로 했습니다.

 

리팩토링이 무엇인가요?

결과의 변경 없이 코드의 구조를 재조정함

 

제가 리팩토링 이라는 개념을 들었을 때 떠오르는 것은

어질러진 물건들을 정리정돈하는 것. '방 정리' 입니다.

 

방이 있습니다. 물건도 그대로 있습니다.

정리하기 전과 정리 후에는 모습이 많이 다르긴 하지만, '방' 이라는 것은 변치 않죠.

코드를 개선 이전은 '정리 정돈되지 않은 방' 입니다.

코드를 개선 이후는 '정리 정돈을 한 방' 입니다.

 

결과물의 변경없이, 코드를 정리하는 것 이고

여기서 코드를 정리정돈하는 것 코드의 구조를 개선시키거나, 가독성이 좋게 하는 것 등을 의미합니다.


1. Controller 작성할 때 지켜야하는 규칙사항

컨트롤러는 클라이언트 요청을 받아서 서비스에 전달해야합니다.

여기서는 View가 존재하지 않지만, 컨트롤러에서 처리된 결과를 View에게 전달을 합니다.

컨트롤러 로직의 규칙은  클라이언트로부터 요청한 데이터를 받고, 서비스로 부터의 응답을 리턴해야합니다.


2.  Controller 개선하기

 

서비스 로직을 만들지 않고, 컨트롤러 로직에서 데이터베이스 처리까지 했습니다.

하지만 이는 컨트롤러 로직이 길어지면, 하나의 로직을 읽는데도 오래리는 단점이 있습니다.

 

Node.js 와 express 를 독학한지 시간이 얼마 되지않아

컨트롤러 로직안에 서비스 로직과 데이터베이스 처리 하는 로직이 포함되는 좋지않은 코드가 되었습니다.

 

리팩토링을 하면서 컨트롤러는 입력데이터를 받고, 서비스로부터 응답데이터를 전달하기만 하여 최대한 간결하게 나타내고

실질적인 데이터 처리 부분을 서비스 영역에 , 데이터베이스가 필요한 로직은 데이터베이스 영역 으로

난장판된 코드를 정리해보겠습니다.

 

프로젝트 구조

프로젝트 구조는 다음과 같습니다.

개선이전에는 repository, services, commons 영역이 없었습니다.

services와 repository를 추가하여 방대해진 controllers 로직의 길이를 줄였습니다.

commons/errors.js 는 에러메시지 를 직접 만들어서 리턴해야되는 불편함을 줄이고자 한군데에서만 에러메시지를 관리하는 목적으로 만들었습니다.

래포지토리
ㄴcommons
| ㄴerror.js
|
ㄴcontrollers
| ㄴuserController.js
|    
ㄴmodels
| ㄴUser.js
|    
ㄴrepository
| ㄴusers.js
|    
ㄴroutes
| ㄴusers.js
|    
ㄴservices
  ㄴusers.js

라우터에서 컨트롤러 호출 로직 개선과정

1) 개선 이전

import { Router } from "express";
import {
  showUserById,
  createNewUser,
  updateUserInfo,
  deleteUserById,
  updateSnsList,
} from "../controllers/userController";

const router = Router();

// 유저 생성
router.post("/create", createNewUser);

// 유저 정보 수정
router.put("/update/sns", updateSnsList);
router.put("/update/:id", updateUserInfo);

// 유저삭제
router.delete("/delete/:id", deleteUserById);

// 유저 조회
router.get("/", showUserById);

export default router;

 

2) 개선 이후

URL(Uniform Resource Locator)은 되도록이면 '행동'을 나타내지 않도록 하는 것이 좋습니다.

import { Router } from "express";
import {
  showUserByIdController,
  createNewUserController,
  updateUserInfoController,
  deleteUserByIdController,
  updateSnsListController,
} from "../controllers/userController";

const router = Router();

// 유저 생성
router.post("/", createNewUserController);

// 유저 조회
router.get("/", showUserByIdController);

// 유저삭제
router.delete("/", deleteUserByIdController);

// 유저 정보 수정
router.put("/sns", updateSnsListController);
router.put("/:id", updateUserInfoController);

export default router;

컨트롤러 로직 개선 과정

1) 개선 이전 유저 컨트롤러 로직

다시보니 너무 부끄러운 코드군요...

 

// userController.js

import Users from "../models/Users";
import mongoose from "mongoose";
import ProjectRequests from "../models/ProjectRequests";
import Projects from "../models/Projects";

export const showUserById = async (req, res) => {
  try {
    const { userId } = req.query;
    const userData = await Users.findById(userId);
    res.status(200).json({ user: userData });
  } catch (error) {
    console.log(error);
    res
      .status(500)
      .json({ message: "Internal Server Error", description: error });
  }
};

export const createNewUser = async (req, res) => {
  try {
    const { nickname, birth_of_years } = req.body;
    const newUser = await Users.create({
      nickname,
      birthOfYears: birth_of_years,
    });

    res.status(200).json({
      userId: newUser._id,
    });
  } catch (error) {
    console.log(error);
    res
      .status(500)
      .json({ message: "Internal Server Error", description: error });
  }
};

export const updateUserInfo = async (req, res) => {
  try {
    const { id } = req.params;
    const { snsList, nickname } = req.body;

    if (!id) {
      return res
        .status(400)
        .json({ message: "유저 아이디 값이 존재하지 않습니다." });
    }

    if (!snsList || !nickname) {
      return res
        .status(400)
        .json({ message: "snsList와 nickname 모두 입력해주세요!" });
    }

    const _snsList = Users.checkSnsList(snsList);

    if (!_snsList.length) {
      return res.status(404).json({
        message: "snsList는 Instagram 또는 NaverBlog를 입력해주세요!",
      });
    }

    const user = await Users.findById(id);
    user.nickname = nickname;
    user.snsList = _snsList;
    user.save();

    res.status(204).end();
  } catch (error) {
    console.log(error);
    return res
      .status(500)
      .json({ message: "Internal Server Error", description: error });
  }
};

export const updateSnsList = async (req, res) => {
  try {
    const { userId, snsList } = req.body;
    console.log(userId, snsList);
    if (!userId || !snsList) {
      return res
        .status(400)
        .json({ message: "userId, snsList 데이터가 비어있습니다." });
    }

    const _snsList = Users.checkSnsList(snsList);
    if (!_snsList.length) {
      return res.status(400).json({
        message: "입력한 sns 로 유저의 snsList에 등록할 수 없습니다.",
      });
    }

    // snsList 컬럼 업데이트

    await Users.updateOne({ _id: userId }, { snsList: _snsList });

    res.status(204).end();
  } catch (error) {
    console.log(error);
    return res
      .status(500)
      .json({ message: "Internal Server Error", description: error });
  }
};

export const deleteUserById = async (req, res) => {
  try {
    const { id } = req.params;

    if (!id) {
      return res
        .status(400)
        .json({ message: "userId 데이터가 누락되었습니다." });
    }

    const user = Users.findById(id);
    if (!user) {
      return res
        .status(404)
        .json({ message: "해당 아이디의 유저를 찾을 수 없습니다." });
    }

    let query = { user: id };
    const prjReqList = await ProjectRequests.find(query);
    if (prjReqList.length) {
      // 해당 유저아이디가 포함되어있는 projectRequest들을 삭제합니다.
      await ProjectRequests.deleteMany({ user: id });
    }

    // project의 requestUserList 에서 해당 유저아이디를 제외시킵니다.
    query = { requestUserList: { $in: [id] } };
    const prjList = await Projects.find(query);
    if (prjList.length) {
      prjList.map((prj) => {
        // 각 프로젝트 정보에서 requestUserList 에서 탈퇴유저아이디를 제외시킵니다.
        const _prjReqUserList = prj.requestUserList;
        prj.requestUserList = _prjReqUserList.filter((userId) => {
          // == (Equal Operator)을 사용하여
          // userId(type: ObjectId) 와 id(string) 데이터타입 상관없이 값자체만으로 비교합니다
          return userId != id;
        });
        prj.save();
      });
    }

    // 해당 유저를 삭제합니다.
    await Users.deleteOne({ _id: id });

    res.status(204).end();
  } catch (error) {
    console.log(error);
    return res
      .status(500)
      .json({ message: "Internal Server Error", description: error });
  }
};

 

 

2) 개선 이후 유저컨트롤러 로직

컨트롤러에는 서비스로직만을 불러오도록 했습니다.

또한 입력데이터의 존재유무를 확인하는 로직과 데이터가 존재하지 않으면 에러메시지를 리턴하는 식으로

응답데이터를 전송하는 목적으로 컨트롤러 내부로직을 변경했습니다.

코드구조를 분리시킴으로써 컨트롤러에 있는 코드 의존성이 줄이고자 했습니다.

import * as service from "../service/users";
import * as prjService from "../service/projects";
import * as prjReqService from "../service/projectRequests";
import errorMsgs from "../commons/errors";

export const showUserByIdController = async (req, res) => {
  try {
    const { userId } = req.query;

    const userData = await service.findUserById(userId);

    if (!userData) {
      return res.status(404).json(errorMsgs.NOT_FOUND_USER_ID);
    }

    return res.status(200).json({ user: userData });
  } catch (error) {
    return res
      .status(500)
      .json({ message: "Internal Server Error", description: error });
  }
};

export const createNewUserController = async (req, res) => {
  try {
    const { nickname, birthOfYears } = req.body;
    const newUser = await service.createNewUser(nickname, birthOfYears);

    return res.status(200).json({
      userId: newUser._id,
    });
  } catch (error) {
    res
      .status(500)
      .json({ message: "Internal Server Error", description: error });
  }
};

export const updateUserInfoController = async (req, res) => {
  try {
    const { id } = req.params;
    const { snsList, nickname } = req.body;

    if (!id) {
      return res.status(404).json(errorMsgs.NOT_FOUND_USER_ID);
    }

    if (!snsList) {
      return res.status(400).json(errorMsgs.EMPTY_SNSLIST);
    }

    if (!nickname) {
      return res.status(400).json(errorMsgs.EMPTY_NICKNAME);
    }

    // 유저조회
    const user = await service.findUserById(id);
    if (!user) {
      return res.status(400).json(errorMsgs.NOT_FOUND_USER_ID);
    }

    // snsList 체크
    const isAvailableSNSList = service.checkSnsList(snsList);
    if (!isAvailableSNSList) {
      return res.status(400).json(errorMsgs.NOT_ALLOW_SNSLIST);
    }

    // update userInfo
    const _snsList = service.splitSnsList(snsList);
    const errMsg = await service.updateUserInfo(id, _snsList, nickname);
    if (errMsg) {
      return res.status(400).json(errMsg);
    }

    return res.status(204).end();
  } catch (error) {
    console.log(error);
    return res
      .status(500)
      .json({ message: "Internal Server Error", description: error });
  }
};

export const updateSnsListController = async (req, res) => {
  try {
    const { userId, snsList } = req.body;

    if (!userId) {
      return res.status(400).json(errorMsgs.EMPTY_USER_ID);
    }
    if (!snsList) {
      return res.status(400).json(errorMsgs.EMPTY_SNSLIST);
    }

    // 유저조회
    const user = await service.findUserById(userId);
    if (!user) {
      return res.status(400).json(errorMsgs.NOT_FOUND_USER_ID);
    }

    // snsList체크
    const isAvailableSNSList = service.checkSnsList(snsList);
    if (!isAvailableSNSList) {
      return res.status(400).json(errorMsgs.NOT_ALLOW_SNSLIST);
    }

    // 유저 snsList 정보 수정
    const _snsList = service.splitSnsList(snsList);
    const errMsg = await service.updateSnsList(userId, _snsList);
    if (errMsg) {
      return res.status(400).json(errMsg);
    }

    res.status(204).end();
  } catch (error) {
    console.log(error);
    return res
      .status(500)
      .json({ message: "Internal Server Error", description: error });
  }
};

export const deleteUserByIdController = async (req, res) => {
  try {
    const { userId } = req.body;

    if (!userId) {
      return res.status(400).json(errorMsgs.EMPTY_USER_ID);
    }

    const user = await service.findUserById(userId);
    if (!user) {
      return res.status(404).json(errorMsgs.NOT_FOUND_USER_ID);
    }

    //1. project의 requestUserList 에서 해당 유저아이디를 제외시킵니다.
    await prjService.deleteUserIdFromAllRequestUserList(userId);

    // 2. 프로젝트 리스트 삭제
    const prjReqList = await prjReqService.findProjectRequestsByUserId(userId);
    if (prjReqList.length > 0) {
      await prjReqService.deleteProjectRequestsByUserId(userId);
    }

    // 3. 해당 유저를 삭제합니다.
    await service.deleteUser(userId);

    res.status(204).end();
  } catch (error) {
    console.log(error);
    return res
      .status(500)
      .json({ message: "Internal Server Error", description: error });
  }
};

3. Service & Repository 로직추가

Service 로직 추가

Service 로직은 실질적으로 요청사항을 처리하는  로직을 의미합니다. 처리가 완료되면 컨트롤러한테 전달합니다.

// service/users.js

import * as repository from "../repository/users";
import SNS_LIST from "../commons/snsList";

export const findUserById = async (userId) => {
  try {
    // 유저조회
    const user = await repository.findUserById(userId);
    return user;
  } catch (error) {
    throw error;
  }
};

export const createNewUser = async (nickname, birthOfYears) => {
  try {
    return await repository.createNewUser(nickname, birthOfYears);
  } catch (error) {
    throw error;
  }
};

export const updateUserInfo = async (userId, snsList, nickname) => {
  try {
    // 유저정보 수정
    await repository.updateUserInfo(userId, nickname, snsList);
    return;
  } catch (error) {
    throw error;
  }
};

export const splitSnsList = (snsListStr) => {
  let splitInputSNSList = snsListStr.split(",").map((e) => e.trim());

  // 요소의 중복을 막기 위해 set으로 변환하고 다시 array로 타입변경합니다.
  splitInputSNSList = Array.from(new Set(splitInputSNSList));
  return splitInputSNSList;
};

export const checkSnsList = (snsListStr) => {
  const splitInputSNSList = splitSnsList(snsListStr);
  let result = true;

  splitInputSNSList.forEach((sns) => {
    const r = SNS_LIST.includes(sns);
    result = result && r;
  });

  return result;
};

export const updateSnsList = async (userId, snsList) => {
  try {
    // snsList 컬럼 업데이트
    await repository.updateSnsList(userId, snsList);
    return;
  } catch (error) {
    throw error;
  }
};

export const deleteUser = async (userId) => {
  try {
    await repository.deleteUser(userId);
  } catch (error) {
    throw error;
  }
};

Repository 로직 추가

controller에서 데이터베이스 처리하는 코드를 Repository 로직으로 이동했습니다.

실질적으로 모델을 활용하여 mongodb(데이터베이스)에 접근하여 처리하는 영역 입니다.

import Users from "../models/Users";

export const findUserById = async (userId) => {
  try {
    return await Users.findById(userId);
  } catch (error) {
    throw error;
  }
};

export const createNewUser = async (nickname, birthOfYears) => {
  try {
    return await Users.create({
      nickname: nickname,
      birthOfYears: birthOfYears,
    });
  } catch (error) {
    throw error;
  }
};

export const updateUserInfo = async (userId, nickname, snsList) => {
  try {
    return await Users.updateOne(
      { _id: userId },
      { nickname: nickname, snsList: snsList }
    );
  } catch (error) {
    throw error;
  }
};

export const updateSnsList = async (userId, snsList) => {
  try {
    return await Users.updateOne({ _id: userId }, { snsList: snsList });
  } catch (error) {
    throw error;
  }
};

export const updateRequestCount = async (userId, requestCounts) => {
  try {
    await Users.updateOne(
      { _id: userId },
      { $set: { requestCounts: requestCounts } }
    );
  } catch (error) {
    throw error;
  }
};

export const substractAllUserRequestCounts = async (requestUserList) => {
  try {
    return await Users.updateMany(
      {
        _id: { $in: requestUserList },
        requestCounts: { $gt: 0 },
      },
      { $inc: { requestCounts: -1 } }
    );
  } catch (error) {
    throw error;
  }
};

export const deleteUser = async (userId) => {
  try {
    return await Users.deleteOne({ _id: userId });
  } catch (error) {
    throw error;
  }
};

느낀점

기존에 작업한 코드를 리팩토링하면서 느낀점은 정말 그당시에 머리를 쥐어짠 게 여기까지다. 라고 생각했지만

1~2주 정도의 시간지나서 막상 되돌아보면 참 어리석어보였고, 고치는데도 시간이 걸렸습니다.

단위를 쪼개서 기능단위로 독립적으로 최대한 쪼개보는 것을 목표로했습니다.

 

뿐만아니라 리팩토링하면서, 몽고디비 쿼리문 을 시도해볼 수 있는 기회였습니다.

특히 데이터 수정부분에서 다양한 옵션을 활용했더니

불필요한 코드를 제거함으로써 더 간결하게 코드를 리팩토링할 수 있게 되었습니다.

728x90
반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함