오늘도 개발

Layered Pattern 본문

웹 프로그래밍/Javascript

Layered Pattern

Sueeeeeee 2022. 10. 1. 13:04

1. Layered Pattern이란?

레이어드 패턴이란?

코드를 목적에 따라 분리한 후 각각 다른 레이어에 작성하는 방식을 레이어드 패턴이라고 한다.

코드를 목적에 따라 분리하는 것을 관심사의 분리라고 한다.

 

모든 직원이 모든 것을 다 해야하는 식당이 있다고 생각해보자.

모든 직원은 고객도 맞고 주문도 받고 재료도 찾아 요리도 해야한다.

이 경우 혼동이 발생할 수도 있고 효율이 떨어질 수도 있다.

실수가 생겼을 때 주문을 잘못 받았는지 요리를 잘못했는지, 어디서 오류가 났는지 알기 어렵다.

 

업무를 나눠 각 직원이 각자 잘하는 일을 맡아서 하면 이런 문제를 해결할 수 있다.

그럼 웨이터는 서빙만, 요리사는 요리만 하면 된다.

 

프로그래밍도 마찬가지다. 

한 파일이 모든 것을 다 처리한다면 디버깅이 어렵고 비효율적이다.

출처 - [JUSTCODE] Layered pattern

이 경우 역시 프로그램이 할 업무를 나누고,

업무별로 파일(=레이어)을 만들어 각 업무를 맡도록 하면 효율적일 것이다.

출처 - [JUSTCODE] Layered pattern

 

레이어드 패턴의 장점

1) 각 구성 요소의 역할이 명확해진다.

2) 각 레이어는 독립적이므로 서로에게 미치는 영향이 최소화된다.

3) 가독성이 높아진다.

4) 재사용 가능성이 높아진다.

 

특징 1 - 단방향성

레이어드 패턴의 레이어는 한 쪽의 레이어에게만 의존함.

예를 들어 presentation layer는 business layer의 모듈만 import함. 

business layer는 persistence layer의 모듈만 import함.

 

특징 2 - SoC(Seperation of Concern)

코드를 목적에 따라 분리시킴.

 

2. 레이어

백엔드의 레이어는 보통 다음과 같이 나눌 수 있다.

MVC 패턴의 View가 프론트엔드로 독립하면서 M, C를 좀 더 세분화해서 세 레이어로 나눈다.

출처 - [JUSTCODE] Layered pattern

1) Presentation Layer(=Controller)

클라이언트와 직접적으로 연결되는 레이어.

API 엔드포인트 정의.

HTTP 요청을 읽는 로직 구현

 

2) Business Layer(=Service)

비즈니스 로직을 구현하는 레이어.

실제로 시스템이 해야 할 일을 구현함(ex. 비밀번호에 특수문자가 없으면 회원가입 거부)

 

3) Persistence Layer(=Model)

데이터베이스와 관련된 로직을 구현하는 레이어.

데이터를 생성, 수정, 읽어서 데이터베이스에서 CRUD 진행

 

3. 구현 예시

레이어드 패턴 적용 전

구조

.
|_ server.js

server.js

app.js의 signUp()이라는 함수는 서버 켜기, 요청/응답 처리, 비즈니스 로직 처리, 데이터베이스 로직 처리를 모두 하고 있다.

const dotenv = require('dotenv');
dotenv.config(); // 전 프로젝트 어느 곳에서나 이제 .env에 접근 가능
const http = require('http');
const express = require('express');

const app = express()
app.use(express.json())
app.post('/signup', async(req, res) => {
    // Controller 역할(요청 컨트롤)
    const { username, password } = req.body;
    const hasKey = { username: false, password : false };
    const requiredKey = Object.keys(hasKey);
    Object.entries(req.body).forEach((keyValue) => {
        const [ key, value ] = keyValue
        if (requiredKey.includes(key) && value){
            hasKey[key] = true;
        }
    })
    const hasKeyArray = Object.entries(hasKey);
    for (let i = 0; i < hasKeyArray.length; i++){
        const [key, value] = hasKeyArray[i]
        if (!value){
            res.status(400).json({message: `키(${key})가 존재하지 않습니다.`})
        }
    }

    // Service 역할(비즈니스 로직)
    const salt = bcrypt.genSaltSync(12);
    const hashedPw = bcrypt.hashSync(password, salt)

    try {
        // Model 역할(DB 로직)
        await myDataSource.query(
            `INSERT INTO users (username, password) VALUES (?, ?)`,
            [username, hashedPw]
        )
        // Controller 역할(응답 컨트롤)
        res.status(201).json({message: "userCreated"})
    }
    catch(err) {
        console.log(err)
        res.status(500).json({message: 'error'})
    }
})

const server = http.createServer(app)
server.listen(8000, () => {
    console.log('server is listening on port 8000');
})

 

레이어드 패턴 적용 후

구조

.
|_server.js
|_app.js
|_.env
|_.env.test
|_controllers
|____userController.js
|_services
|____userService.js
|_models
|____userDao.js (Dao는 DataAccessObject의 줄임말)
|_routers
|____index.js
|____userRouter.js
|_tests
|____user.test.js

1) app.js

앱 관련 함수만 넣음

const morgan = require('morgan');
const cors = require('cors');
const express = require('express');
const router = require("./routers");

const createApp = () => {
    const app = express();
    app.use(morgan('dev'));
    app.use(express.json());
    app.use(router);
    app.use(cors());

    return app;
}

module.exports = { createApp };

2) server.js 

서버 구동 관련 함수만 넣음

const http = require('http')
const dotenv = require('dotenv')
dotenv.config()
const { createApp }= require('./app')

const app = createApp()
const server = http.createServer(app)

server.listen(8000, () => {
  console.log('server is listening on PORT 8000')
})

3) routers

<index.js>

요청이 들어오면 일단 거치는 곳. 여기서 각 라우터로 연결해줌.

const express = require('express');
const router = express.Router();

const userRouter = require('./userRouter');

// uri에 users가 있으면 userRouter 파일로 보내줌
router.use('/users', userRouter);

// module.exports = {router}는 오류남
module.exports = router

<userRouter.js>

const express = require('express');
const userController = require("../controllers/userController")

const router = express.Router() // express 자체 메서드 사용할 수 있음

// localhost:8000/users/signup으로 들어오면 아래로 연결됨
router.post('/signup', userController.createUser)

// module.exports = {router}는 오류남
module.exports = router

4) controllers

<userController.js> 

signUp() 함수에서 유저 요청 처리 완료 후 userService.js의 signUp() 함수 호출

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

const createUser = async (req, res) => {
    const { username, password } = req.body;
    const hasKey = { username: false, password : false };
    const requiredKey = Object.keys(hasKey);
    Object.entries(req.body).forEach((keyValue) => {
        const [ key, value ] = keyValue
        if (requiredKey.includes(key) && value){
            hasKey[key] = true;
        }
    })
    const hasKeyArray = Object.entries(hasKey);
    for (let i = 0; i < hasKeyArray.length; i++){
        const [key, value] = hasKeyArray[i]
        if (!value){
            res.status(400).json({message: `키(${key})가 존재하지 않습니다.`})
        }
    }

    const user = await userService.createUser(username, password)
    res.status(201).json({message: "userCreated"})
}

module.exports = {
    createUser
}

5) services

<userService.js>

signUp() 함수에서 비즈니스 로직 처리 완료 후 userDao.js의 signUp()함수 호출

const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const userDao = require("../models/userDao")

// res, req가 아니라 username, password를 인자로 받음
const createUser = async (username, password) => {
    const salt = bcrypt.genSaltSync(12);
    const hashedPw = bcrypt.hashSync(password, salt)
    const user = await userDao.createUser(username, hashedPw)
    return user
}

module.exports = { createUser }

6) models

<userDao.js 파일>

DB 관련 작업 처리

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

const myDataSource = 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
})

myDataSource.initialize()
.then(() => {
    console.log("Data Source has been initialized")
})
.catch(() => {
    console.log("Database initiate fail")
})

const createUser = async (username, hashedPw) => {
    const user = await myDataSource.query(
        `INSERT INTO users (username, password) VALUES (?, ?)`,
        [username, hashedPw]
    )
    console.log(`user : ${user}, username : ${user.username}, password : ${hashedPw}`)
    // user : [object Object], username : garfield, password : $2a$12$G7f2kN2mWZmP.ycR0PTsEuVOFhTE/eL9NlFoTFQd1GzXS2Ex9fG7y

    return user 
}

module.exports = { createUser }

 

출처

[JUSTCODE] Layered pattern

'웹 프로그래밍 > Javascript' 카테고리의 다른 글

자바스크립트의 this  (0) 2022.10.17
jest로 unit test하기  (0) 2022.10.02
Node.js에서 인증, 인가 진행하기(bcrypt, jwt)  (0) 2022.09.29
Middleware  (0) 2022.09.28
에러/오류 핸들링  (0) 2022.09.28