어떻게 mongoDB 스키마 설계를 해야할까?

스포하자면 만들고자 하는 서비스에 알맞게 만들어야한다(?) 당연한 것 아닙니까?,,

⚠️

이 글은 927 전에 작성되었습니다.
최신 정보가 아닐 수 있습니다.

mongoDB 스키마 디자인 설계

현재 진행 중인 프로젝트의 DB를 설계할 때, SQL 처럼 테이블을 잘게 잘게 쪼개야하나?라는 고민을 하게 되었습니다. 특히, 채팅 서비스의 DB 구조를 어떻게 해야 할지 잘 몰라서 찾던 도중 mongoDB 의 좋은 글을 읽고 이 게시글을 작성합니다!

mongoDB는 다른 관계형 DB처럼 스키마 설계를 하지 말라고 합니다.

우선은 관계형 DB와 mongoDB를 비교하도록 하겠습니다.

관계형 데이터베이스

관계형 DB의 스키마를 디자인할 땐, 우선 필요한 데이터를 정하고, 정규화 과정을 통해 데이터들이 중복되지 않도록 여러 테이블로 나눕니다.

예를 들어 하나의 유저 테이블이 있고, 프로젝트 테이블도 있을 때 프로젝트 테이블의 외래키유저기본키를 등록하여 쿼리가 가능하도록 설계한다고 합니다. (안 써봐서 잘 몰라요😂)

더불어 관계형 데이터베이스의 경우 기존에 작성된 스키마를 수정하기 어렵다는 의견이 많이 있습니다. (변경이 있을 때 기존에 생성된 데이터들도 수정을 요하기 때문?,,)

mongoDB

mongoDB의 경우 매우 유연하게 스키마를 수정이 가능합니다.

우선 스키마 디자인을 할 때 고려할 수 있는 디자인 방식은 Embedding or Referencing 방식입니다.

Embedding


장점

  • 하나의 쿼리문으로 필요한 모든 데이터를 가져올 수 있습니다.
  • $lookup 과 같은 Join 동작을 수행하지 않고 데이터를 가져올 수 있습니다.

한계점

  • Document가 커지면 오버헤드 또한 함께 커진다 (문서의 크기를 제한하여 쿼리 성능도 챙길 수 있습니다.)

  • Document16mb 의 크기 제한을 갖고 있어서 Embedding방식을 계속 사용한다면 언젠간 한계에 도달할 수도 있습니다.

Referencing


참조형 같은 경우 각각의 Document들이 가지고 있는 특별한 id 인 object id$lookup 을 이용해 데이터를 참조할 수 있습니다.

이는 관계형 데이터베이스에서의 JOIN흡사하게 동작하며, 이러한 구조는 데이터를 효율적이고 관리하기 쉽게 나누면서도 데이터 간의 관계를 유지할 수 있습니다.

장점

  • Document를 쪼개어 더욱 작은 단위의 Document를 가질 수 있으며, 이를 통해 16mb 크기 제한을 피할 수 있습니다.
  • 필요하지 않은 정보에 대한 접근을 줄일 수 있습니다.
  • Document 를 참조하여 데이터를 가져오기에 Embedding 방식보다 데이터의 중복을 줄일 수 있습니다.

한계점

  • 하나의 Document 안에 다수의 데이터가 참조형이라면 다수의 쿼리, $lookup, populate필요합니다. (현재 프로젝트하며 걱정인 부분)

설계

One-to-One


User

{
    "_id": "ObjectId('mdkalsfmk2')",
    "nickname": "junseo",
    "campus": "42seoul",
}

위와 같은 User document 가 있을 때, nickname 이나 campus 와 같이 하나만 존재하는 값이 1:1 관계며, key-value 로 모델링 할 수 있습니다.

One-to-Few


User

{
    "_id": "ObjectId('mdkalsfmk2')",
    "nickname": "junseo",
    "campus": "42seoul",
    "projects": [
        { "name": "42WE", "isDone": false},
        { "name": "Decrypto", "isDone": true}
    ]
}

위와 같은 User document 에서 projects 의 타입은 배열이며 2개의 값을 가지고 있습니다. 이렇듯 User document 에서 projectsOne-to-Few 관계입니다.

One-to-Few 처럼 몇 개의 데이터만 가지고 있을 경우엔 값을 Embedding 구조로 설계합니다.

One-to-Many 자식참조


User

{
    "_id": "ObjectId('mdkalsfmk2')",
    "nickname": "junseo",
    "campus": "42seoul",
    "projects": [
        { "name": "42WE", "isDone": false},
        { "name": "Decrypto", "isDone": true}
    ],
    "creditCard": ["ObjectID('1234')", "ObjectID('4321')", "ObjectID('9399')"]
}

Card

{
    "_id": "ObjectId('4321')",
    "bank": "kakao",
    "isActive": "true",
    "accountNumber": "1234-56-7891234"
}

위의 User document 에서 creditCardCard 스키마로 생성된 한 Document 를 참조하는 중입니다.

이처럼 한 유저가 여러 카드를 갖고 있으며 따로 관리가 필요할 때, 관계가 필요할 때 One-to-Many 구조로 설계합니다.

가능한 경우 $lookup 이나 populate 같은 Join 을 피하지만 이를 통해 더 나은 스키마 디자인 설계가 가능하다면 걱정하지 말고 사용하기!

One-to-Squillions 부모참조


ChatRoom

{
    "_id": "ObjectId('4321')",
    "createdAt": ISODate("2021-04-28T09:42:41.382Z"),
    "users":"???",
}

Chat

{
    "_id": "ObjectId('328492489')",
    "chatRoom_id": "ObjectId('4321')", // 부모의 obj id 를 참조
    "createdAt": ISODate("2021-04-29T12:42:41.382Z"),
    "content":"피곤쓰",
}

이러한 채팅방, 채팅 스키마를 디자인할 때, One-to-Many 방식에서 Embedding 구조를 사용한다면, 채팅 데이터가 300개 넘어갔을 때 Document16mb 제한을 초과해 버릴 것입니다. 더불어 Referencing 구조를 사용한다 하더라도 ObjectID 가 몇 천 개 쌓인다면 똑같이 용량 초과될 것입니다.

One-to-Squillions 구조를 사용한다면 채팅방의 ObjectID 로 해당 채팅방에서 주고받은 데이터를 쿼리 할 수 있게 되고, 용량 제한을 피하면서도 채팅방과 채팅의 관계를 유지시킬 수 있습니다.

결론

  • mongoDB schema design 의 가장 좋은 예는 만들고자 하는 서비스에 알맞게 만드는 것.

  • mongoDB 디자인은 관계형 디비 디자인처럼 하지 말자.

  • 이전에 걱정하던 다수의 쿼리 접근의 경우, 이를 통해 더 나은 스키마 디자인 설계가 가능하다면 걱정하지 말자.

  • Document 를 나눠야 할 이유가 명확하지 않다면 Embedding 하여 사용하자.

  • Document 값 중 배열의 경우 제한 없이 데이터가 추가되다 보면 16mb 를 넘길 수 있기에 조심하자.

  • 원래는 ChatOne-to-Many 구조로 하려고 하였으나 글을 읽고나니 One-to-Squillions 구조로 설계해야하는 이유를 알게됐다.

One-to-One : key-value 선호

One-to-Few : Embedding 선호

One-to-Many : Embedding 선호

One-to-Squillions : Referencing 선호

Many-to-Many : Referencing 선호