본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


Node.js/Nestjs (Nest.js)

Nestjs 프레임워크 서버(Mongodb, database) -14

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2022. 9. 16.

 

비관계형 데이터베이스 중 몽고DB는 제가 비관계형 데이터베이스에서 가장 좋아하는(?) 데이터 베이스 입니다.

무료에다 성능 또한 훌륭하기 때문 입니다!

얼마전 데이터베이스 인기순위에서도 1등(비관계 중)을 한 것을 볼 수 있었습니다. * 기준 : 2022.09

* 출처 : https://db-engines.com/en/ranking

 

Nestjs에서 Mongodb를 사용하기 위해서는 역시나 모듈을 설치해야 합니다.

Express 프레임워크에서부터 널리 사용 하는 몽구스(mongoose)를 설치 해 줍니다.

npm install @nestjs/mongoose mongoose

 

#1. 도큐먼트(모델, 스키마) 생성

도큐먼트란 데이터를 매핑하기 위한 ORM의 첫번째 단계 입니다.

시퀄라이즈에서의 모델을 만들어 준 것 처럼 동일한 방식으로 클래스를 만들어야 합니다.

특히 몽고db같은 경우에는 데이터에 대한 제약조건이 없으므로 도큐먼트를 만들때 제약조건(not null, type 등)또한 고려를 해 주어야 합니다.

* 파일이름 : 내도큐먼트.ts

import { Prop, Schema, SchemaOptions, SchemaFactory } from '@nestjs/mongoose';
import { Document, Schema as sch } from 'mongoose';

const options: SchemaOptions = {
  timestamps: true,  //자동으로 등록일, 수정일을 넣어줍니다.
  collection : 'testCollection',
  _id : true  //기본 인덱스인 id값을 매핑하여 줍니다.
};

@Schema(options)
export class 내도큐먼트 extends Document {
 
  @Prop({type : sch.Types.String})
  text : string;

  @Prop({type : sch.Types.Number})
  num : number;

  @Prop({type : sch.Types.Array})
  arr : Array<any>;  
}
export const 내스키마 = SchemaFactory.createForClass(내도큐먼트);
export type 내타입 = 내도큐먼트 & Document;

 

몽구스(mongoose) Document 라는 클래스를 상속받는 "내도큐먼트" 라는 클래스 입니다.

해당 클래스는 testCollection 컬렉션을 매핑하게 되어 있습니다.

testCollection 이라는 이름의 컬렉션에 3개의 속성을 가진 도큐먼트를 정해주었습니다.

옵션을 자세히 살펴보면 _id와 timestamps 라는 항목의 값이 true 인 것을 볼 수 있습니다.

해당 값에 의해서 자동으로 _id값과 등록(createdAt), 수정(updatedAt)과 관련된 속성값이 생성/매핑 될 예정 입니다.

 

여기서 export 부분주의해야 합니다!

"내스키마" 는 몽구스 모듈이 사용할 스키마를 만드는 객체이고,

"내타입"은 나중에 사용할 서비스에서의 모델의 제네릭에 사용될 타입 입니다.

 * 코드로 내용을 이해하는 게 좋을 수도 있겠습니다....

 

#2. 모듈 추가

도큐먼트를 등록 하였다면 이제 모듈을 추가하여 줍니다.

* 파일이름 : app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
import {내도큐먼트, 내스키마} from './내도큐먼트';

@Module({
  imports: [
    //아이디(id), 비밀번호(pwd)가 있는 경우 : mongodb://id:pwd@localhost:27017/디비명
    MongooseModule.forRoot('mongodb://localhost:27017/디비명', {  }),
    MongooseModule.forFeature([{name : 내도큐먼트.name, schema : 내스키마}])
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 

forRoot 함수가 사용되는 부분은 몽고DB에 접속하기 위한 기능입니다.

만약 커넥션과 관련된 오류가 난 다면 해당 몽고db가 외부접속이 가능한지 확인해야 하며,

권한과 관련된 오류가 나면 아이디와 비밀번호를 확인 후 주석처리된 방법을 통해서 요청해야 합니다.

 

다음으로는 사용할 도큐먼트를 몽구스(mongoose)모듈에 알려주는 것 입니다.

forFeature라는 함수를 통해서 사용 할 도큐먼트를 배열 형태로 등록하여 줍니다.

이때 "내도큐먼트" 클래스에서 export "내스키마"를 사용해야 합니다.

"내도큐먼트" 클래스에서 name이라는 값은 몽구스의 Document를 상속 받았기 때 문에 사용한 값 입니다.

여기 까지 오류가 나지 않았다면 이제 다음단계로 넘어 갑니다!

 

#3. 의존성 주입

컨트롤러에서 바로 사용하는 것 보다는 역시나 서비스에서 만들어진 도큐먼트를 주입받아서 사용하게 만듭니다.

* 파일이름 : app.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import {내도큐먼트, 내타입} from './내도큐먼트';

@Injectable()
export class AppService {

  constructor(@InjectModel(내도큐먼트.name) private doc : Model<내타입>) {

  }
  
}

 

 "내도큐먼트" 클래스에서 export "내타입"을 몽구스의 Model 클래스의 제네릭에 매핑하여 주었습니다.

몽구스의 Model 클래스는 데이터를 관리 할 때  "내도큐먼트" 클래스에서 선언한 데이터를 기반으로 동작하게 됩니다.

 

#3-1. 저장

먼저 저장관련 기능 입니다.

Model 클래스를 new 연산자를 통해서 인스턴스화 하여 줍니다.

이때  "내도큐먼트" 클래스에서 정의한 속성을 따라야 합니다.

그리고 나서 해당 객체에 save 함수를 통해서 해당 도큐먼트를 몽고db에 등록 할 수 있습니다.

createdAt과 updateAt, _id 속성이 자동추가 되었습니다!

 

일반적인 데이터 등록하는 방법 입니다.

내가 정의한 도큐먼트의 모습 그대로 데이터를 만들어서 넣어주면 됩니다.

* 파일이름 : app.service.ts

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import {내도큐먼트, 내타입} from './내도큐먼트';

@Injectable()
export class AppService {

  constructor(@InjectModel(내도큐먼트.name) private doc : Model<내타입>) {
    this.insertType1();
  }

  insertType1(){
    const data1 = new this.doc({text:'abcd', num : 1234, arr : [1,2,3,4,5]});
    data1.save();
  }
  
  
}

 

만약 데이터를 여러개 한번에 등록 한 다면 insertMany 함수를 사용할 수 있습니다.

* 파일이름 : app.service.ts

//생략..


  insertType2(){
    const docs = [
      new this.doc({text:'abcd', num : 5678, arr : [1,2,3,4,5]}),
      new this.doc({text:'ggqe', num : 8923, arr : [11,22]}),
      new this.doc({text:'refw', num : 5123, arr : ['aaa', 'fff', 12121]})
    ];
    this.doc.insertMany(docs);
  }

 

배열에 도큐먼트를 담아둔 뒤에 전역객체인 Model 클래스에서 insertMany  함수를 통해 저장하는 모습 입니다.

데이터를 한번에 등록 한다면 insertMany를 사용하는 것도 나쁘지 않습니다.

 

#3-2. 수정, 삭제

수정과 삭제하는 기능은 어렵지 않습니다.

수정과 삭제에서는 대상을 찾기위한 조건(where)이 필수 입니다.

여기서는 기본으로 생성되는 인덱스인 _id 값을 가지고 수정과 삭제에 사용하였습니다.

* 파일이름 : app.service.ts

  //생략..
  update (_id : string = '631992be602911b2c3b92474'){
    //upsert가 true 면 insert 행위를 함
    this.doc.updateOne({ _id }, {$set: {text:'bbbbbb'}}, {upsert : true}, err=>{
      console.log(err)
    });
  }

  delete (_id : string = '631992be602911b2c3b92474'){
    this.doc.remove({ _id }, err=>{
      console.log(err)
    });
  }

 

updateOne 함수는 1개의 데이터를 업데이트 할 때 사용되며, 여러개의 도큐먼트를 업데이트 하려면 updateMany 함수를 사용해야 합니다.

첫번째 값은 수정하기 위한 조건(where)을 입력 해 주도록 합니다.

두번째 값은 적용할 데이터를 의미 합니다.

세번째 옵션에서의 upsert는 없는 경우 등록(insert)  옵션입니다.

만약 upsert를 사용하는 데 도큐먼트의 속성이 없는 상태라면 데이터 수정간 오류가 발생하므로 주의하여야 합니다.

 

#3-3. 읽기

데이터를 읽는 것 또한 쉽습니다.

* 파일이름 : app.service.ts

  //생략..
  
  select(){
    this.doc.find({}, (err, arg : Array<any>)=>{
      if(err) return;
      arg.forEach(  (element : 내도큐먼트) => {
        console.log(element)
      });
    })
  }

 

첫번째 find 함수 뒤에 객체에는 조건을 붙여줄 수 있습니다.

가령 문자값을 가지고 있는 text 속성에서 ab라는 값이 있는 데이터를 조회 하려면 아래와 같이 작성 합니다.

find({text :{$regex : 'ab'}}, (err, arg : Array<any>)=>{
    if(err) return;
    arg.forEach(  (element : 내도큐먼트) => {
        console.log(element)
    });
})

 

조회에 사용되는 연산자는 몽고db 매뉴얼 홈페이지에서 확인가능 합니다.

https://www.mongodb.com/docs/manual/

 

What is MongoDB? — MongoDB Manual

Customer Stories Learn how businesses are taking advantage of MongoDB

www.mongodb.com

 

#3-4. 집계(aggregate, group)

몽고DB의 성능을 체감하려면 역시나 등록(insert) 행위 보다도 데이터를 그룹(aggregate, 집계)핑을 해 보아야 합니다.

몽구스 모듈은 몽고DB 쿼리를 거의 대부분 그대로 재현하고 있기 때 문에 같은 방식으로 사용 할 수 있습니다.

* 파일이름 : app.service.ts

  //생략..
  
groupBy(){
  this.doc.aggregate([
    { 
      $project: {   //가져올 데이터 선언
        text: 1, //컬렉션에 저장된 도큐먼트의 text속성입니다.
        num: 1,   //컬렉션에 저장된 도큐먼트의 num속성입니다.
        createdAt:1,  //컬렉션에 저장된 도큐먼트의 createdAt속성입니다.
        arr : 1,  //컬렉션에 저장된 도큐먼트의 arr속성입니다.
          
        //여기부터는 저장되어 있는 데이터를 변경하여 가져오는 부분 입니다.
        dateToStr: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },  //날짜->문자1
        dateToStrMonth: { $dateToString: { format: "%Y-%m", date: "$createdAt" } }, //날짜->문자2
        dateToStrYear: { $dateToString: { format: "%Y", date: "$createdAt" } }, //날짜->문자3
        ArrayCnt : { $cond: { if: { $isArray: "$arr" }, then: { $size: "$arr" }, else: 0} }, //배열의 갯수를 측정 후 숫자
        ArraySum : { $cond: { if: { $isArray: "$arr" }, then: { $sum: "$arr" }, else: 0} }  //배열의 숫자 원소의 총 합
      }
    },
    { $match: { num: { $gte: 5 }}},   //조건
    { 
      $group: { 
        _id : "$text",  //그룹할 대상
        firstCreateDate : { $first: "$createdAt" },  //가장 처음값만 가져오기
        lastCreateDate : { $last: "$createdAt" },  //가장 나중값만 가져오기
        dataSum : { $sum: "$num" },  //sum 속성의 합계 구하기
        dataCount : { $sum: 1 },  //그룹한 text의 총 갯수 
        ArraySum : { $sum: "$ArraySum" },  //ArraySum 값의 총 합
        ArrayCount : { $sum: "$ArrayCnt" }, //ArrayCnt 값의 총 합
        pushStyle: { $push: "$dateToStr" }  //dateToStr 값을 배열형태로 밀어넣기
      }
    }, 
    { $skip: 0 },  //페이징을 위한 시작지점 커서
    { $limit: 50 } //페이징을 위한 갯수
  ]).allowDiskUse(true).then( arg=>{
    console.log(arg)
  }).catch(err=>{
    console.log(err)
  })
}

 

주로 사용되는, 사용 할 만한 내용을 바탕으로 작성하여본 집계관련 내용 입니다.

몽고DB의 특성상 JOIN 같은 기능은 지양하기 때 문에 조회하는 컬렉션의 도큐먼트의 속성값만 가지고 기능을 만드는 것을 추천합니다.

* 만약 lookup과 unwind 연산자를 통해 다른 컬렉션을 join 하여 집계(aggregate)다면, 데이터가 몇십만건으로 늘어난 경우 조회가 불가능할 정도의 시간이 걸리는 것을 볼 수 있습니다.

* 통계가 필요한 컬렉션에 join이 없도록 설계하는 것이 몽고DB의 기초라 생각 합니다.

 

facet이라는 함수를 활용하면 다른종류의 집계 행위를 병렬로 처리하도록 할 수 있습니다.

* 파일이름 : app.service.ts

  //생략..
  
  groupByWithFacet(){
    this.doc.aggregate().facet({
      result1 : [{$group : {_id : "$text"}}],
      result2 : [{$group : {_id : "$num"}}]
    }).then( arg=>{
      console.log(arg[0])
    }).catch(err=>{
      console.log(err)
    })
  }

 

result1 이라는 키 값으로 1개의 집계동작을 하게 하였고, result2 라는 키 값으로 또 다른 집계 동작을 하게 하였습니다.

이렇게 동작한 결과는 사용자가 만든 키 값을 가지고 확인가능 합니다.

간단한게 조회하여 보았습니다.

 

여기까지 Nestjs에서 몽고db와 관련된 내용이였습니다.

위 내용에 사용된 코드는 아래 제 깃허브에서 받아볼 수 있습니다.

https://github.com/TaeSeungRyu/NestProject/tree/main/step13

 

GitHub - TaeSeungRyu/NestProject: Nestjs 프로젝트

Nestjs 프로젝트. Contribute to TaeSeungRyu/NestProject development by creating an account on GitHub.

github.com

 

공부하면 할 수록 재미있고 즐거운 nestjs!!!
궁금한점 또는 틀린부분은 언제든 연락 주세요

 

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글