MongoDB 모델링

MongoDB 모델링

최근에 시험도 준비하고, 일도 바빠서 쉽게 글을 남기기 힘들었는데, 쓸 내용들은 차곡차곡 쌓아두긴 했었다. 우선 정리와 복습도 할 겸, 최근 서비스에서 사용하고 있는 MongoDB 모델링 하는 걸 공부한 내용을 정리했다. 이 내용은 공식 문서를 보고 번역하고 재배열한 내용이다.

Flexible Schema

MongoDB는 어떤 데이터를 집어 넣을지 미리 결정되어 있는 “테이블”과 다르게, 스키마에 대해 자유로운 document(다큐멘트)가 모이는, collection(콜렉션)으로 구성된다.

  • 하나의 콜랙션에 있는 다큐멘트들은 같은 필드나, 같은 데이터 타입을 가질 필요가 없다.
  • 필드를 더하거나, 필드의 데이터 타입을 바꾸는 등, 다큐멘트의 구조를 바꾸려면, 그 특정 다큐멘트의 구조를 바꿔주면 된다.

실제로는 document rules를 강제해 하나의 콜렉션이 유사한 구조를 공유하도록 강제한다.

Document Structure

“MongoDB란 무엇일까?”에 대한 내용을 다루는 글이 아니므로, 스키마의 특징, 데이터 타입 등은 건너 뛰고, 다큐멘트 구조에 대해서 한 번 살펴보자. MongoDB 앱의 데이터 모델을 설계할 때 중요한 사항은 다큐멘트의 구조와 애플리케이션이 데이터 관계를 어떻게 나타내는지이다. MongoDB는 관련 데이터를 하나의 다큐멘트 안에 포함하도록 한다. 두 가지 방법으로 관계에 대한 표현을 할 수 있는데, 하나의 다큐멘트 안에 포함시키는 Embedded 방식, 참조하도록 하는 Reference 방식이다.

Embedded Data

Embedded Data 모델에서는 유관한 데이터를 하나의 다큐멘트에 담는다. 이러한 스키마는 일반적으로 “비정규화” 모델로 알려져있다.

임베디드된 데이터 모델들은 애플리케이션이 유관한 정보 조각들을 하나의 데이터베이스 레코드에 저장할 수 있게 한다. 결과적으로 애플리케이션은 더 적은 쿼리와 업데이트를 가지고 필요한 작업을 수행할 수 있게 된다.

엔티티 사이에 포함 관계가 있는 경우 (1:1 관계)

  1. Embedded Document Pattern
    “고객”과 주소 관계를 이어주는 예시를 생각해보자. addresspatron에게 종속되는 관계이다. 정규화 되어있다면, 아래와 같은 모습을 보인다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // patron document
    {
    _id: "joe",
    name: "Joe Bookreader"
    }

    // address document
    {
    patron_id: "joe",
    street: "123 Fake Street",
    city: "Faketon",
    state: "MA",
    zip: "12345"
    }

    이 경우, 만약 address의 데이터가 자주 이름 정보와 함께 검색되는 편이라면, 레퍼런스를 참조하기 위해 여러 쿼리를 실행하게 된다. 해결 방안은 address 데이터를 patron 데이터에 임베드 하는 것이다. 임베디드 데이터 모델은 애플리케이션에서 완전한 patron 정보를 가져오기 위해 한 번의 쿼리만 사용할 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    _id: "joe",
    name: "Joe Bookreader",
    address: {
    street: "123 Fake Street",
    city: "Faketon",
    state: "MA",
    zip: "12345"
    }
    }
  2. Subset Pattern
    Embedded Document Pattern은 애플리케이션이 사용할 때 별로 필요 없는 정보를 포함한 큰 다큐멘트를 만들 수 있다는 문제가 있다. 이런 불필요한 데이터는 추가적인 로드를 야기하고, 당연히 읽기 퍼포먼스를 떨어뜨릴 수 있다. 대신, 자주 접근하는 정보를 모아 Subset 패턴틀 사용할 수 있다. 영화 정보를 보여주는 애플리케이션을 생각해보자. 데이터베이스는 아래와 같은 movie 스키마를 가지고 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    {
    _id: 1,
    title: "The Arrival of a Train",
    year: 1896,
    runtime: 1,
    released: ISODate("01-25-1896"),
    poster: "http://ia.media-imdb.com/images/M/MV5BMjEyNDk5MDYzOV5BMl5BanBnXkFtZTgwNjIxMTEwMzE@._V1_SX300.jpg",
    plot: "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, ...",
    fullplot: "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, the line dissolves. The doors of the railway-cars open, and people on the platform help passengers to get off.",
    lastupdated: ISODate("2015-08-15T10:06:53"),
    type: "movie",
    directors: [ "Auguste Lumière", "Louis Lumière" ],
    imdb: {
    rating: 7.3,
    votes: 5043,
    id: 12
    },
    countries: [ "France" ],
    genres: [ "Documentary", "Short" ],
    tomatoes: {
    viewer: {
    rating: 3.7,
    numReviews: 59
    },
    lastUpdated: ISODate("2020-01-09T00:02:53")
    }
    }

    movie 콜렉션은 애플리케이션이 간단한 영화의 Overview를 보여줄 때 사용하지 않는 정보들을 가지고 있다. 예를 들어서, fullPlot, rating 관련된 정보들이 그렇다고 가정하자. 이 경우, 모든 영화 데이터를 하나의 콜렉션에 저장하기 보다는 두 콜렉션으로 나눌 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 영화 기본 정보를 담고 있는 movie 콜렉션. 애플리케이션이 기본적으로 불러오게 되는 데이터
    {
    _id: 1,
    title: "The Arrival of a Train",
    year: 1896,
    runtime: 1,
    released: ISODate("1896-01-25"),
    type: "movie",
    directors: [ "Auguste Lumière", "Louis Lumière" ],
    countries: [ "France" ],
    genres: [ "Documentary", "Short" ],
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // movie_detail 콜렉션은 부가적이고, 덜 접근이 되는 정보를 담는다.
    {
    _id: 156,
    movie_id: 1, // reference to the movie collection
    poster: "http://ia.media-imdb.com/images/M/MV5BMjEyNDk5MDYzOV5BMl5BanBnXkFtZTgwNjIxMTEwMzE@._V1_SX300.jpg",
    plot: "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, ...",
    fullplot: "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, the line dissolves. The doors of the railway-cars open, and people on the platform help passengers to get off.",
    lastupdated: ISODate("2015-08-15T10:06:53"),
    imdb: {
    rating: 7.3,
    votes: 5043,
    id: 12
    },
    tomatoes: {
    viewer: {
    rating: 3.7,
    numReviews: 59
    },
    lastUpdated: ISODate("2020-01-29T00:02:53")
    }
    }

    이 방법은 더 적은 데이터를 일반적인 요청 상황에서 불러오기 때문에, 읽기 퍼포먼스를 향상시킨다. 애플리케이션에서 필요하다면, movie collection의 데이터를 불러올 수 있게 구성한다.

    이 방식에서는 Trade-off가 존재한다. 자주 접근되는 데이터를 담은 더 작은 다큐멘트들은 Working set의 전체적인 크기를 줄여주고, 읽기 퍼포먼스 향상, 그리고 애플리케이션 메모리를 절약해준다. 하지만, 애플리케이션이 데이터를 읽어오는 방식에 대한 이해도가 필요하다. 만약 데이터를 여러 콜렉션으로 부적절하게 나눈다면, 애플리케이션은 필요한 데이터를 가져오기 위해 JOIN을 수행하게 될 수 있다. 게다가, 데이터를 많은 조각으로 나누면, 어떤 데이터가 어떤 콜렉션에 저장되어 있는지 추적하기 어려워지기 때문에 필요한 데이터베이스 유지보수가 증가할 수 있다.

엔티티 사이에 1:N 관계가 있고, N이 1 부분에 항상 보여야 할 때

이 경우에도 위와 같이, Embedded Document Pattern과, Subset Pattern이 있다. 구현 방식 역시 유사하다.

  1. Embedded Document Pattern
    아래 예시에서는 고객과 여러 주소 관계를 표현하고 있다. 많은 데이터 엔티티들을 다른 하나의 컨텍스트 안에서 봐야 할 때, 임베딩 하는 것이 레퍼런스 하는 경우보다 나은 경우를 설명한다 (안 그런 경우도 있다). patronaddress 사이의 1:N 관계에서, patron은 복수의 address를 가지고 있다. 정규화 되어있는 경우, address 다큐멘트는 patron 다큐멘트를 참조하고 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 정규화 된 데이터
    {
    _id: "joe",
    name: "Joe Bookreader"
    }

    // address documents
    {
    patron_id: "joe", // reference to patron document
    street: "123 Fake Street",
    city: "Faketon",
    state: "MA",
    zip: "12345"
    }

    {
    patron_id: "joe",
    street: "1 Some Other Street",
    city: "Boston",
    state: "MA",
    zip: "12345"
    }

    만약 애플리케이션이 address 데이터를 이름 정보와 함께 조회하는 경우가 많다면, 참조된 데이터를 가져오기 위해 여러번의 쿼리를 수행해야 한다. 더 최적화된 스키마는 address 데이터를 patron에 임베드 시키는 것이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    {
    _id: "joe",
    name: "Joe Bookreader",
    addresses: [
    {
    street: "123 Fake Street",
    city: "Faketon",
    state: "MA",
    zip: "12345"
    },
    {
    street: "1 Some Other Street",
    city: "Boston",
    state: "MA",
    zip: "12345"
    }
    ]
    }
  2. Subset Pattern
    Embbed Document Pattern의 문제는 거대한 다큐멘트를 만들 수 있다는 점이다. 이런 경우, Subset Pattern을 사용해서 오직 애플리케이션이 요구하는 데이터에만 접근하도록 수 있다. 프로덕트에 리뷰를 달 수 있는 커머스 사이트를 예로 들어보자.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    {
    _id: 1,
    name: "Super Widget",
    description: "This is the most useful item in your toolbox.",
    price: { value: NumberDecimal("119.99"), currency: "USD" },
    reviews: [
    {
    review_id: 786,
    review_author: "Kristina",
    review_text: "This is indeed an amazing widget.",
    published_date: ISODate("2019-02-18")
    },
    {
    review_id: 785,
    review_author: "Trina",
    review_text: "Nice product. Slow shipping.",
    published_date: ISODate("2019-02-17")
    },
    ...
    {
    review_id: 1,
    review_author: "Hans",
    review_text: "Meh, it's okay.",
    published_date: ISODate("2017-12-06")
    }
    ]
    }

    리뷰는 최신순으로 정렬되어있다. 프로덕트 페이지에 들어가면, 애플리케이션은 10개의 가장 최근 리뷰를 보여준다. 이 경우 모든 리뷰를 하나의 콜렉션에 담을 필요는 없다. 그 대신, 콜렉션을 두 개로 나눌 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 10개의 최신 리뷰와 함게, 각 프로덕트 정보를 저장하느 product 콜렉션
    {
    _id: 1,
    name: "Super Widget",
    description: "This is the most useful item in your toolbox.",
    price: { value: NumberDecimal("119.99"), currency: "USD" },
    reviews: [
    {
    review_id: 786,
    review_author: "Kristina",
    review_text: "This is indeed an amazing widget.",
    published_date: ISODate("2019-02-18")
    }
    ...
    {
    review_id: 776,
    review_author: "Pablo",
    review_text: "Amazing!",
    published_date: ISODate("2019-02-16")
    }
    ]
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 모든 리뷰들을 저장하는 review 콜렉션, 각 리뷰들은 product를 참조한다.
    {
    review_id: 786,
    product_id: 1,
    review_author: "Kristina",
    review_text: "This is indeed an amazing widget.",
    published_date: ISODate("2019-02-18")
    }
    {
    review_id: 785,
    product_id: 1,
    review_author: "Trina",
    review_text: "Nice product. Slow shipping.",
    published_date: ISODate("2019-02-17")
    }
    ...
    {
    review_id: 1,
    product_id: 1,
    review_author: "Hans",
    review_text: "Meh, it's okay.",
    published_date: ISODate("2017-12-06")
    }

    최근 10개의 리뷰만 product 콜랙션에 담아두는 것을 통해서, 필요한 부분만 production 콜랙션에 요청을 했을 때 받을 수 있다. 만약 유저가 추가적인 리뷰를 보고 싶어 하면, 애플리케이션은 review 콜랙션에서 이를 가져오면 된다.

    마찬가지로, 이 경우의 Subset Pattern에서도 Trade off가 존재한다. 더 자주 접근 되는 데이터만 담고 있는 작은 다큐멘트를 쓰는 것은 working set의 전체 사이즈를 줄여준다. 이러한 더 작은 다큐멘트들은 결과적으로 읽기 퍼포먼스를 향상시킨다. 하지만, Subset 패턴은 데이터 중복으로 이어진다. 예를 들어서, review는 product와 review 콜랙션 모두에게 있다. 추가적인 단계들이 각 콜랙션 사이에 동일성을 유지하기 위해 필요하다. 또한 애플리케이션에서 제한된 Subset 수를 유지하기 위한 추가적인 로직이 필요하다. (최근 리뷰가 들어오면, 마지막 리뷰를 빼주는 등)


일반적으로 임베딩 하는 것이 읽기 작업에서 더 나은 퍼포먼스를 보여준다. 임베디드 데이터 모델은 연관된 데이터를 업데이트 하는 것을 하나의 쓰기 작업으로 가능하게 만든다.

Normalized Data Model (Reference)

정규화된 데이터 모델은 관계를 reference를 통해 표현한다.

일반적으로, 정규화된 데이터 모델은 다음과 같은 상황에서 사용한다.

  • 1:N 관계에서 임베딩 하는 것이 데이터 중복을 일으키고, 충분한 읽기 퍼포먼스를 제공하지 못하는 경우
  • 더 복잡한 N:M 관계를 표현해야 할 때
  • 큰 계층적 데이터 셋을 표현해야 할 때

문서에서는 N:M에 대한 부분을 직접 언급하는 페이지는 없었고, 1:N 관계와, 트리구조라는 형태로 분류되어있다.

1:N 관계

publisherbook의 관계를 연결하는 예시를 살펴보자. 아래 예시의 경우는 publisher 정보의 반복을 피하기 위해서는 임베딩 하는 것보다 참조를 사용하는 것이 더 좋다는 내용이다. 임베딩 된 경우를 확인해보면 아래와 같다. publisher에 대한 정보가 book 다큐멘트 안에서 중복되고 있다는 것을 알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
title: "MongoDB: The Definitive Guide",
author: [ "Kristina Chodorow", "Mike Dirolf" ],
published_date: ISODate("2010-09-24"),
pages: 216,
language: "English",
publisher: {
name: "O'Reilly Media",
founded: 1980,
location: "CA"
}
}

{
title: "50 Tips and Tricks for MongoDB Developer",
author: "Kristina Chodorow",
published_date: ISODate("2011-05-06"),
pages: 68,
language: "English",
publisher: {
name: "O'Reilly Media",
founded: 1980,
location: "CA"
}
}

이런 중복을 피하기 위해서 reference를 사용한다. 참조를 사용할 때 관계들의 증가에 따라 참조를 저장할 위치를 결정한다. 위 예시 상황을 말 해보자면, publisherbook의 수가 조금만 증가 하는 경우, book 참조를 publisher 다큐멘트 안에 넣는것이 좋다. 그렇지 않다면, 즉, publisherbook의 수가 제한 없이 커진다면, 이 데이터 모델은 아래 예시처럼 변경 가능하고 증가 하는 배열 형태를 가져야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
name: "O'Reilly Media",
founded: 1980,
location: "CA",
books: [123456789, 234567890, ...]
}

{
_id: 123456789,
title: "MongoDB: The Definitive Guide",
author: [ "Kristina Chodorow", "Mike Dirolf" ],
published_date: ISODate("2010-09-24"),
pages: 216,
language: "English"
}

{
_id: 234567890,
title: "50 Tips and Tricks for MongoDB Developer",
author: "Kristina Chodorow",
published_date: ISODate("2011-05-06"),
pages: 68,
language: "English"
}

트리 구조

Parent References

Parent References 패턴은 각 트리 노드를 다큐멘트에 저장한다. 트리 노드 외에도 문서는 노드의 부모 ID를 저장한다.

위 이미지 형태의 구조를 아래와 같이 저장한다고 볼 수 있다.

1
2
3
4
5
6
7
8
db.categories.insertMany( [
{ _id: "MongoDB", parent: "Databases" },
{ _id: "dbm", parent: "Databases" },
{ _id: "Databases", parent: "Programming" },
{ _id: "Languages", parent: "Programming" },
{ _id: "Programming", parent: "Books" },
{ _id: "Books", parent: null }
] )

노드의 부모를 검색할 때는 다음과 같이 검색할 수 있다.

1
db.categories.findOne( { _id: "MongoDB" } ).parent

부모 필드를 통해 children을 검색할 수 있다.

1
db.categories.find( { parent: "Databases" } )

Child References

Child References 패턴은 각 트리 노드를 하나의 다큐멘트에 저장한다. 트리 노드 데이터에 자식 노드들의 id 배열을 저장한다.

즉 아래와 같이 위 이미지 형태의 트리 구조를 저장한다는 뜻이다.

1
2
3
4
5
6
7
8
db.categories.insertMany( [
{ _id: "MongoDB", children: [] },
{ _id: "dbm", children: [] },
{ _id: "Databases", children: [ "MongoDB", "dbm" ] },
{ _id: "Languages", children: [] },
{ _id: "Programming", children: [ "Databases", "Languages" ] },
{ _id: "Books", children: [ "Programming" ] }
] )

자식 노드들을 찾을 때는 아래와 같이 쿼리를 하게 된다.

1
db.categories.findOne( { _id: "Databases" } ).children

특정 자식 노드 값을 가지고 있는 부모 값을 찾기 위해서는 다음과 같은 쿼리를 사용한다.

1
db.categories.find( { children: "MongoDB" } )

Children References 패턴은 하위 트리에 대한 작업이 필요하지 않다면 적합한 방법이다. 이 패턴은 노드가 여러 개의 부모를 가질 수 잇는 경우에도 적합한 방법이 될 수 있다.

Array of Ancestors

Array of Ancestors 패턴은 각 트리 노드를 하나의 다큐멘트에 담는 방법이다. 트리 노드 데이터에, 노드의 조상들 또는 경로의 id 배열을 저장하는 방법이다.

위 이미지 형태를 아래와 같이 저장하는 방법이다.

1
2
3
4
5
6
7
8
db.categories.insertMany( [
{ _id: "MongoDB", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" },
{ _id: "dbm", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" },
{ _id: "Databases", ancestors: [ "Books", "Programming" ], parent: "Programming" },
{ _id: "Languages", ancestors: [ "Books", "Programming" ], parent: "Programming" },
{ _id: "Programming", ancestors: [ "Books" ], parent: "Books" },
{ _id: "Books", ancestors: [ ], parent: null }
] )

조상들 또는 경로를 찾기 위해 다음과 같이 쿼리를 사용할 수 있다.

1
db.categories.findOne( { _id: "MongoDB" } ).ancestors

자손들을 모두 찾기 위해서 다음과 같이 쿼리를 사용할 수 있다.

1
db.categories.find( { ancestors: "Programming" } )

Array of Ancestors 패턴은 빠르고 효율적으로 자손과 조상들을 찾을 수 있게 해준다.

Array of Ancestors 패턴은 Materialized Paths 패턴보다는 약간 느리지만, 좀 더 직관적으로 사용할 수 있다.

Reference

Author

changhoi

Posted on

2021-05-08

Updated on

2021-05-08

Licensed under

댓글

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×