Studying/JavaScript & Frameworks

[Node.js 떠먹여 주는 남자] 컨트롤러, 서비스 를 이용한 샘플 코드 확장

국장 지킴이 앨런 2022. 3. 8. 23:53
반응형

컨트롤러와 서비스는 무엇인가?

시작하기에 앞서 왜 컨트롤러와 서비스가 필요한 지 간단하게 짚고 넘어가도록 하겠습니다. 지난 포스트
2022.03.07 - [Studying/Node.js & Express.js] - Node.js 부먹편 - Node.js, Express.js 를 이용한 샘플 코드 를 확인하셨다면, 우리는 이미 클라이언트 사이드에서 요청을 받고, routes 를 통해서 응답을 전달할 수 있다는 것을 확인하였습니다.

여기, 컨트롤러와 서비스에 관하여 잘 설명되어 있는 글이 있어 줍줍 해왔습니다.

The controller takes what it needs from Express (or whatever framework you're using),
does some checking/validation to figure out to which service(s) should the data from the request be sent to,
and orchestrates those service calls.

So there is some logic in the controller, but it is not the business logic/algorithms/database calls/etc
that the services take care of. Again, the controller is a manager/supervisor.

정리해 보면, 컨트롤러의 역할은

  • 요청 밸리데이션 (파라미터, 쿼리 스트링 등) 수행 후 어떤 서비스 혹은 서비스들이 이 요청을 수행해야 하는지 확인하는 역할을 한다.
  • 컨트롤러는 비지니스 로직, 알고리즘, 데이터베이스 오퍼레이션 등을 수행하지 않고, 이 역할은 서비스가 하게 된다.
  • 컨트롤러는 매니저/수퍼바이저 역할을 담당한다.

위의 설명에서 볼 수 있듯, 컨트롤러는 유저의 요청을 받고, 이 요청을 수행하기 위해서 어떠한 서비스를 사용하여야 하는지 확인하는 역할을 한다고 합니다. 서비스의 역할도 이미 나와 있지만 recap 개념으로 한번 더 보도록 하겠습니다.

The service is responsible for getting the work done and returning it to the controller. It contains the business logic that is necessary to actually meet the requirements and return what the consumer of the API is requesting.

서비스의 역할은 위에서도 보셨듯, 비지니스 로직, 알고리즘, 데이터베이스 오퍼레이션 등, 유저의 요청에 응답하기 위하여 필요한 작업들을 수행하는 역할을 합니다.

출처: https://www.coreycleary.me/what-is-the-difference-between-controllers-and-services-in-node-rest-apis/

그렇다면, 컨트롤러와 서비스는 왜 필요한가?

앞서 말했듯, 우리는 샘플 코드에서 routes 만으로 가장 기본적인 API 응답 핸들링을 완성시킬 수 있었습니다. 만약 저 간단한 코드로 사용자의 요구 사항이 모두 만족 된다면, 컨트롤러, 서비스, 모델 등 다 필요 없겠지요. 하지만 왜 샘플 코드겠습니까? 그냥 샘플이지 끝이 아닙니다. 가장 기본중의 기본 뼈대 역할일 뿐이며, 저는 이런 식으로 웹 서버 개발을 시작한다 라는 것을 보여드리고 싶었습니다.

만약 한 곳에서 모든 요청 처리 과정을 다 작성한다면, 다음과 같은 문제가 발생할 수 있습니다.

  1. 애플리케이션의 규모가 커질 수록 코드가 복잡해진다.
  2. 코드가 복잡해지면 가독성이 떨어지며, 문제 발생 시 트러블슈팅에 많은 시간이 소요될 수 있다.
  3. 쿨하지 않다.

반대로, 컨트롤러와 서비스, 루트를 구별하여 코드를 작성했을 시에 얻을 수 있는 이점들에 대한 제 생각은 이렇습니다.

  1. 복잡한 코드를 목적에 맞게 작성하여 임포트 함으로서 어떤 부분이 어떤 역할을 수행하는지 명확하게 구분할 수 있게 된다.
  2. 위의 결과로 가독성이 향상될 수 있으며, 트러블슈팅 또한 쉬워진다.
  3. 각종 테스트 케이스들을 작성하기 용이해진다.
  4. 코드 이쁘고 간결하게 짜면 기분이 좋아진다.

위에 열거된 단점들을 반대로 뒤집으면 제가 생각하는 장점이 됩니다. 그 이유는 둘 다 제 머리에서 나온 것이기 때문이죠. 저것 외에도 많은 것들이 있을 수 있지만 지극히 제 개인적인 사견이므로, 이 글을 읽으시는 분들께서도 다른 의견이 있으시다면, 망설이지 말고 댓글로 남겨주시기 바랍니다

본격 코드 작-성

별다른 설명 없이 바로 코드 작성 부분으로 넘어갈 수도 있었지만 이렇게 주저리주저리 미사여구를 앞에 많이 붙인 이유는 제가 공부를 하기 위함이었습니다.

 

하지만!

 

만약 이런 개념들에 대하여 모르셨거나, 대충 들어만 봤다, 혹은 뭐하러 알아야 되나 라는 생각을 하셨던 분들이 계시다면 (꽤 많을 거라 생각합니다. 저도 그 중 한 명 이었으므로...), 이번 기회에 좀 더 자세히 알아보시는 것을 추천드립니다. 경력직 이직을 위해서 물론 알고리즘도 중요하겠지만, foundation 을 탄탄히 다지는 것 역시 중요하다는 것을 몇번의 면접을 통해서 깨닫게 되었습니다. 사설은 이정도로 마무리 짓고 컨트롤러와 서비스 파일을 생성 후 루트 파일에서 필요한 부분을 옮겨보도록 하겠습니다. 그럼..


userController.js

"use strict";
const userService = require('../services/userService');
const { validateUserId } = require('../helper/helper'); // req.params.id 가 존재하는지 확인하는 헬퍼 펑션

module.exports = {
  getUsers: (req, res, next) => {
    const users = userService.getUsers();
    res.status(200).json(users);
  },

  getUserById: (req, res, next) => {
    // helper 펑션을 만든 이유는 여러가지 endpoints 에서 파라미터 유저 아이디 체크가 반복적으로 행하여지기 때문입니다.
    validateUserId(req, res, next);
    const userId = Number(req.params.id);
    try {
      const user = userService.getUserById(userId);
      res.status(200).json(user);
    } catch (e) {
      res.status(404).json({ message: e.message });
    }
  },

  createNewUser: (req, res, next) => {
    // 아래의 4가지 프로퍼티가 모두 required 라는 가정 하에 validation 을 진행하고, 만약 만족하지 않을 시 400 Bad Request 리턴
    if (
      !req.body.name || !req.body.gender || !req.body.country || !req.body.job
    ) {
      res.status(400).json({
        message: 'Invalid request body. Must include name, gender, country, and job.'
      });
      next();
    }
    const newUser = userService.createNewUser(req.body);
    res.status(201).json(newUser);
  },

  updateUserById: (req, res, next) => {
    validateUserId(req, res, next);
    try {
      const updatedUser = userService.updateUserById(Number(req.params.id), req.body);
      res.status(200).json(updatedUser);
    } catch (e) {
      res.status(404).json({ message: e.message });
    }
  },

  deleteUserById: (req, res, next) => {
    validateUserId(req, res, next);
    try {
      const deletedUser = userService.deleteUserById(Number(req.params.id));
      // 만약 유저가 성공적으로 삭제 되었다면, 어레이의 형태로 삭제 된 유저의 정보가 리턴됩니다.
      deletedUser.length > 0 ? res.status(200).json({ deleteUser: deletedUser[0] })
        : res.status(500).json({ message: `Failed to delete the user with ID ${req.params.id}` });
    } catch (e) {
      res.status(404).json({ message: e.message });
    }
  }
};

userService.js

"use strict";
const users = require('../../mockData/users'); // 유저 목 데이터

module.exports = {
  getUsers: () => {
    return users;
  },

  getUserById: (userId) => {
    const user = users.find(user => user.id === userId);
    if (user) {
      return user;
    } else {
      throw new Error(`User entry with ID ${userId} does not exist.`);
    }
  },

  createNewUser: (userInfo) => {
    userInfo.id = users.length + 1;
    users.push(userInfo);
    return userInfo;
  },

  updateUserById: (userId, userInfo) => {
    const targetUser = users.find(user => user.id === userId);
    if (!targetUser) {
      throw new Error(`User with ID ${userId} does not exist.`);
    }
  
    for (const [key, value] of Object.entries(userInfo)) {
      targetUser[key] = value;
    }

    return targetUser;
  },

  deleteUserById: (userId) => {
    const userIdx = users.findIndex(user => user.id === userId);
    if (userIdx === -1) {
      throw new Error(`User with ID ${userId} does not exist.`);
    }
    // 목 유저 어레이에서 타겟 유저를 삭제해 주고 그 유저 디테일을 리턴해 줍니다.
    return users.splice(userIdx, 1);
  }
}

userRoute.js

'use strict';
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');

router.get('/', userController.getUsers);
router.get('/:id', userController.getUserById);
router.post('/', userController.createNewUser);
router.put('/:id', userController.updateUserById);
router.delete('/:id', userController.deleteUserById);

module.exports = router;

이렇게 컨트롤러, 서비스 파일을 추가하여 줌으로서, 더욱 직관적으로 컨트롤러, 서비스, 루트 파일들이 각각 어떤 역할을 수행하는지 알 수 있게 되었습니다? 저는 실제로 일을 할 때도 이 구조를 바탕으로 API 를 개발해 왔습니다. 물론 개발자의 성향이나 회사의 규칙 (?) 등에 따라 조금씩 다를 수도 있겠지만, 이 스트럭쳐도 아주 많이 쓰이는 흔한 구조입니다.

마지막으로 현재 프로젝트 폴더 구조가 어떻게 바뀌었는지 보여드리면서 오늘은 이만 사라져 보겠습니다.

프로젝트 폴더 구조

이렇게 해서 샘플 코드에 컨트롤러와 서비스 파일을 추가해 보았습니다. 다음 포스트에서는 Model 파일을 추가하여 Models-Routes-Controllers-Service 구조를 완성 시키고, 실제로 데이터베이스 를 연결해서 목 유저 데이터가 아닌 실제 데이터베이스 핸들링 하는 코드를 작성하여 보도록 하겠습니다. 그럼

반응형