[Database] cmarket 데이터베이스 구축

view를 서버가 맡게되면 server side rendering (SSR) 이고
클라이언트가 맡게되면 client side rendering (CSR) 이다.

비즈니스 로직

데이터베이스와 사용자 인터페이스 사이 정보 교환을 처리하는 알고리즘을 설명하는 비기술적 용어

프로그램을 통해 어떤 문제를 해결할 것인가?
수도코드 -> 기능 -> 비즈니스 로직 -> 컨트롤러
순으로 해결한다.

이번 스프린트에서의 비즈니스 로직은 다음과 같다.

데이터베이스에서 아이템 가져오기

  • GET 요청이 들어온다 ⚙️ controller
  • Router를 통해 items 컨트롤러로 분기 ⚙️ controller
  • 데이터베이스에서 테이블 조회 ⚙️ model
  • 성공 시 데이터를 json으로 전송 ⚙️ controller

데이터베이스에 주문내역 저장하기

  • POST 요청이 들어온다 ⚙️ controller
  • Router를 통해 orders 컨트롤러로 분기 ⚙️ controller
  • 새로운 record를 만든다 (URL 파라미터, POST 요청의 body 로) ⚙️ model
  • 성공 시 상태코드 201 전송 ⚙️ controller

데이터베이스에서 주문내역 가져오기

  • GET 요청이 들어온다 ⚙️ controller
  • Router를 통해 orders 컨트롤러로 분기 ⚙️ controller
  • 데이터베이스에서 테이블 조회 (URL 파라미터 로) ⚙️ model
  • 성공 시 데이터를 json으로 전송 ⚙️ controller

기본 설정

mysql에 접속해 cmarket 데이터베이스를 생성
CREATE DATABASE cmarket

미리 구성된 스키마를 기반으로 테이블을 생성
mysql -u root -p < server/schema.sql -Dcmarket

생성된 테이블에 미리 구성된 데이터를 저장
mysql -u root -p < server/seed.sql -Dcmarket

mysql> show tables;
+-------------------+
| Tables_in_cmarket |
+-------------------+
| items             |
| order_items       |
| orders            |
| users             |
+-------------------+
4 rows in set (0.01 sec)

mysql> desc items;
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int          | NO   | PRI | NULL    | auto_increment |
| name  | varchar(255) | YES  |     | NULL    |                |
| price | int          | YES  |     | NULL    |                |
| image | varchar(255) | YES  |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+
4 rows in set (0.01 sec)

mysql> desc order_items;
+----------------+------+------+-----+---------+----------------+
| Field          | Type | Null | Key | Default | Extra          |
+----------------+------+------+-----+---------+----------------+
| id             | int  | NO   | PRI | NULL    | auto_increment |
| order_id       | int  | YES  | MUL | NULL    |                |
| item_id        | int  | YES  | MUL | NULL    |                |
| order_quantity | int  | YES  |     | NULL    |                |
+----------------+------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

mysql> desc orders;
+-------------+----------+------+-----+-------------------+-------------------+
| Field       | Type     | Null | Key | Default           | Extra             |
+-------------+----------+------+-----+-------------------+-------------------+
| id          | int      | NO   | PRI | NULL              | auto_increment    |
| user_id     | int      | YES  | MUL | NULL              |                   |
| total_price | int      | YES  |     | NULL              |                   |
| created_at  | datetime | YES  |     | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
+-------------+----------+------+-----+-------------------+-------------------+
4 rows in set (0.00 sec)

mysql> desc users;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int          | NO   | PRI | NULL    | auto_increment |
| username | varchar(255) | YES  |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

이번 스프린트는 서버만 다루므로 view 는 생략했다.

/app.js

const express = require('express');
const indexRouter = require('./routes');  // 모든 요청은 indexRouter 를 거친다
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);

module.exports = app.listen(port, () => {
  console.log(`      🚀 Server is starting on ${port}`);
});

Router

/db/routes/index.js

const express = require('express');
const router = express.Router();
const itemsRouter = require('./items');
const usersRouter = require('./users'); 
// users 앤드포인트로 들어온 요청은 usersRouter를 통하게 한다.

router.use('/items', itemsRouter);
router.use('/users', usersRouter);

module.exports = router;

모든 라우터의 진입점으로 여기서 endpoint에 따라 다시 분기된다.


/db/routes/items.js

const router = require('express').Router();
const controller = require('./../controllers');

// GET /items Router와 Controller를 연결한다.
router.get('/', controller.items.get);

module.exports = router;

/db/routes/users.js

const router = require('express').Router();
const controller = require('./../controllers');

router.get('/:userId/orders', controller.orders.get);
router.post('/:userId/orders', controller.orders.post);
// GET /users/1/orders 방식으로 요청이 들어온다.
// userId는 파라미터이므로 콜론을 붙힌다. 

module.exports = router;

Controller

다음 세 endpoint에 대한 각기 다른 구현이 필요하다

  • GET /items
  • GET /users/:userId/orders
  • POST /users/:userId/orders

/controllers/index.js

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

module.exports = {
  items: {
    get: (req, res) => {
      models.items.get((error, result) => {
        if (error) {
          res.status(500).send('Internal Server Error');
        } else {
          res.status(200).json(result);
        }
      });
    },
  },
  orders: {
    get: (req, res) => {
      const userId = req.params.userId;
      models.orders.get(userId, (error, result) => {
        if(error){
          res.sendStatus(500); // 서버 내부 에러 상태코드 500
        } else {
          res.json(result); // 조회를 했으므로 결과를 json형식으로 보내준다
        }
      });
    },
    post: (req, res) => {
      const userId = req.params.userId; 
      //파라미터 userID변수에 접근
      const { orders, totalPrice } = req.body;
      //요청바디 구조분해할당
      if(!orders || !totalPrice){ //orders 나 totalPrice가 없을 경우 상태코드400을 보낸다. 
        res.status(400).send(); //클라이언트 비정상적인 요청 에러 상태코드 400
      } else {
        models.orders.post(userId, orders, totalPrice, (error, result) => {
          //서버가 데이터베이스에서 자료를 찾고 상태를 보내줘야 되므로(비동기) 콜백함수를 사용한다.
          if(error){
            res.sendStatus(500); // 서버 내부 에러 상태코드 500
          } else {
            res.sendStatus(201); // 클라이언트 PUT 요청이 성공한 상태코드 201
          }
        });
      }
    },
  },
};

환경 구분

/config/config.js

//환경변수를 이용해 개발,테스트 환경으로 구분했다.
const dotenv = require('dotenv');
dotenv.config();

const config = {
  development: {
    host: 'localhost',
    user: 'root',
    password: process.env.DATABASE_SPRINT_PASSWORD, // .env에 저장된 password
    database: 'cmarket'
  },
  test: {
    host: 'localhost',
    user: 'root',
    password: process.env.DATABASE_SPRINT_PASSWORD,
    database: 'cmarket_test'
  }
};

module.exports = config;

mysql 연결

/db/index.js

//데이터베이스와 models를 연결하는 역할을 하는 파일
const mysql = require('mysql'); // mysql 라이브러리를 불러옴
const dotenv = require('dotenv');
const config = require('../config/config'); 
dotenv.config();

const con = mysql.createConnection( //mysql객체를 config정보로 환경을 구분하여 con 객체를 만듬
  config[process.env.NODE_ENV || 'development']
);

//mysql을 nodejs에 연결해 js로 mysql을 조회할 수 있게 해줘야 한다.
con.connect((err) => { //서버와 데이터베이스를 연결하는 과정
  if (err) throw err;
});

module.exports = con;

Model

/db/models/index.js

const db = require('../db');
//데이터베이스에 메서드를 보낼 수 있다.
module.exports = {
  items: {
    get: (callback) => {
      // 모든 상품을 가져오는 함수
      const queryString = `SELECT * FROM items`;

      db.query(queryString, (error, result) => {
        callback(error, result);
      });
    },
  },
  orders: {
    get: (userId, callback) => {
      // 해당 유저가 작성한 모든 주문을 가져오는 함수
      const queryString = `
      SELECT orders.id, orders.created_at, orders.total_price, items.name, items.price, items.image, order_items.order_quantity 
      FROM items
      INNER JOIN order_items ON (order_items.item_id = items.id)
      INNER JOIN orders ON (orders.id = order_items.order_id)
      WHERE (orders.user_id = ?)
      `; //?에 파라미터값이 들어간다.
      //id는 다른 테이블에도 존재하므로 id가 덧씌워질수 있으므로 필드를 일일이 지정해 select 했다 
      
      const params = [userId];
      db.query(queryString, params, (error, result) => {
        callback(error, result);
      });
    },
    post: (userId, orders, totalPrice, callback) => {
      // 해당 유저의 주문 요청을 데이터베이스에 생성하는 함수
      // order_items에 order_id를 FK로 가지고 있으므로 테이블을 순차적으로 채워야 한다 
      //(orders 테이블을 채우고 생성된 record의 id를 orders_items의 FK로 넣어 record 생성)
      const queryString = `
      INSERT INTO orders (user_id, total_price) VALUES (?, ?)
      `;
      const params = [userId, totalPrice];

      db.query(queryString, params, (error, result) => {
        if(error){
          callback(error, null); // 첫번째 쿼리가 성공하고 두번째 쿼리가 실패할 경우 첫번째 쿼리를 롤백해줘야 되지만 생략
        } else {
          const queryString = `
            INSERT INTO order_items (order_id, item_id, order_quantity) VALUES ?
          `;
          // 생성해야하는 레코드의 갯수가 매 요청마다 달라짐
          // 묶어서 요청을 보내 여러개의 record를 한번의 쿼리로 생성해야 서버에 부하를 덜 수 있다. (bulk insert)
          // [{}, {}, {}] 를 [[], [], []] 로
          const params = orders.map(order => [
            result.insertId,
            order.itemId,
            order.quantity,
          ]);
          
          db.query(queryString, [params], callback);
        }
      })
    }
  },
};

테스트가 통과 되었다.🫠

🗄  Cmarket Database
    🗺 -------- Cmarket Router
dd
      🚀 Server is starting on 4000
      ✓ users router 파일이 존재해야 합니다
      ✓ orders controller에는 get, post 메소드가 각각 존재해야 합니다
      GET /users/1/orders 200 2 - 2.076 ms
      ✓ GET /users는 orders controller의 get 메소드를 실행합니다
      POST /users/1/orders 201 7 - 6.014 ms
      ✓ POST /users는 orders controller의 post 메소드를 실행합니다
    🕹 -------- Cmarket Controller
      GET /items 200 617 - 0.827 ms
      ✓ GET /items 요청에 성공했을 경우 상태코드 200을 보내야합니다.
      GET /users/1/orders 200 2 - 0.714 ms
      ✓ GET /users/:userId/orders 요청에 성공했을 경우 상태코드 200을 보내야합니다.
      POST /users/1/orders 400 - - 0.230 ms
      ✓ POST /users/:userId/orders 요청에서 클라이언트가 잘못된 요청을 했을 경우 상태코드 400을 보내야합니다.
      POST /users/1/orders 201 7 - 1.304 ms
      ✓ POST /users/:userId/orders 요청에 성공했을 경우 상태코드 201을 보내야합니다.
    ✨-------- Cmarket Model
      GET /items 200 617 - 0.719 ms
      ✓ 데이터베이스에 저장된 상품 목록을 가져와야합니다.
      POST /users/1/orders 201 7 - 1.232 ms
      ✓ 주문내역을 데이터베이스에 저장해야합니다.
      POST /users/1/orders 201 7 - 1.932 ms
      POST /users/1/orders 201 7 - 1.005 ms
      GET /users/1/orders 200 622 - 0.537 ms
      ✓ 데이터베이스에 저장된 주문내역을 가져와야합니다.


11 passing (482ms)

좋은 웹페이지 즐겨찾기