github-blog.png


✍️ Today I Learned


1. MVC


  • 모델-뷰-컨트롤러(model–view–controller, MVC)는 소프트웨어 공학에서 사용되는 소프트웨어 디자인 패턴이다.

    하나의 애플리케이션, 프로젝트를 구성할 때 그 구성요소를 세가지의 역할로 구분한 디자인 패턴을 일컫는다.



1-1. 웹 애플리케이션 MVC design pattern


위의 모식도는 개념적인 MVC 를 나타낸 그림이며, 대부분의 웹 애플리케이션에서의 MVC는 다음 아래와 같은 구조로 이루어진다.

Router-MVC-DB svg

모델-뷰-컨트롤러 각각의 구성요소들 사이에는 다음과 같은 관계가 있다.

  • 모델(Model) : 애플리케이션의 정보, 데이터를 나타낸다. 데이타베이스 혹은 또한 이러한 여러 정보들의 가공을 책임지는 컴포넌트를 말한다.

  • 뷰(View) : input 텍스트, 체크박스 항목 등과 같은 사용자 인터페이스 요소를 나타낸다. 다시 말해 데이터 및 객체의 입력, 그리고 보여주는 출력을 담당한다.

  • 컨트롤러(Controller) : 데이터와 사용자인터페이스 요소들을 잇는 다리역할을 한다. 즉, 사용자가 데이터를 클릭하고, 수정하는 것에 대한 “이벤트”들을 처리하는 부분이다.



2. Cmarket Database


  • MVC 모델 디자인 관념으로 본다면, V(client)는 완성되어 있다.

    3 tier architecture 완성시키는게 목적인 스프린트이다.



2-1. Database(스키마, 시드파일)


  • DB 작성은 커맨드 창에 명령어를 하나하나 입력하는 방법과 다르게, 미리 구성되어 있는 Cmarket 스키마를 기반으로 MySQL에 배치모드를 활용하여 cmarket 데이터베이스의 테이블을 생성한다.

  • 우선 스키마im-sprint-cmarket-database/server/schema.sql 파일에 명시되어 있다. 해당 파일을 들여다 보면 다음과 같다.

    CREATE TABLE users ( /* 테이블 생성 : CREATE TABLE 테이블이름 */
      id INT AUTO_INCREMENT,
      username varchar(255),
      PRIMARY KEY (id)
    );
    
    /* ...생략 */
    
    ALTER TABLE orders ADD FOREIGN KEY (user_id) REFERENCES users (id); /* 테이블 수정(컬럼 추가[외래키]) : ALTER TABLE 테이블이름 ADD ~ */
  • 시드파일im-sprint-cmarket-database/server/seed.sql 파일에 명시되어 있다. 해당 파일을 들여다 보면 다음과 같다.

    /* ...생략  */
    
    INSERT INTO users (username) VALUES ("김코딩"); /* INSERT INTO 테이블이름(필드이름1 ...) VALUES (데이터값...) */
  • 스프린트에서는 위 schema.sql파일과 seed.sql파일을 토대로 node.j mysql 모듈을 통해서 DB를 다룬다. im-sprint-cmarket-database/server/db/index.js 파일을 살펴보면 다음과 같다.

    const mysql = require('mysql'); // mysql 모듈 사용 선언, npm install mysql 후 사용가능하다.
    const dotenv = require('dotenv'); // DB 비밀번호등 환경변수는 .env 모듈을 통해 사용한다.
    const config = require('../config/config'); // config 또한 환경변수를 다루는 모듈이다. .env와 함께 사용한다.
    dotenv.config(); // 환경변수 불러오기
    
    const con = mysql.createConnection(
      // mysql 연결
      config[process.env.NODE_ENV || 'development'], // development 환경, test 환경 이지선다로 실행한다. (서버를 킨 채로 test를 실행하면 오류 발생의 원인)
    );
    
    // ...생략

    im-sprint-cmarket-database/server/.env.sampleim-sprint-cmarket-database/server/config/config.js 파일에 환경변수들이 담겨져있다. 개발환경에 맞게 설정하자.

  • 위와 같은 설정이 모두 끝났다면, config.js 파일에 DB 이름은 cmarket 으로 설정되어 있으므로 mysql을 통해서 빈 cmarket DB를 만들어 준다

  • schema.sql 파일을 활용하여 내부 테이블을 배치모드로 한번에 만들 수 있게 된다. mysql -u (유저이름) -p < server/schema.sql -Dcmarket

  • seed.sql 파일을 활용하여 테이블 내에 준비된 시드파일을 심어준다. mysql -u (유저이름) -p < server/seed.sql -Dcmarket



2-2. 서버


  • 서버를 실행하기 전, 위에서 만든 DB를 사용하게끔 환경 설정이 필요하다.

  • package.json을 확인하면 dependenciesmysql 모듈이 있다.

    이 모듈을 통해 서버와 데이터베이스서버를 연결해줄 수 있다. 해당 모듈을 통해 mysql에 접속하기 위한 username, password를 코드에 작성할 수도 있겠지만, 보안상/편의상 이유로 비밀번호는 .env 파일에 환경 변수로 분리해놓고, .gitignore.env파일을 올려두어 외부에 노출되지 않게끔 관리되고 있음을 볼 수 있다.

  • 해당 환경을 서버에서 사용하기 위해 config/config.js파일을 보면 .env 와 연결되어 있는 걸 볼 수 있다.

    즉, .env 파일을 통해 환경변수 등 민감한 개인정보가 외부로 노출되지 않게끔 설정하고, .env파일을 config.js 에 연결하여 환경변수를 서버에서 활용하는 모습을 볼 수 있다.

    const dotenv = require('dotenv');  /* .env 사용 */
    dotenv.config();
    
    const config = {
    	development: {
    		host: 'localhost',
    		user: process.env.DATABASE_SPRINT_USER,  /* .env 환경변수 사용 */
    		password: process.env.DATABASE_SPRINT_PASSWORD,
    		database: 'cmarket'
    	}
    
    /* 생략 */
  • 이제 mysql DB가 node.js 서버환경에서 어떻게 사용되는지는 파악하였다. 이제 스프린트 통과를 위해 서버를 보자면, app.js 파일에는 express로 서버를 만드는 코드가 이미 작성되어있다.

    const express = require('express');
    const indexRouter = require('./routes');
    const cors = require('cors');
    const morgan = require('morgan');
    const app = express();
    const port = 4000;
    
    app.use(morgan('      :method :url :status :res[content-length] - :response-time ms'));
    app.use(cors());
    app.use(express.urlencoded({ extended: true }));
    app.use(express.json());
    app.use('/', indexRouter); /* router 진입점 */
    
    module.exports = app.listen(port, () => {
      console.log(`      🚀 Server is starting on ${port}`);
    });

    해당 코드를 통해 router 진입점을 알 수 있다.
    라우팅 연결 시작점을 알았으니 해당 진입점부터 작성해나가자.



2-3. Router


  • 라우터는 컨트롤러로 진입할 수 있게 도와주는 endpoint이다.

    스프린트에서 요구하는 endpoint는 세가지이다.

    아래의 요구사항에 맞춰서 users router 파일을 추가로 작성해주고, routes/index.js 파일에서도 routes/users.js 로 연결되게끔 설정해 준다.

    • [GET] /items

    • [GET] /users/:userId/orders

    • [POST] /users/:userId/orders

  • 따라서 routes/index.js에서는,

    const itemsRouter = require('./items'); /* GET/items */
    const usersRouter = require('./users'); /* GET /users/:userId/orders 와 POST /users/:userId/orders */
    
    // TODO: Endpoint에 따라 적절한 Router로 연결해야 합니다.
    router.use('/items', itemsRouter);
    router.use('/users', usersRouter);

    위와 같이 routes/users.js로 연결을 해줘야 한다.

  • 그리고 routes/users.js에서는 각 endpoint에 알맞게 작성해준다.

    const controller = require('./../controllers');
    
    router.get('/:userId/orders', controller.orders.get);
    router.post('/:userId/orders', controller.orders.post);


2-4. Controller


  • 세 endpoint에 이르는 경로는 router에서 모두 구현을 하였다.

    이제 각 endpoint에 대한 각기 다른 구현이 필요하다. controller를 작성해보자.



아이템 가져오기 : [GET] /items

  • 스프린트에서 이미 작성되어 있다.

    items: {
    		get: (req, res) => {
    			models.items.get((error, result) => {
    				if (error) {
    					res.status(500).send('Internal Server Error');
    				} else {
    					res.status(200).json(result);
    				}
    			});
    		},
    	},

    요구사항도 Response로 json 형식으로 데이터값을 받으며 상태코드 200번을 출력하라는 요구사항이니 문제없이 통과가 된다.



주문하기 : [POST] /users/:userId/orders

  • 우선 주문은 해당 json 형식으로 주문을 해야 한다.

    {
      "orders": [
        {
          "quantity": 1,
          "itemId": 2
        }
        // ...여러 개의 주문 아이템
      ],
      "totalPrice": 16900
    }

    [POST] /users/:userId/orders 요청에서 클라이언트가 잘못된 요청을 했을 경우 상태코드 400을 보내야합니다.

    [POST] /users/:userId/orders 요청에 성공했을 경우 상태코드 201을 보내야합니다.

    그리고 위 두가지의 테스트케이스를 통과해야한다.

  • 우선 테스트 케이스의 잘못된 요청이 들어온 경우는 req.body로 받아온 orders, totalPrice의 값이 올바르지 못한 경우를 말한다.

    if (!orders || !totalPrice) {
      /* 잘못된 요청이 들어 올경우 400번 */
      return res.status(400).send('잘못된 요청');
    }
  • 그리고 요청에 성공하는 경우 201번과 위에 있는 json 형식에 알맞는 주문을 받아야 한다.

    주문에 필요한 인자는 userId, orders, totalPrice 이다. 해당 값들을 적절히 사용하면 [POST] /users/:userId/orders endpoint는 다음과 같이 작성이 가능하다.

    post: (req, res) => {
      const userId = req.params.userId;
      const { orders, totalPrice } = req.body;
      // TODO: 요청에 따른 적절한 응답을 돌려주는 컨트롤러를 작성하세요.
    
      if (!orders || !totalPrice) {
        /* 잘못된 요청이 들어 올경우 400번 */
        return res.status(400).send('Bad request');
      } else {
        models.orders.post(userId, orders, totalPrice, (error, result) => {
          if (error) {
            res.status(500).send('Internal Server Error');
          } else {
            res.status(201).json('success POST');
          }
        });
      }
    };


주문 내역 조회 : [GET] /users/:userId/orders

  • 주문 내역 조회는 userId를 params로 받아와서 json형태로 상태코드 200번과 함께 Response 해주면 되는 간단한 로직이다. DB의 구조가 복잡하고 양이 많지만…해당 부분은 Models에서 제어하자.

    다음과 같은 json 형식으로 반환해주어야 한다.

    [
    	{
    		"id": 1, // orders 테이블의 id
    		"created_at": "2021-02-19T04:34:11.000Z",
    		"total_price": 7800,
    		"name": "칼라 립스틱",
    		"price": 2900,
    		"image": "../images/lip.jpg",
    		"order_quantity": 1,
    	},
    	{
    		"id": 1,
    		"created_at": "2021-02-19T04:34:11.000Z",
    		"total_price": 7800,
    		"name": "뜯어온 보도블럭",
    		"price": 4900,
    		"image": "../images/block.jpg",
    		"order_quantity": 1,
    	},
    	// ...여러 개의 주문내역
    ];
  • 아이템 가져오기 로직과 비슷하다. userId params만 추가해서 해당 내역만 뿌려주면 된다.

    get: (req, res) => {
      const userId = req.params.userId;
      // TODO: 요청에 따른 적절한 응답을 돌려주는 컨트롤러를 작성하세요.
      models.orders.get(userId, (error, result) => {
        res.status(200).json(result);
      });
    };


2-5. Model


  • server/models/index.js 파일에서는 controller 에서 사용할 orders, items 모델을 정의해야 한다.

  • server/db/index.js 의 함수를 불러온 뒤, SQL 쿼리문으로 DB의 정보를 처리해 주어야 하는 구조이다.

    데이터베이스 쿼리는 비동기 요청으로 진행 되야 하는 점을 고려하여 작성하자.



아이템 가져오기 SQL 쿼리문

  • 간단하다. items 테이블의 내용을 모두 보여주는 쿼리문을 작성하면 된다.

    get: (callback) => {
      // TODO: Cmarket의 모든 상품을 가져오는 함수를 작성하세요
      const queryString = `SELECT * FROM items`;
    
      db.query(queryString, (error, result) => {
        callback(error, result);
      });
    };


주문하기 SQL 쿼리문

  • 주문하기 가 구현이 다소 복잡하다.

    우선 다시 주문 json 형식을 보자면,

    {
      "orders": [
        {
          "quantity": 1,
          "itemId": 2
        }
        // ...여러 개의 주문 아이템
      ],
      "totalPrice": 16900
    }

    orders라는 1개의 주문안에 배열 형식으로 여러개의 주문 아이템과 totalPrice 값이 들어가 있는 걸 확인 할 수 있다.

    즉, orders 쿼리문과 내부에 들어가는 items 쿼리문 두개의 쿼리가 이중으로 필요함을 알 수 있다.

    ordersuserIdtotalPrice가 들어가야 하며, 내부에서는 orders_items의 목록(order_id, item_id, order_quantity)을 쏴주는 쿼리가 필요함을 알 수 있다.

    post: (userId, orders, totalPrice, callback) => {
      // TODO: 해당 유저의 주문 요청을 데이터베이스에 생성하는 함수를 작성하세요
      const orderPostSQL = `INSERT INTO orders(user_id, total_price) VALUES (${userId}, ${totalPrice})`;
      const orderItemPostSQL = `INSERT INTO order_items(order_id, item_id, order_quantity) VALUES ?`;
    
      db.query(orderPostSQL, (error, result) => {
        let params = orders.map((item) => [
          result.insertId, //result 객체 내부의 insertId 활용
          item.itemId,
          item.quantity,
        ]);
    
        db.query(orderItemPostSQL, [params], () => callback(error, result));
      });
    };
  • orderItemPostSQLVALUES값은 prepared statement 방식으로 처리하였다.



주문 내역 조회 SQL 쿼리문

  • 간단하면서…복잡하다. 여러개의 join문을 통해서 원하는 데이터 값을 모두 출력하는 쿼리문을 완성하자.

    	get: (userId, callback) => {
    		// TODO: 해당 유저가 작성한 모든 주문을 가져오는 함수를 작성하세요
    		const orderGetSQL = `
    			SELECT orders.id, orders.created_at, orders.total_price, items.name, items.price, items.image, order_items.order_quantity
    			FROM orders INNER JOIN order_items ON orders.id = order_items.order_id
    			INNER JOIN items ON order_items.item_id = items.id
    			INNER JOIN users ON orders.user_id = users.id
    			WHERE users.id = ${userId}
    		`;
    
    		db.query(orderGetSQL, (error, result) => {
    			callback(error, result);
    		});
    	}
    	```
    <br>
    <br>


🤔 Understanding

  • VC 로 나뉘어진 3 tier architecture를 만들어 내는 것이 이번 스프린트의 핵심이다.

  • 순수 mysql문을 이용하여 node.js에서 활용하는 프레임워크를 배웠다.

    mysql 문법자체가 어렵진 않기때문에 갓 구글링, 어렵지 않게 쿼리문들을 작성할 수 있었다.

  • 앞으로 만들어 내야할 웹 애플리케이션의 초석을 오늘 시간에 다진거 같다.

    이러한 나눠진 디자인 패턴으로 인해 각각의 역할이 뚜렷하게 구분이 되며, 코드의 유지보수성이 확실히 올라가는 구조다…라는 개념정도는 학습한 것 같다.