Layered Architecture Pattern 을 통한 express 모듈화

Pien·2022년 9월 4일
2

위코드

목록 보기
6/10

지난 시간에 Node.js와 Express를 이용해 하나의 파일안에 CRUD API 시스템을 구축했다.
API의 모든 기능이 하나의 파일에 집약 되어 코드의 재사용성, 유지 보수, 확장성 등에 문제가 있다.
오늘은 Layered Pattern 을 이용해 기존에 생성한 API를 모듈화로 변경 하고자 한다.

Layered Pattern

Layered Pattern은 백엔드 API에 가장 널리 적용되는 패턴 중 하나다.
논리적 부분, 역할에 따라 독립된 모듈이 층층히 쌓듯이 연결 되어 레이어를 쌓아놓은 것 같은 구조가 된다고 하여 레이어 패턴이라고 한다.

Presentation Layer

시스템을 사용하는 사용자, 클라이언트 시스템과 연결되는 레이어다.
백엔드 API의 엔드포인트에 해당하며, HTTP 요청을 읽는 로직을 구현한다. 그 이상의 역할, 로직은 다음 레이어로 전달 한다.

Business Layer

시스템이 구현해야 하는 로직, 역할을 구현하는 레이어다.
회원가입시 비밀번호에 조건을 거는등 운영에 필요한 로직이 들어 있는 레이어다.

Persistence Layer

DB와 관련된 로직을 구현하는 레이어다.
DB의 데이터 생성, 수정, 읽기를 처리하는 역할을 한다.

단방향 의존성, 관심사 분리

Layered Pattern의 핵심 요소는 단방향 의존성과 관심사 분리이다.
단방향 의존성은 레이어는 자기보다 하위에 있는 레이어에만 의존하고 있음을 의미한다.
관심사 분리는 레이어는 역할이 명확히 구분되어 있어 역할의 중첩이 없음을 의미한다.

폴더 구조

westagram
├── node_modules
├── package.json
├── routes
│   ├── index.js
│   ├── userRoute.js
│   └── postRoute.js
├── services
│   ├── userService.js
│   └── postService.js
├── controllers
│   ├── userController.js
│   └── postController.js
├── models
│   ├── userDao.js
│   └── postDao.js
└── app.js

기존의 app.js 파일을 관심사 분리를 통해 routes, services, controllers, models 총 네개의 폴더, 폴더 안에 각각의 파일로 분리 시켰다.


Layered Pattern에 따른 역할 분리

서버 구동

서버 구동은 app.js 에서 그대로 진행 한다.
서버 구동에 필요 하지 않는 필요 없는 코드 들은 제거 했다.

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const routes = require("./routes");

const app = express();
const PORT = process.env.PORT;

app.use(cors());
app.use(morgan('dev'));
app.use(express.json());
app.use(routes);

app.get("/ping", (req,res) => {
  res.status(200).json({"message" : "pong"});
});

const start = async () => {
  try {
    app.listen(PORT, () => console.log(`Server is listening on ${PORT}`));
  } catch (err) {
    console.error(err);
  }
}

start();

이전에 서버 실행 코드에 비해 추가된 것은require("./routes")app.use(routes) 이다. app.js 에서 코드를 바로 실행 시키는 것이 아니기 때문에, 걸쳐가는 routes 를 미들웨어로 실행 시켰다.

Routes

Routes 는 외부의 요청을 하위 레이어로 전달 시키는 역할을 한다. 보통 index.js파일에 Routes를 모아 관리 한다.

//index.js
const express = require("express");
const router = express.Router();

const userRouter = require("./userRouter");
const postRouter = require("./postRouter");

router.use("/users", userRouter.router);
router.use("/post", postRouter.router);

module.exports = router
//userRouter.js
const express = require("express");
const router = express.Router();

const userController = require("../controllers/userController");
router.get("/:userId", userController.userPost);
router.post("/signup", userController.signUp);

module.exports = {
    router
}

http 통신이 들어오면 index.js 파일의 엔드포인트를 거친 뒤 각자의 Routes로 넘겨준다. 위의 코드를 예시로 들면 /users/signup 엔드포인트를 받은 경우 index.js의 Routes를 거쳐 userRouter.js 파일의 엔드포인트에 도달한다.

Controller

Routes 에서 받은 엔드포인트를 정의 하고, HTTP 요청을 읽어 들이는 로직을 구현한다.
Controllers는 데이터의 검증 작업도 주로 하는 공간이다.

const userService = require("../services/userService");

const signUp = async (req, res) => {
    try {
        const { name, email, password, profileImage } = req.body;
        if( !name || !email || !password || !profileImage) {
            return res.status(400).json({ message: "KEY_ERROR" });
        }
        await userService.signUp( name, email, password, profileImage );

        res.status(201).json({ message: "SIGNUP_SUCCESS" });
    } catch (err) {
        console.log(err);
        return res.status(err.statusCode || 500).json({ message: err.message });
    }
};

module.exports = {
    signUp
}

HTTP 요청에 대한 4가지 값을 받아 하나의 값이라도 존재하지 않는 경우 400 ERROR를 발생 시키는 로직을 구현했다.
4가지 값이 모두 존재 하며 문제가 없을 경우 점차 하위 레이어로 진입해 최하위 레이어에도 진입했을 때 문제가 없을 경우 이곳으로 돌아와 201 Created 코드와 메시지를 반환해 준다.

Services

규칙과 로직을 적용하는 공간이다. 이곳에서 Controllers 에서 받은 데이터를 내가 원하는 데이터를 DB에 접근하기 위한 로직을 구현하고, 원하는 표현식을 사용한다.

const userDao = require("../models/userDao");
const signUp = async ( name, email, password, profileImage ) => {
    const pwValidation = new RegExp(
        `^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,20})`
    );
    if(!pwValidation.test(password)) {
        const err = new Error(`PASSWORD_IS_NOT_VALID`);
        err.statusCode = 400;
        throw err;
    }
    const createUser = await userDao.createUser(
        name,
        email,
        password,
        profileImage
    );
    return createUser;
};
module.exports = {
    signUp
}

위 코드는 HTTP 요청으로 받은 password 데이터를 특정 조합이 만족하기 위한 정규표현식을 구현한 모습이다.
Services 공간은 위와 같이 Controllers 에서 받은 검증된 데이터들을 DB에 넣기전 최종적으로 로직을 구현하는 공간이다.

models

DB와 직접적으로 소통할 수 있는 유일한 공간이다. DB는 models를 통해서만 접근이 가능하다.
models에서는 데이터의 입, 출력 만 담당한다.

const { DataSource } = require('typeorm');

const appDataSource = new DataSource({
  type: process.env.TYPEORM_CONNECTION,
  host: process.env.TYPEORM_HOST,
  port: process.env.TYPEORM_PORT,
  username: process.env.TYPEORM_USERNAME,
  password: process.env.TYPEORM_PASSWORD,
  database: process.env.TYPEORM_DATABASE
})

appDataSource.initialize()
    .then(() => {
      console.log("Data Source has been initialized!")
    })
    .catch((err) => {
      console.error("Error during Data Source initialization", err)
      appDataSource.destroy()
    })
const createUser = async ( name, email, password, profileImage ) => {
    try {
        return await appDataSource.query(
            `INSERT INTO users(
                name,
                email,
                password,
                profile_image
            ) VALUES (?, ?, ?, ?)
            `,
            [ name, email, password, profileImage ]
        );
    } catch (err) {
        const error = new Error(`INVALID_DATA_INPUT`);
        error.statusCode = 500;
        throw error;
    }
};

module.exports = {
    createUser
}

DB와 통신하기 위해 app.js 에서 제거한 TypeORM 관련 코드를 넣어 주었다.
해당 공간에서 최종적으로 검증과 각종 로직을 거친 데이터를 DB에 입력, 수정, 삭제 등의 작업을 거치는 공간이다.
DB에 데이터와 포멧이 다르거나 기타 오류가 발생할 경우 코드 ERROR 500을 반환한다.

마치며

기존에 app.js에 모든 코드를 집어 넣은 방식은 내가 구현한 CRUD의 갯수도 적고 에러 검출과 같은 작업을 하지 않아 코드의 가독성이 그렇게 나쁘지 않았다.
하지만, 앞으로 계속 기능을 추가하거나 에러 검출 로직을 추가하면 가독성과 유지 보수성이 나빠 질 것이다.
이번에, Layered Pattern을 공부해 기존 코드를 리팩토링을 하며, 에러 검출 로직과 비밀번호 유효성 검출 로직등 몇가지 추가구현을 진행 했다.
로직을 추가 했음에도 가독성이 나빠지지 않고, 오히려 모듈화가 되어있어 코드를 보는데 있어 불편함이 없었다.
앞으로도 Layered Pattern 을 이용해 API 아키텍쳐를 구축해 나갈 예정이다.

0개의 댓글