Introduction
매주 금요일 09시에 회사 팀 내에서 기술 세미나라는 것을 진행한다. 사용 해보지 못한 기술을 공유하거나,
컴퓨터 CS 등을 공유하는 시간을 갖는다. 이번에 내 차례이기도 했고, 대부분을 Spring + RDBMS 로만 개발 해왔는데 좋은 기회다 싶어서 Express(Node.js) + MongoDB(NoSQL)로 간단한 RestAPI를 만들어보고자 한다.
ㅁ Node.Js 란 ?
구글 V8 엔진을 활용한 단일 스레드 + 이벤트루프 기반 고성능 네트워크 서버다. 언어로는 자바스크립트를 사용한다.
ㅁ NoSQL 란 ?
Not Only SQL 의 약자로 SQL만을 사용하지 않는 다른 DBMS를 가르키는 용어이다. 관계형 데이터베이스를 사용하지 않는 것이
아닌 여러 유형의 데이터베이스를 사용한다는 것이다.
Install (MAC OS / apple silicon)
1.1 home brew 설치
본문에서는 SQL Server를 클라우드가 아닌 로컬서버로 실행 하는걸 기본으로 하기 때문에 로컬에 MongoDB를 설치하기 위해 MAC OS 패키지 관리 프로그램인 home brew 를 설치한다.
터미널을 열고, home brew를 설치 한다.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
설치가 완료되면 Next Step 으로 터미널에 추가적인 명령어 입력을 친절하게 알려준다. 그대로 실행한다.
( 다음 명령어는 brew 명령어 사용을 위해 brew 컨텐츠를 리다이렉트 해준다. )
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/사용자/.zprofile
( zsh 셸이 실행될 때 자동으로 brew 같이 실행되도록 해준다. )
eval "$(/opt/homebrew/bin/brew shellenv)"
1.2 MongoDB 설치 (Local Server)
MongDB 설치는 기본적으로 터미널로 설치 및 실행하며, 기본 포트는 27017이다.
ㅁ MongoDB 란 ?
NoSQL database 이며, 스키마가 존재하지 않기 때문에 다양한 데이터를 저장할 수 있기 때문에 높은 확장성을 제공한다.
Read와 write 성능이 뛰어나고, JSON 구조를 다루기 때문에 직관적으로 데이터를 이해할 수 있다.
터미널을 열고, 다음 명령어를 순차적으로 입력한다.
brew tap mongodb/brew
brew install mongodb-community@6.0
설치가 완료 되었다면, 실행 및 중지를 할 수 있다.
brew services start mongodb-community@6.0
brew services stop mongodb-community@6.0
MongoDB가 실행중인지 확인하려면 다음 명령어를 입력한다.
brew services list
윈도우 설치방법(https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-windows/)
1.3 MongoDB Compass 설치
MongoDB Compass는 Mysql의 workbench, Oracle의 SQL developer 와 같이 MongoDB 를 GUI로 조작할 수 있게 해주는 프로그램이다.
MongoDB Compass 설치 URL : https://www.mongodb.com/try/download/compass
Try MongoDB Tools - Download Free Here
Free download for MongoDB tools to do more with your database. MongoDB Shell, Compass, CLI for Cloud, BI Connector and other database tools available.
www.mongodb.com
설치를 완료한 후 실행하면 아래와 같은 창이 뜬다. URL은 클라우드로 Server를 실행하고 있다면 Server URL를 넣어주고,
로컬에서 실행할 경우 기본 설정 되어 있는 URL로 Connect 버튼을 눌러준다.
정상적으로 연결에 성공하면 왼쪽 네비게이션 바에 기본적으로 설치 되어 있는 DB 목록들이 표시되고,
+ 버튼을 눌러서 temp 라는 임시 데이터베이스와 todoList 라는 컬렉션을 생성하면 사용할 준비는 끝났다.
1.3 Postman 및 Node.js (외부라이브러리) 설치
Postman 다운로드 주소(https://www.postman.com/downloads/)
Node.js 다운로드 주소 (https://nodejs.org/ko/download)
해당 주소로 들어간 후, 본인 운영체제 환경에 맞춰 설치를 진행한다.
그리고 원하는 이름의 빈폴더를 만든 후 아래 명령어를 입력해, 외부 라이브러리를 설치한다.
npm install express
npm install http-status
npm install mongodb
npm install nodemon
ㅁ nodemon 란 ?
코드수정시 서버의 자동재시작을 통해 생산성을 높여준다.
ㅁ http-status 란 ?
HTTP 프로토콜의 응답코드에 관련한 정보들과 사용의 편의를 제공해준다.
ㅁ mongodb 란 ?
MongoDB를 연결해 사용하기 위한 편의 기능들을 제공한다.
ㅁ Postman 란 ?
포스트맨은 개발자들이 API를 디자인하고 빌드하고 테스트하고를 반복하기 위한 API 플랫폼이다.
ㅁ Express 란 ?
Express 는 대표적인 Node.js의 표준 프레임워크다. 라우팅 기능, 미들웨어 등등 편의기능들을 제공한다.
RestAPI (공통코드)
RestAPI의 도메인 주제는 todoList이고, 간단하게 프로젝트 구조 및 MongoDB의 CRUD를 설명한다.
connection.js
MongoDB와 연결을 담당하는 함수를 만들고, Service 에서 함수를 활용해 MongoDB에게 CRUD 진행한다.
const MongoClient = require("mongodb").MongoClient;
const dbConfig = require("../config/db-config");
exports.getSession = () => {
return new Promise((resolve, reject) => {
MongoClient.connect(`mongodb://127.0.0.1:27017/temp`).then((db) => {
const database = db.db("temp");
resolve(database);
});
});
};
app.js
최상위 루트에 app.js 파일을 생성. http 서버 실행 및 라우팅을 담당한다.
const express = require("express");
const app = express();
app.use(express.json());
const todoRouter = require("./src/routes/todoRouter");
app.use("/todolist", todoRouter);
app.listen(3000, () => {
console.log("server run");
});
todoRouter.js
들어오는 요청에 따라 실행할 컨트롤러를 지정해준다.
const express = require("express");
const router = express.Router();
const todoController = require("../controller/todoController");
router.post("/", todoController.insertTodo);
router.delete("/:todoId", todoController.deleteTodo);
router.get("/", todoController.selectTodo);
router.put("/", todoController.putTodo);
module.exports = router;
todoController.js
라우트를 통해 들어오는 요청에 따라 프로세스를 진행하고, 진행 결과를 응답한다.
exports.insertTodo = async (req, res, next) => {
const todo = req.body;
todo.createDate = new Date(todo.createDate);
const result = await todoService.insertTodo(todo);
let status = httpStatus.OK;
let message = "success";
if (result == 0) {
status = httpStatus.BAD_REQUEST;
message = "fail";
}
res.status(status).json({
status: status,
message: "success",
result: result,
});
};
전체코드
const httpStatus = require("http-status");
const todoService = require("../service/todoService");
exports.insertTodo = async (req, res, next) => {
const todo = req.body;
todo.createDate = new Date(todo.createDate);
const result = await todoService.insertTodo(todo);
let status = httpStatus.OK;
let message = "success";
if (result == 0) {
status = httpStatus.BAD_REQUEST;
message = "fail";
}
res.status(status).json({
status: status,
message: "success",
result: result,
});
};
exports.deleteTodo = async (req, res, next) => {
const todoId = req.params.todoId;
const result = await todoService.deleteTodo(todoId);
let status = httpStatus.OK;
let message = "success";
if (result == 0) {
status = httpStatus.BAD_REQUEST;
message = "fail";
}
res.status(status).json({
status: status,
message: message,
deleteCount: result,
});
};
exports.selectTodo = async (req, res, next) => {
req.query.startDate = new Date(req.query.startDate);
req.query.endDate = new Date(req.query.endDate);
const result = await todoService.selectTodo(req.query);
res.status(httpStatus.OK).json({
status: httpStatus.OK,
message: "success",
result: result,
});
};
exports.putTodo = async (req, res, next) => {
const todo = req.body;
const result = await todoService.putTodo(todo);
let status = httpStatus.OK;
let message = "success";
if (result == 0) {
status = httpStatus.BAD_REQUEST;
message = "fail";
}
res.status(status).json({
status: status,
message: message,
modifiedCount: result,
});
};
todoService.js
컨트롤러에게 받은 요청을 처리한 후 결과를 돌려준다.
exports.insertTodo = async (todo) => {
const database = await getSession();
const result = await database
.collection("todoList")
.insertOne(todo)
.then((result, error) => {
if (error) {
console.log(error);
return 0;
}
return result.insertedId.toString();
});
return result;
};
전체코드
const { getSession } = require("../database/connection");
const { ObjectId } = require("mongodb");
exports.insertTodo = async (todo) => {
const database = await getSession();
const result = await database
.collection("todoList")
.insertOne(todo)
.then((result, error) => {
if (error) {
console.log(error);
return 0;
}
return result.insertedId.toString();
});
return result;
};
exports.deleteTodo = async (todoId) => {
const database = await getSession();
const result = await database
.collection("todoList")
.deleteOne({ _id: new ObjectId(todoId) })
.then((result, error) => {
if (error) {
return 0;
}
return result.deletedCount;
});
return result;
};
exports.selectTodo = async (query) => {
const database = await getSession();
const findtodoList = await database
.collection("todoList")
.find({})
.toArray((todoList, error) => {
if (error) {
return 0;
}
return todoList;
});
return findtodoList;
};
exports.putTodo = async (todo) => {
const database = await getSession();
const id = todo._id;
const result = await database
.collection("todoList")
.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
createUserId: todo.createUserId,
title: todo.title,
content: todo.content,
},
$currentDate: { lastModified: true },
}
)
.then((result, error) => {
if (error) {
return 0;
}
return result.modifiedCount;
});
return result;
};
MongoDB CRUD
1. Create
MongoDB는 정해진 스키마대로 저장하는 것이 아니라, 스키마가 없는 Document를 저장한다. 그렇기 때문에 컬럼의 갯수나 값의 타입이 일치하지 않아도 저장되며, Id를 지정해주지 않는다면 _id란 값으로 ObjectId 형식의 12바이트 크기를 가진 hexadecimal 값을 지정해준다. 이 형식의 키 값은 앞에서부터 4byte는 timeStamp, 5byte는 랜덤 값, 3byte는 증가하는 Count 값이다.
(connection.js 에서 정의한 DB에 지정한 Collection에 데이터 하나를 추가 하고, 결과 값으로 추가된 데이터의 Id 값을 리턴한다.)
db.collection.insertOne(doc, options).then((result,error)=>result.insertedId);
결과
또 인덱스를 직접 생성해 사용할 수 있다.
공식 홈페이지(https://www.mongodb.com/docs/manual/core/index-single/)
2. Read
데이터를 조회하기 위한 find 명령어는 다양한 방식의 조회를 가능하게 하며, 다양한 체이닝을 제공한다.
db.collection.find({})
= SELECT * FROM collection
db.collection.find({createUserId : userId })
= SELECT * FROM collection where createUserId = userId
db.collection.find({createDate : { $gt: query.startDate, $lte: query.endDate }})
= SELECT * FROM collection where createDate > startDate and createDate < endDate
db.collection.find({content : /삐용/})
= SELECT * FROM collection where content LIKE "%삐용%"
db.collection.find({}, { projection: { _id: 0, title: 1, content: 1 } })
= SELECT title, content FROM collection
-- 체이닝
db.collection.find().count()
= SELECT COUNT(*) FROM collection
db.collection.find({}).limit(5)
= SELECT * FROM collection limit 5
db.collection.find({}).limit(5).skip(10)
= SELECT * FROM collection limit 5 skip 10
추가적인 많은 사용방법들이 존재한다.
공식 홈페이지(https://www.mongodb.com/docs/v6.0/reference/sql-comparison/)
3. Update
업데이트를 할 데이터의 위치를 지정해 준 후, 값을 변경할 수 있다.
본문에서의 프로젝트에서는 id의 생성을 MongoDB에게 위임 했고, id는 12바이트 크기의 obejctId 타입이기 때문에 MongoDB가 만들어준 id 값으로 위치를 검색한 후 Data를 변경하기 위해선 id타입도 objectId로 변경해주어야 한다.
const { ObjectId } = require("mongodb");
db.collection.updateOne({id : new ObjectId(id)}, {$set : {key : value}});
4. Delete
삭제할 데이터의 위치를 지정해 준 후, 값을 삭제할 수 있다.
deleteOne의 경우, 한 개의 데이터만 삭제한다. 하지만 데이터를 검색 했을 시 결과가 복수라면 최상단(입력이 가장 빠른)의 값부터 삭제한다.
db.collection.deleteOne({_id : new ObjectId(id)});