Wecode - Project 2 (부트캠프)/독학

layered pattern : API Architecture

JBS 12 2023. 10. 4. 15:55

1. presentation layer : 사용자, client 와 직접 연결되는 부분 

2. business layer: 회사 비즈니스 로직 구현 

3. persistence layer:  sql 문 쓰는 파일 (데이터베이스 관련)

 

단방향이다. 

 

 

app.js 단일 파일을 파일 분리 

presentation layer: routes, controller(res,req) 

business layer: service 

persistence layer: Dao (databases)

 

app.js :연결 기능

query문 -> dao/  service< - key값 / 필수값->controller 

 

 

 

 

 

project1- 2일차(3): 회원가입 기능 layered pattern

project 1 복사 붙여놓기 복사본은 깃 반영 안 됨 ls- al 해서 .git 떠도 반영 안 된다. ls-al 해서 나오는 건 git 삭제 remote 연결 안 될 것 단방향에 따라서, 각 파일의 앞단에서 export로 내보낸거를 require

pm-developer-justdoit.tistory.com

 

 

Project1- 3일 차: Layered pattern 개념 (API Architecture, 관심사 분리,)

Layered pattern 📍개요 🗣 특별한 구조 없이 한 파일에 모든 코드를 구현: 코드의 양이 많지 않을 때는 간단, but 코드의 양이 조금만 많아져도 오히려 코드 유지 보수가 어려워진다. 실제 기업의 시

pm-developer-justdoit.tistory.com

 

 

[wecode notion 보면서 참고] 

폴더 및 파일 생성

인스타그램의 회원가입 기능에 layered architecture를 접목해봅니다. 현재 app.js 파일에 속해있는 signUp 함수가 하는 기능을 역할에 따라 나누어 서로 다른 모듈에서 진행하도록 분리합니다. 각 레이어에 속하는 모듈의 이름을 아래와 같이 칭하겠습니다.

  • Presentation layer → userController.js
  • Business layer → userService.js
  • Persistence layer → userDao.js (Data Access Objective)

그러므로 app.js 단일 파일로만 존재하던 구조를 아래 형태로 분리해줍니다.

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

역할에 따른 코드 분리

 

먼저 기존 코드를 먼저 볼까요? // 회원가입 로직 

// app.js

const express = require('express');

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

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!")
    })

const PORT = 3000;
const app = express();

app.use(cors());
app.use(morgan('combined'));
app.use(express.json());

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

// user signup logic
app.post("/users/signup", async (req, res) => {
	const { name, email, profile_image, password } = req.body
    
	await myDataSource.query(
		`INSERT INTO users(
			name,
			email,
			profile_image,
			password
		) VALUES (?, ?, ?, ?);
		`,
		[ name, email, profile_image, password ]
	); 
   res.status(201).json({ message : "successfully created" });
});

[layered 시작] 

app.js 

// app.js

const http = require("http");
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');

const dotenv = require("dotenv")
dotenv.config()

const routes = require("./routes");

const app = express();

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

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

const server = http.createServer(app);
const PORT = process.env.PORT;

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

start();

Routes : endpoint 경로 라우팅

이후 외부에서 들어노는 요청을 가장 먼저 맞이하여 하위 폴더로 안내하는 길잡이 역할을 하는 Router 파일입니다. index.js 는 향후 확장성을 고려하여 생성될 수 있는 다양한 Router(예시: userRouter, productRouter 등)들을 한 곳에 모아 관리하는 역할을 합니다.

//routes/index.js

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

const userRouter = require("./userRouter");
router.use("/users", userRouter.router);

module.exports = router;
//routes/userRouter.js

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

const router = express.Router();

router.post('/signup', userController.signUp);

module.exports = {
	router
}

Controller (Presentation Layer)

다음은 API의 엔드포인트들을 정의하고 전송된 HTTP 요청(request)들을 읽어 들이는 로직을 구현하는 Controller 입니다.

res, req 관련해서만

비즈니스 로직으로 흘러들어가야할 데이터들이 올바른 형태를 띄고 있는지 선검증 작업이 이루어 집니다.

 

 ‘Key’ 값이 요청시 전해지지 않았을 때 발생하는 Key Error 를 사전에 에러처리로 검열한 모습입니다.

//controller/userController.js

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 );
    return 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
}

 

Service

다음은 실제 비즈니스 규칙(Rule)과 로직(Logic)들

 

에러 핸들링 등 

비밀번호의 조합이 특정 형태를 띄어야 하는 규칙을 위해 정규표현식을 도입한 모습입니다.

//service/userService.js

const userDao = require('../models/userDao')

const signUp = async (name, email, password, profileImage) => {
    // password validation using REGEX
    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 = 409;
      throw err;
    }
      const createUser = await userDao.createUser(
          name,
          email,
          password,
          profileImage
        );
      
        return createUser;
      };
  
  module.exports = {
      signUp
  }

Models  

다음은 실질적으로 데이터베이스와 소통하는 Model 레이어 입니다. 아래는 typeorm을 이용하여 db에 연결한 후, 앞선 레이어들을 모두 무사히 통과한 데이터들을 db에 직접 Insert 하는 모습을 띄고 있습니다.

 

+ dataSource에 typerom,dataSource만 분리해야 함 

//models/userDao.js

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((err) => {
    console.error("Error occurred during Data Source initialization", err);
	  myDataSource.destroy();
  });

const createUser = async ( name, email, password, profileImage ) => {
	try {
		return await myDataSource.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
}

 

Layer dependency(의존성) 순서

App → Router → Controller → Service → Models 순으로 갈수록 데이터베이스의 접근에 가까워 집니다. 또한, 각각의 파일에서 export 한 module들을 어느 파일들이 require하고 있는지 그 연결흐름을 잘 살펴보면, 상위 레이어에서 오직 하위 레이어로만 의존하는 방향성의 특성이 드러납니다.

다음은 실제 Express를 이용한 서버 웹 개발시 Layered Pattern을 이루는 기본 구조입니다.

  • app.js | server.js: Express App 으로 서버를 여는 로직입니다. 그리고 Express App 인스턴스를 만들고, 필요한 미들웨어를 붙이는 로직입니다. 경우에 따라서 app.js 와 server.js 에 각기 다른 기능을 유도하여 두가지 파일 모두를 유지할 수도 있고, 둘의 기능을 한데 모아서 한 파일만을 유지할 수도 있습니다. 개발자의 의도에 맞게 파생되어질 수 있는 다양한 경우의 수에 유의하며 코드를 작성해주시기 바랍니다.
  • routes: 라우팅(엔드 포인트 나누기) 로직을 담당합니다.
  • controllers: 엔드포인트에 해당하는 함수 로직 - http 요청에 따른 에러 핸들링, service 로직에서 데이터를 받아와서 응답으로 내보내는 로직입니다.
  • services: controller 에서 넘겨받은 인자로 다양한 알고리즘(필터, 정렬 등..)을 처리해서 데이터에 접근하는 로직입니다.
  • models: 데이터베이스에 접근하기 위한 모델(DAO)이 정의되어 있는 폴더입니다.

아래 모듈은 의존성 없이 다양한 레이어에서 사용될 수 있지만 반복되는 로직이기에 분리해 놓은 폴더 입니다.

  • middlewares: 컨트롤러에 닿기 전에 반복되는 로직을 모듈화 해 놓는 폴더입니다. (ex. validateToken - 인증 / 인가)
  • utils: 의존성 없이 모든 레이어에서 공통적으로 자주 사용되는 로직을 모듈화 해 놓는 폴더입니다. (ex. errorGenerator)
  • .env: 프로젝트 내에서 사용할 환경 변수를 선언해 놓는 곳이며 샘플 양식은 .env.sample에 따로 기입하여 공유합니다.
  • node_modules: 노드 패키지 모듈입니다.
  • .gitignore: 위의 두 모듈을 깃이 관리하지 않도록 합니다.
  • package.json: 노드 모듈을 관리하는 파일입니다.