[Pill So Good] (2) MongoDB 연동하기

HY·2022년 8월 1일
0

pill-so-good

목록 보기
2/6
post-thumbnail

MongoDB 선택

이번 프로젝트에선 MySQL 대신 MongoDB를 사용하였다.
익숙한 MySQL 대신 NoSQL을 사용해보고 싶기도 했고, 기획을 하다 보니 저장할 데이터가 계속 변경될 것으로 예상됐기 때문에 MongoDB가 적절할 것으로 예상되었다.

MongoDB?

간단하게 MongoDB를 사용하기 위해 조사했던 내용을 정리하고 넘어간다.
NoSQL은 SQL을 사용하는 관계형 데이터베이스가 아닌 데이터베이스를 의미한다.
RDBMS와 비교하여 스키마가 없어 데이터 모델이 자유롭고, 수평 확장 가능한 분산 아키텍쳐이며 객체 기반 API를 제공한다는 장점이 있다.

MongoDB는 Document 기반으로 구성되어 있고, 오픈 소스로 널리 사용된다.
Database > Collection > Document > Fields 순으로 저장된다.
또한 ACID 대신 BASE를 지원한다. BASE는 ACID와 대립되는 개념으로 Basically Available(기본적으로 언제든지 사용할 수 있다.), Soft state(외부의 개입 없이도 정보가 변경될 수 있다.), Eventually consistent(일시적으로 일관적이지 않은 상태가 되어도 일정 시간 후 일관적인 상태가 되어야 한다.)의 약자이다.

서버 실행 시 MongoDB 연동

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();

Model 구현

.
└──  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

profile
사실은 공부를 비밀스럽게 하고 싶었다

0개의 댓글