이번 프로젝트에선 MySQL 대신 MongoDB를 사용하였다.
익숙한 MySQL 대신 NoSQL을 사용해보고 싶기도 했고, 기획을 하다 보니 저장할 데이터가 계속 변경될 것으로 예상됐기 때문에 MongoDB가 적절할 것으로 예상되었다.
간단하게 MongoDB를 사용하기 위해 조사했던 내용을 정리하고 넘어간다.
NoSQL은 SQL을 사용하는 관계형 데이터베이스가 아닌 데이터베이스를 의미한다.
RDBMS와 비교하여 스키마가 없어 데이터 모델이 자유롭고, 수평 확장 가능한 분산 아키텍쳐이며 객체 기반 API를 제공한다는 장점이 있다.
MongoDB는 Document 기반으로 구성되어 있고, 오픈 소스로 널리 사용된다.
Database > Collection > Document > Fields 순으로 저장된다.
또한 ACID 대신 BASE를 지원한다. BASE는 ACID와 대립되는 개념으로 Basically Available(기본적으로 언제든지 사용할 수 있다.), Soft state(외부의 개입 없이도 정보가 변경될 수 있다.), Eventually consistent(일시적으로 일관적이지 않은 상태가 되어도 일정 시간 후 일관적인 상태가 되어야 한다.)의 약자이다.
MongoDB 연동을 위해 Mongoose를 사용하였다.
Mongoose는 MongoDB의 ODM (Object Document Mapping: 객체와 문서 1대 1로 매칭하는 역할)으로, 그 중 가장 유명하다.
// server.ts 중 mongoose 파트
import dotenv from "dotenv";
import mongoose from "mongoose";
dotenv.config();
declare let process : {
env : {
MONGODB_URL : string;
KAS_ACCESSKEY_ID : string;
KAS_SECRET_ACCESS_KEY : string;
PORT: string;
}}
const MONGO_DB_URL = process.env.MONGODB_URL;
async function initMongoose() {
await mongoose.connect(MONGO_DB_URL) // MongoDB와 서버 연결
.then(() => {
console.log("MongoDB Connection succeeded");
})
.catch((e: Error) => {
console.log("seq ERROR: ", e);
});
}
void initMongoose();
.
└── models
├── admin.ts // 관리자 정보 관리
├── base.ts // 기본 캐릭터 관리
├── character.ts // 사용자 캐릭터 관리
├── deco.ts // 사용자 캐릭터 - 꾸미기 아이템 조인 관리
├── health.ts // 건강 정보 관리
├── item.ts // 꾸미기 아이템 관리
├── log.ts // 사용자 API 사용 로그 관리
├── medication.ts // 약 복용 정보 관리
├── nft.ts // 사용자 발행 NFT 관리
├── prescription.ts // 약 처방 정보 관리
└── user.ts // 사용자 정보 관리
예시로 사용자 정보 관리 model 코드는 다음과 같다.
// user.ts
import { model, Schema } from "mongoose";
const user = new Schema({
nickname: String,
email: String,
dateOfBirth: String,
password: String,
pointBalance: Number,
disease: [Number],
createdAt: String,
phoneNumber: String,
firebaseToken: String
})
module.exports = model('User', user); // user스키마를 User라는 이름으로 export
User 데이터 정보 입출력 시 다음과 같이 사용했다.
// user.ts
import { getUserInfoByToken } from "../../utils/jwt"
import { status } from "../../constants/code"
import { createLog } from "../../utils/log"
const User = require("../../models/user")
const moment = require("moment")
type user = {
_id: string
email: string
password: string
nickname: string
dateOfBirth: string
pointBalance: number
createdAt: string
phoneNumber: string
disease: [number]
}
type token = {
jwt:string
}
export default {
Mutation: {
async join(_:any, args: {nickname:string, email:string, dateOfBirth:string, password:string, phoneNumber:string, disease:[number]}) {
const crypto = require('crypto');
const encryptedPassword = crypto.createHmac('sha256', process.env.PASSWORD_SECRET).update(args.password).digest('hex');
// User 컬렉션에서 email이 일치하는 document 조회
const savedUser = await User.findOne({
email:args.email
});
if(savedUser) return status.ALREADY_EXISTS_DATA
const newUser = new User()
newUser.nickname = args.nickname
newUser.email = args.email
newUser.dateOfBirth = args.dateOfBirth
newUser.password = encryptedPassword
newUser.pointBalance = 0
newUser.createdAt = moment().format("YYYY-MM-DD HH:mm:ss")
newUser.phoneNumber = args.phoneNumber
newUser.disease = args.disease
const res = await newUser.save() // User 컬렉션에 신규 document 저장
if(!res) return status.SERVER_ERROR
return status.SUCCESS
},
async login(_:any, args: {email:string, password:string, firebaseToken:string}) {
const crypto = require('crypto');
const encryptedPassword = crypto.createHmac('sha256', process.env.PASSWORD_SECRET).update(args.password).digest('hex');
const loginUser = await User.findOne({
email:args.email, password:encryptedPassword
});
if(!loginUser) return status.WRONG_USER_INFO
createLog("login", loginUser._id)
await User.updateOne(
{_id: loginUser._id},
{firebaseToken: args.firebaseToken}
)
const jwt = require('jsonwebtoken')
const accessToken = jwt.sign(
{
_id: loginUser._id,
email: loginUser.email,
nickname: loginUser.nickname
},
process.env.ACCESS_SECRET,
{expiresIn:'365d'}
)
return {"jwt": accessToken, "email": loginUser.email, "nickname": loginUser.nickname, "_id":loginUser._id}
},
async updateUserInfo(_:any, args:{jwt:string, nickname:string, password:string, phoneNumber:string, email:string, disease:[number]}) {
const userInfo = getUserInfoByToken(args.jwt)
if(!userInfo) return status.TOKEN_EXPIRED
var newData = {}
if(args.password !== null && args.password !== undefined) {
const crypto = require('crypto');
const encryptedPassword = crypto.createHmac('sha256', process.env.PASSWORD_SECRET).update(args.password).digest('hex');
newData = {nickname:args.nickname, password:encryptedPassword, phoneNumber:args.phoneNumber, email:args.email, disease:args.disease}
} else {
newData = {nickname:args.nickname, phoneNumber:args.phoneNumber, email:args.email, disease:args.disease}
}
// _id가 userInfo._id인 Document를 조회하여 newData로 필드를 수정한다.
const res = await User.updateOne({_id:userInfo._id}, newData)
if(!res) return status.SERVER_ERROR
return status.SUCCESS
},
async updateUserPassword(_:any, args:{jwt:string, _id:string, password:string}) {
const userInfo = getUserInfoByToken(args.jwt)
if(!userInfo) return status.TOKEN_EXPIRED
const crypto = require('crypto');
const encryptedPassword = crypto.createHmac('sha256', process.env.PASSWORD_SECRET).update(args.password).digest('hex');
const res = await User.updateOne({_id:args._id}, {password:encryptedPassword})
if(!res) return status.SERVER_ERROR
return status.SUCCESS
}
}
}
Mongoose를 사용하면서 MYSQL의 JOIN이나 COUNT같은 기능을 사용하고 싶어 비슷한 기능을 찾아 사용하였다. 처음에는 어색했지만 익숙해지자 사용하기 편했다.
유용하게 사용한 연산자는 다음과 같다.
$match
, $group
, $sort
const aggregatorOpts = [
{
$match:{// 조회 시 조건
createdAt:args.createdAt
}
},
{
$group:{
_id:"$methodName", //methodName 필드 명으로 그룹화하고
count:{$sum:1} // 조회된 결과를 더한다.
}
},
{
$sort:{
'count':1 // methodName으로 그룹화한 수를 count를 key로 value를 리턴한다.
}
}
]
const logs = Log.aggregate(aggregatorOpts).exec()
$inc
await Prescription.updateOne(
{userId:userInfo._id, medicine:args.medicine},
// lastMedicationCount 필드 값 -= 1
{$inc: { lastMedicationCount: -1}}
)
https://kciter.so/posts/about-mongodb
https://devlog-h.tistory.com/27