ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [MongoDB] MongoDB 사용하며 헷갈리는 부분 정리
    데이터베이스/Mongodb 2023. 3. 29. 20:46

     

    틀린 내용을 발견하신 경우 말씀 부탁드립니다! 🙇


     

    MySQL에 익숙했던 나로서 MongoDB를 사용하며 막막했던 것은 설계를 어떻게 해야 좋을지였다. 그리고 또 하나, Nest.js에서 비슷한 내용을 DTO, Interface, Schema 등 여러 개의 폴더와 파일로 나누어서 관리를 하는데, 굳이 왜 이렇게 해야 하는지 감이 잘 안 왔다. 개인적으로 답답했던 부분을 알아보는 김에 MongoDB 개념까지 가볍게 정리해 보기로 했다🔥
     

     

    1. MongoDB 란

    NoSQL 기반 DBMS의 하나(NoSQL에 대해서는 밑에서 정리). MongoDB는 DB안에 Collection들이 존재하고, Collection안에 Document가 존재한다. 아래는 관계형 DBMS 구조와 MongoDB 구조를 비교한 것이며, 주로 RDBMS만 사용한 사람(나)이 보면 대략적인 그림이 그려질 것이다.

    https://www.c-sharpcorner.com/article/approaching-mongodb-design-basic-principles/

     

    MongoDB의 Document는 key(Field)와 value로 구성되며, value의 타입은 String, Number 외 다양하게 사용할 수 있다.

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

     

    MongoDB를 검색하면 나오는 Schema less는 스키마가 없다는 뜻이 아니라 고정된 스키마가 없다는 의미이다. MySQL에서는 테이블 단위로 스키마가 정해져 있고 데이터는 그에 맞춰 저장해야 한다. 반면 MongoDB에서는동일한 Collection에 있는 Document일지라 하더라도 서로 다른 구조를 가질 수 있다. 간단히 이야기하면 MongoDB에서 Schema는 Document 데이터 구조를 의미하는 것이다.

    https://www.mongodb.com/docs/manual/core/data-modeling-introduction/

     

    그래서 실제로 MongoDB 상에서는 같은 Collection내에 있는 Document일지라도 아래와 같이 다른 구조로 저장될 수 있다.

    [
        # document 1
        {  name : “Joe”, age : 30, interests : ‘football’ },
        # document 2
        {  name : “Kate”, age : 25 },
        ...
    ]

     

     

    2. NoSQL과 RDBMS 비교

    NoSQL (비관계형 DBMS)

    • 수평적 확장(Scale out) 가능. 수직적 확장도 가능.
      → 확장성 좋음
    • 여러 대의 백업 서버 구성을 할 수 있기 때문에 장애 발생 시 무중단 서비스 가능.
      → 가용성 좋음
    • 유연한 스키마를 갖고 있기 때문에, 명시되지 않은 필드 추가 및 삭제 가능.
      → 유연성 좋음
    • 컬렉션간 관계가 존재하지 않음. 
    • 비정형 데이터를 처리하기 쉬움.
    • 중복된 값이 존재할 수 있기에 데이터 정합성이 떨어짐.
    • 읽기 연산이 빈번한 케이스에서 사용하기 적합. 
    • 구조화된 질의어가 없음.

     

    RDBMS (관계형 DBMS)

    • 수직적 확장 가능(Scale up). 수평적 확장은 어려움.
    • 테이블 생성 시 스키마를 정하므로 스키마에 맞지 않는 데이터는 테이블에 들어갈 수 없음.
      데이터 무결성 보장
    • 테이블 간에 관계가 존재하며 관계를 통해 테이블 간 조인(join) 가능.
    • 중복되는 값이 없는 게 기본이기에 데이터 정합성이 높음.
    • 데이터 수정이 자주 발생하는 케이스에서 사용하기 적합.
    • 스키마가 엄격하므로 비정형 데이터를 처리하기 힘듦.
    • 구조화된 질의어가 있음.

     

     

    3. MongoDB에서의 데이터 모델링 방식

    난 MySQL DB 설계할 때 아래와 같은 흐름으로 진행했었다.

    서비스에서 필요한 기능 정의 > 기능에 필요한 데이터 확인 > 테이블 정의 > 테이블 간의 관계 설계

     

    그럼 Mongodb에서 효율적으로 데이터를 저장하기 위한 데이터 구조를 설계하는 과정은 어떨까?

    필요한 기능 정의 > 기능에 필요한 데이터 확인 > Document 설계 > Collection 설계

    (딱 맞아 떨어지는 것은 아니지만) MySQL은 큰 범위에서 작은 범위 순서대로 설계를 진행한다면, MongoDB는 작은 범위에서 큰 범위로 설계를 진행한다고 느껴졌다. 


    Document 설계는 기능과 데이터에 따라 Document 구조를 어떻게 구성할지 고민하는 단계이며, 데이터의 관계를 표현하는 방식에 따라 크게 임베디드 방식과 레퍼런스 방식으로 나뉜다고 한다. 

    • 임베디드 방식과 레퍼런스 방식 둘 중 어떤 걸 선택해서 적용할지가 MongoDB 스키마 설계의 핵심.
    • Collection설계는 컬렉션 명을 정하고 적절한 컬렉션 종류를 선택하는 것.
    • 레퍼런스 방식은 또 세부적으로 나누면 자식참조, 부모참조, 상호참조 방식으로 나뉨.

     

     

    4. NestJS 환경에서 MongoDB

    Nest.js에서 MongoDB를 사용한다면 DTO, Schema, Interface 이 3개 유형의 파일을 보게 될 것이다. 내용을 보면 비슷비슷하게 느껴지는데, 각각 어떤 역할을 하는지 알아보고 왜 써야 하는지 확인해 보자. (참고로 DTO, Schema, Interface는 Nest.js에만 국한된 개념은 아니다.) 

     

    DTO (Data Transfer Object) 

    DTO는 데이터가 네트워크를 통해 전송되는 방법을 정의한 객체이다. DTO 스키마는 인터페이스(Interface) 또는 클래스(Class) 방식을 통해 정할 수 있다. (Nest.js 공식 문서에서는 클래스 방식을 추천하고 있다. 클래스는 Javascript ES6 표준 중 일부이기 때문에 컴파일된 Javascript 코드에서 실제 엔티티로 보존되는 반면, Typescript 인터페이스는 변환 중에 제거되기 때문에 Nest가 런타임에서 이를 참조할 수 없다고 한다.) 정의된 DTO는 아래와 같이 사용된다. 즉 DTO를 통해 전달받을 값의 형태를 정의하고 이를 controller에서 참조하여 서버 API로 들어온 값이 양식에 맞는 값인지 확인고 걸러낼 수 있다.

    // create-cat.dto.js
    // class 방식으로 DTO 정의
    export class CreateCatDto {
      name: string;
      age: number;
      breed: string;
    }
    
    
    // cats.controller.js
    @Controller('cats')
    export class CatsController {
      @Post()
      // 정의한 DTO를 controller에서 사용
      create(@Body() createCatDto: CreateCatDto) {
        return 'This action adds a new cat';
      }
      ...
      
     }

     

    Schema

    위에서 이야기했듯 MongoDB는 유연한 스키마를 가진 것이지 스키마가 없는 게 아니다. MongoDB 내에서 Schema를 정의한다는 것은 document가 어떤 형태인지 정해주는 것이라 생각하면 된다. NestJS에서 MongoDB 스키마를 정의하는 방법은 크게 2가지이다.

    1. NestJS 데코레이터를 통해 스키마 생성
    2. Mongoose 자체를 사용해서 스키마 생성 - Mongoose: MongoDB와 node.js를 위한 Object Data Modeling(ODM)

     

    1. NestJS 데코레이터를 통해 스키마 생성

    데코레이터를 사용해 스키마를 생성하면 코드 가독성을 높일 수 있다고 하는데 이건 각자 편한 방식대로 진행하면 될 듯하다. 아래는 NestJS 데코레이터를 사용해 스키마를 정의한 것이다.

    import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
    import { HydratedDocument } from 'mongoose';
    
    // 1
    export type CatDocument = HydratedDocument<Cat>;
    
    // 2
    @Schema()
    export class Cat {
      @Prop()
      name: string;
    
      @Prop()
      age: number;
    
      @Prop()
      breed: string;
    }
    
    // 3
    export const CatSchema = SchemaFactory.createForClass(Cat);

     

    순서대로 보면 크게 3개 부분으로 나뉜다. 첫 번째 부분은 Cat 타입의 Mongoose Document를 CatDocument type으로 정의한다. HydratedDocument<Cat>를 통해 여러 메소드와 Mongoose 특정 기능이 포함된 Mongoose Document를 나타낼 수 있다.

    export type CatDocument = HydratedDocument<Cat>;

     

    두 번째 부분은 @Schema, @Prop 데코레이션을 사용해 스키마를 정의하는 곳으로, document의 필드와 타입을 정의한다.

    @Schema()
    export class Cat {
      @Prop()
      name: string;
    
      @Prop()
      age: number;
    
      @Prop()
      breed: string;
    }

     
    마지막으로 세 번째 부분이다.

    export const CatSchema = SchemaFactory.createForClass(Cat);

    SchemaFactory.createForClass(Cat) 부분이 실행되면 NestJS 내부에서 Class 문법을 Mongoose 스키마 문법으로 변환하여 아래와 같이 만든다. 

    const schema = new mongoose.Schema({
        name: { type: String },
        age: { type: Number },
        breed: { type: String }
    });

     

    이 부분까지 실행이 되어야 정의한 대로 스키마를 생성된다. (참고로 데코레이터를 통해 스키마를 정의할 때 type은 string, number인데, Mongoose 스키마 문법으로 변환된 후에는 String, Number로 변경된다.)

     

    2. Mongoose 자체를 사용해서 스키마 생성

    Mongoose에서의 모든 것은 스키마(Schema)로부터 시작된다. 각각의 스키마는 MongoDB 컬렉션(Collection)과 연결되고, 컬렉션 안의 도큐먼트(document)의 형태를 정의한다. 정의한 스키마를 사용하기 위해서는 모델(Model)로 변환시킬 필요가 있고, mongoose에서 제공하는 mongoose.model() 메소드를 통해 스키마를 모델로 변환시킬 수 있다. mongoose.model()의 첫 번째 인자는 생성할 모델 이름이며 두 번째 인자는 정의한 스키마이다. 이렇게 생성된 모델로 인스턴스를 생성할 수 있으며, 여기서 인스턴스는 곧 document다.

    // 스키마 정의
    const schema = new mongoose.Schema({ name: 'string', size: 'string' });
    // 모델 생성
    const Tank = mongoose.model('Tank', schema);
    // 모델 인스턴스 생성 (다큐먼트)
    const small = new Tank({ size: 'small' });
    
    small.save(function (err) {
      if (err) return handleError(err);
      // saved!
    });

     
    Schema Type
    스키마 타입(SchemaType)은 document의 필드(field) 타입을 설정할 때 사용하는 객체이다. 사용 가능한 스키마 타입은 다음과 같다.

    // 스키마 정의
    const schema = new Schema({
      name: String,
      age: Number,
      updated: { type: Date, default: Date.now },
    });
    
    // 모델 생성
    const Thing = mongoose.model('Thing', schema);
    
    // 도큐먼트 생성
    const m = new Thing;
    m.name = 'Statue of Liberty';
    m.age = 125;
    m.updated = new Date;

     
    아래와 같이 'type' key를 이용해 필드 타입을 정의할 수도 있다.

    // 3 string SchemaTypes: 'name', 'nested.firstName', 'nested.lastName'
    const schema = new Schema({
      name: { type: String },
      nested: {
        firstName: { type: String },
        lastName: { type: String }
      }
    });

     
    옵션 목록
    type 외에도 여러가지 옵션들이 있으며 아래는 그 일부이다. 이 외 옵션은 링크 참고

    • required: boolean or function, if true adds a required validator for this property
    • default: Any or function, sets a default value for the path. If the value is a function, the return value of the function is used as the default.
    • select: boolean, specifies default projections for queries
    • validate: function, adds a validator function for this property
    • get: function, defines a custom getter for this property using Object.defineProperty().
    • set: function, defines a custom setter for this property using Object.defineProperty().
    • alias: string, mongoose >= 4.10.0 only. Defines a virtual with the given name that gets/sets this path.
    • immutable: boolean, defines path as immutable. Mongoose prevents you from changing immutable paths unless the parent document has isNew: true.
    • transform: function, Mongoose calls this function when you call Document#toJSON() function, including when you JSON.stringify() a document.
    • index: boolean, whether to define an index on this property.
    • unique: boolean, whether to define a unique index on this property.
    • sparse: boolean, whether to define a sparse index on this property.

     

     

    Interface

    인터페이스에 대한 짧은 예시를 보자. 아래 코드에서 human라는 인터페이스를 정의하고, Tom객체를 human 타입으로 정의했다. 즉 인터페이스는 함수나 클래스 등에서 타입을 검사하고 강제하는데 활용된다.

    interface human {
      name:string;
      age:number;
      gender:string;
    }
    
    const Tom: human = {
      name:'Tom Potter',
      age: 35,
      gender:'Male'
    }

     

    인터페이스는 Nest.js 내부에서 다음과 같이 사용된다. Mongoose Document 기반으로 Cat이라는 인터페이스를 생성한 뒤, 해당 인터페이스를 Service 쪽에서 모델의 타입으로 참조하여 사용한다. 위에 정리한 내용 중 Schema 파일에서 class 형식으로 스키마를 정의하고, 이를 토대로 Document를 생성하는 부분과 동일한 것이라 생각된다. 만약 이게 맞다면 파일 관리하기 힘들기 때문에 그냥 class 방식 사용하는 걸로 통일하는 게 편한 것 같다. 

    // cat interface
    import { Document } from 'mongoose';
    
    export interface Cat extends Document {
      readonly name: string;
      readonly age: number;
      readonly breed: string;
    }
    
    // cats.service.ts
    import { Model } from 'mongoose';
    import { Injectable, Inject } from '@nestjs/common';
    import { Cat } from './interfaces/cat.interface';
    import { CreateCatDto } from './dto/create-cat.dto';
    
    @Injectable()
    export class CatsService {
      constructor(
        @Inject('CAT_MODEL')
        private catModel: Model<Cat>,
      ) {}
    
      async create(createCatDto: CreateCatDto): Promise<Cat> {
        const createdCat = new this.catModel(createCatDto);
        return createdCat.save();
      }
    
      async findAll(): Promise<Cat[]> {
        return this.catModel.find().exec();
      }
    }

     

     

    글을 마무리 하면서 MongoDB 털린 내용을 기록한 포스팅도 첨부한다. MongoDB 사용시 계정 설정 꼭 하자!

    https://jaejade.tistory.com/179

     

    [MongoDB] DB 털림 (랜섬웨어)

    발견 아침에 테스트 앱에서 데이터가 보이지 않았다. docker 컨테이너 문제인가 싶었으나, 확인해 보니 mongodb 컬렉션이 사라졌다. mongo db log를 확인해 보았다. dropDatabase..? $ sudo docker logs mongodb -t --s

    jaejade.tistory.com

     

     

    요약

    궁금했던 부분들을 어느정도 정리할 수 있었고, 다음과 같이 요약해볼 수 있겠다.

    • MongoDB 설계는 Document 구조를 먼저 생각해야한다. 즉 MongoDB의 스키마 정의는 document 형태를 정하는 것이다.(설계 관련한 건 나중에 다시 참고자료 읽어보자!)
    • 정의한 스키마는 모델(Model)로 변환하여 DB 읽기, 쓰기에 사용할 수 있다.
    • DTO를 사용하면 API를 통해 서버로 들어오는 데이터가 형식에 맞는 데이터인지 확인하기 용이하다.
    • 인터페이스 방식과 클래스 방식 중 클래식 방식을 사용하도록 하자.
    • DTO, Schema, Interface 중 Interface는 굳이 사용 안해도 될 듯...? (이건 잘 모르겠다.)

     

     


    참고자료

    댓글

jaejade's blog ٩( ᐛ )و