MVC, Cmarket Database
✍️ Today I Learned
1. MVC
-
모델-뷰-컨트롤러(model–view–controller, MVC)는 소프트웨어 공학에서 사용되는 소프트웨어 디자인 패턴이다.
하나의 애플리케이션, 프로젝트를 구성할 때 그 구성요소를 세가지의 역할로 구분한 디자인 패턴을 일컫는다.
1-1. 웹 애플리케이션 MVC design pattern
위의 모식도는 개념적인 MVC 를 나타낸 그림이며, 대부분의 웹 애플리케이션에서의 MVC는 다음 아래와 같은 구조로 이루어진다.
모델-뷰-컨트롤러 각각의 구성요소들 사이에는 다음과 같은 관계가 있다.
-
모델(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.jmysql
모듈을 통해서 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.sample
과im-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
을 확인하면dependencies
에mysql
모듈이 있다.이 모듈을 통해 서버와 데이터베이스서버를 연결해줄 수 있다. 해당 모듈을 통해 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 쿼리문 두개의 쿼리가 이중으로 필요함을 알 수 있다.
orders는
userId
와totalPrice
가 들어가야 하며, 내부에서는 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)); }); };
-
orderItemPostSQL
의 VALUES값은 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 문법자체가 어렵진 않기때문에
갓 구글링, 어렵지 않게 쿼리문들을 작성할 수 있었다. -
앞으로 만들어 내야할 웹 애플리케이션의 초석을 오늘 시간에 다진거 같다.
이러한 나눠진 디자인 패턴으로 인해 각각의 역할이 뚜렷하게 구분이 되며, 코드의 유지보수성이 확실히 올라가는 구조다…라는 개념정도는 학습한 것 같다.