::: IT인터넷 :::

MongoDB의 Document 구조와 관계 패턴

곰탱이푸우 2022. 7. 28. 08:20
MongoDB의 Document의 구조와 관계 패턴을 정리한다.
 
MongoDB의 특징과 주요 개념은 아래 포스팅을 참고한다.
본 포스팅은  MongoDB 공식 문서와 아래 문서를 참고하여 작성했다.
정말 깔끔하게 정리가 잘 되어 있으므로 해당 포스팅을 읽어볼 것을 권한다.
MongoDB의 공식 문서는 아래 사이트를 참고한다.

Document 구조

Collection은 RDBMS의 Table이고 Document는 RDBMS의 Row를 의미한다.
Document는 표현이 자유로운 JSON 포맷을 사용하기 때문에 구조에 따라 세 가지 방법으로 구분된다.
 
자세한 내용은 아래 페이지를 참고한다.

플레인 방식

나머지 두 방식이 서로 다른 Collection의 Document를 참조하기 위한 것이다.
 
먼저 아래 내용을 이해하기 위해선 심플한 데이터 형태를 정의해야 한다.
공식적인 명칭은 아니지만 개념 설명을 위해 사용한다.
 
아주 심플한 데이터는  아래와 같이 정의 될 수 있다.
{
    _id: <ObjectId1>,
    username: "123xyz",
    contact_phone: "123-456-7890",
    contact_email: "xyz@example.com",
    access_level: 5,
    access_group: "dev"
}

 

필드 구조가 동일한 계층에 존재하는 평평한 구조이다.
이러한 구조는 여러 항목의 데이터를 하나의 Document에 표현한 것이다.
 
필드 개수가 적고 데이터가 단순한 경우 효율적이다.
그러나 필드 개수가 너무 많아지면 특정 필드를 찾거나 해당 데이터의 성격을 정의하는 것이 어렵다.
특히 성격이 다른 여러 필드가 섞여 있으므로 해당 데이터에 대한 파악이 어려워진다.
 
MongoDB는 메모리 의존적이기 때문에 Document가 커질수록 메모리 사용 면에서 굉장히 비효율적이다.
Document는 필연적으로 데이터의 성격과 사용 목적에 따라 Collection이나 Document가 적절하게 분할 되어야 한다.
 
MongoDB는 Schemaless이기 때문에 굉장히 유연하므로, 아래 정리 항목을 참고해서 적절하게 진행한다.
 
 

임베디드 방식

동일한 성격을 갖는 필드들을 묶어 Document-in-Document 형태로 구성한 것이다.
좀 더 쉽게 표현하면 JSON 특징을 이용하여 필드들을 계층 구조로 구성할 수 있다.
 
예를 들면 다음과 같다.
 
contact_phone과 contact_email 필드는 연락 정보라는 공통점이 있으므로 contact 필드로 묶을 수 있다.
contact 필드의 Value (값) 부분을 보면 중괄호 ({}) 사이에 필드와 값으로 구성된 Document 형태인 것을 알 수 있다.
기존 데이터를 계층적으로 표현할 때도 사용하는 방식이다.
 
또한 다른 Collection이나 Document의 일부 필드를 가져와서 구성할 수도 있다.
Join 연산을 지원하지 않지만 이러한 방법으로 Join과 비슷한 표현을 할 수 있다.
 
데이터 표현이 직관적이기 때문에 쿼리가 단순해지며 조회 속도가 빠르다는 장점이 있다.
그러나 데이터 불일치 문제가 발생할 수 있고, Document 크기가 커지므로 디스크 IO에 대한 성능 저하도 고려해야 한다.
 
따라서 데이터를 저장한 이후 주로 조회하는 용도로 사용할 때 권장된다.
데이터의 변경은 없거나 최소화하는 것이 권장된다.
 

레퍼런스 방식

Document의 고유 식별자를 다른 Document의 참조키로 지정하여 연결 관계를 맺어주는 방식이다.
RDBMS의 테이블간 연결 관계와 유사하다.
 
데이터를 합쳐야 하는 경우 해당 Document들을 조회해서 응용프로그램에서 Join 하면 된다.
 
가급적 데이터의 관리와 직관성을 위해 별도의 Collection으로 분리하는 것을 권장한다.
데이터의 불일치가 발생하지 않는 정규화 모델이며, 연결 관계로 표현하기 때문에 Document의 크기 증가가 적다.
요구 사항 변경으로 인한 필드 (스키마)에 변경이 발생해도 유연한 대처가 가능하다.
 
그러나 연결 관계가 복잡하거나 대규모로 조회하는 경우 여러 단계에 나눠서 조회하므로 성능 저하가 있을 수 있다.
또한 참조 관계에 대한 관리가 미흡할 경우 데이터의 정합성 부분에서 문제가 발생할 수 있다.
 
따라서 데이터의 무결성이 중요하거나, 복잡하고 계층 구조를 가지는 데이터를 다룰 때 사용한다.
데이터의 변경이 빈번하게 발생하는 경우에도 활용 가능하다.
 
 

정리

사실 MongoDB는 JSON 포맷을 사용하는 Schemaless를 지향하므로 굉장히 유연하다.
데이터를 분할하는 과정에서 데이터의 필드에 따라 단일 Collection을 사용할지, 별도의 Collection을 분할할지 강제하지 않는다.
 
단일 Collection에 필드가 제각각인 데이터를 넣어도 된다.
이 부분은 응용 프로그램 또는 서비스의 데이터 접근 패턴에 달려 있다.
어떻게 읽어서, 어떻게 보여주고, 어떻게 갱신할 것인지에 따라 최적의 방법을 선택한다.
 
일반적으로 컬렉션의 분리는 다음 경우에 권장된다.
  • 단일 Collection에 쓰기 작업이 많은 경우
  • 단일 Collection에서 대량의 데이터를 정기적으로 삭제하는 경우
  • 단일 Collection에 저장되는 Document들의 액세스 패턴이 다양한 경우
  • 자주 조회되거나 Document가 적은 Collection인 경우 (메모리에 캐시 가능)

 

해당 내용은 아래 문서를 참고한다.
유연하다는 것은 정해진 규칙이나 원칙이 있는 건 아니라는 것을 의미한다.
아래 문서를 참고하면 경험에 따른 Best Practice를 제공한다.
요약하면 다음과 같다.
  1. 불가피한 문제가 없다면 Document에 포함해라. (Embedded 방식)
  2. 특정 Document에 직접 접근할 필요가 있다면 분리해라.
  3. 배열이 지나치게 커져서는 안된다. 데이터가 크다면 one-to-many로 나눠라. 배열의 밀도가 높다면 분리해라.
  4. 애플리케이션에서 Join 하는 것을 두려워하지 말아라. 대신 index 지정과 데이터 모델링이 중요하다.
  5. 비정규화는 Join 하는 비용(읽기)이 분산 된 데이터를 찾고 갱신하는 비용(쓰기)보다 비싸면 고려해라.
  6. MongoDB의 데이터 모델링은 애플리케이션의 데이터 접근 패턴이 결정한다.
 
 

Document 관계 패턴

MongoDB의 Collection과 Document간의 관계를 설정하는 방법에 대해 정리한다.
 
Collection은 RDBMS의 Table이고 Document는 RDBMS의 Row를 의미한다.
MongoDB도 RDBMS처럼 1:1, 1:N, N:M 관계를 제공한다.
 
우선 RDBMS에서 정의하는 개념을 이해할 필요가 있다.
해당 내용은 아래 포스팅을 참고한다.

1:1 패턴

특정 Document와 상대 Document가 반드시 하나의 관계를 가지는 것을 의미한다.
 
후원자와 후원자 주소에 대한 데이터를 가정해보자.
// patron document
{
  _id: "joe",
  name: "Joe Bookreader"
}

// address document
{
  patron_id: "joe", // reference to patron document
  street: "123 Fake Street",
  city: "Faketon",
  state: "MA",
  zip: "12345"
}
 
patron (후원자)와 address (주소) Document는 서로 1:1 관계를 맺고 있다.
단순한 1:1 관계의 경우 앞에서 다룬 임베디드 방식을 사용하여 단일 Document로 표현하는 것이 더 나을 수 있다.
{
  _id: "joe",
  name: "Joe Bookreader",
  address: {
              street: "123 Fake Street",
              city: "Faketon",
              state: "MA",
              zip: "12345"
            }
}
 
1:1 패턴을 통합하다 보면 불필요한 필드가 포함되어 큰 Document가 되는 단점이 발생한다.
이러한 경우 필드의 성격과 사용 목적에 따라 Document를 구분할 필요가 있다.
서브셋(Subset) 패턴이라고 하며, 앞에서 다룬 레퍼런스 방식을 참고하여 Document를 분리한다.
 
자세한 사항은 아래 문서를 참고한다.
 

1:N 패턴

특정 Document가 연결 된 여러 Document들을 가질 수 있는 것을 의미한다.
현실 세계에서 가장 많은 패턴이며, 실제 DB 설계에서도 자주 사용된다.
 
임베디드 방식과 레퍼런스 방식을 사용할 수 있으며, 결정 기준은 다음과 같다.
  • 임베디드 방식 - 부모 Document만 사용하는 경우 (조회 성능 향상)
  • 레퍼런스 방식 - 단독 또는 다른 Document도 사용하는 경우 (읽기 성능 저하)
필요에 맞는 방식을 선택해서 사용하면 된다.

 

임베디드 방식
특정 필드에서 발생 가능한 여러 경우의 데이터를 Document에 모두 포함하는 형태이다.
 
1:1 패턴에서 다룬 후원자와 주소 Document 예제를 다시 보면 다음과 같다.
후원자의 주소 정보는 집, 직장 등 여러 개일 수 있다.
// patron document
{
  _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"
}

 

아래 처럼 배열 (대괄호 [] 사용) 형태를 사용하여 하나의 필드에 넣을 수 있다.
{
  "_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"
                }
              ]
}

 

배열은 포함한 원소 개수가 많아지면 처리 속도가 느려질 수 있다.
또한 통합하다 보면 불필요한 필드가 포함되어 큰 Document가 되는 단점이 발생한다.
 
이러한 경우 레퍼런스 방식을 참고하여 서브셋 패턴으로 Document를 분리한다.
자세한 내용은 아래 문서를 참고한다.
레퍼런스 방식
Document의 특정 필드의 내용이 지속적으로 반복되는 경우 유용하다.
 
아래 예제와 같이 각 도서마다 동일한 출판사 정보가 반복된다면 분리하는 것이 좋다.
{
  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"
  }
}
 
아래와 같은 형태로 분리할 수 있다.
하나의 출판사는 수많은 책을 출판 할 수 있으므로 Child Refrences 보다 Parent References가 유용하다.
주로 단방향 참조로 표현하는 경우가 많다.
// publisher documents
{
  _id: "oreilly",
  name: "O'Reilly Media",
  founded: 1980,
  location: "CA"
}

// book documents
{
  _id: 123456789,
  title: "MongoDB: The Definitive Guide",
  author: [ "Kristina Chodorow", "Mike Dirolf" ],
  published_date: ISODate("2010-09-24"),
  pages: 216,
  language: "English",
  publisher_id: "oreilly"
}
{
  _id: 234567890,
  title: "50 Tips and Tricks for MongoDB Developer",
  author: "Kristina Chodorow",
  published_date: ISODate("2011-05-06"),
  pages: 68,
  language: "English",
  publisher_id: "oreilly"
}
 
자세한 사항은 아래 문서를 참고한다.
 

N:M 패턴

공식 문서에는 N:M 관계를 포함하고 있지 않지만 중요한 개념이므로 설명한다.
데이터를 설계할 때 MECE(Mutually Exclusive Collectively Exhaustive) 개념으로 분리하는 것이 도움이 된다.
 
MECE 개념은 아래 사이트를 참고한다.
그러나 현실적으로 완벽한 MECE 형태로 구성하는 것은 어렵다.
N:M 패턴도 마찬가지이다.
 
학생과 학원 관계를 예를 들면 쉽다.
학생은 여러 학원에 다닐 수 있고, 학원에 등록 된 학생은 여러 명이다.
다시 말하면 N:M 패턴은 여러 개의 1:N 패턴으로 분리할 수 있다.
 
MongoDB에서는 아래와 같이 양방향 참조로 표현할 수 있다.
// academy
{ "_id" : 1, "name" : "Ace", "desc" : "Math", "student" : [1001, 1003, 1004] }
{ "_id" : 2, "name" : "Best", "desc" : "English", "student" : [1001, 1002] }
{ "_id" : 3, "name" : "Choice", "desc" : "Science", "student" : [1001, 1002, 1003] }

// student
{ "_id" : 1001, "name" : "Kim", "age" : 15, "categoryid" : [ 1, 2, 3 ] }
{ "_id" : 1002, "name" : "Shim", "age" : 18,  "categoryid" : [ 2, 3 ] }
{ "_id" : 1003, "name" : "Lee", "age" : 17,  "categoryid" : [ 1, 3 ] }
{ "_id" : 1004, "name" : "Park", "age" : 16,  "categoryid" : [ 1 ] }
 
복잡한 관계를 표현할 수 있지만, 연결 된 Document 확인하려면 추가 조회가 필수적이다.
처리 성능과 조회 패턴에 따라 단순화 하는 것이 좋다.