Skip to content

너무 자유로운 구조는 초보 개발자를 불안하게 해요!

twoo1999 edited this page Dec 14, 2023 · 3 revisions

배경

말 그대로입니다…

저는 인터미션기간 동안 SQL을 쓰겠다고 생각했습니다. 그래서 시간을 내서 ERD와 정규화를 다시 공부하고 이전 프로젝트에 있던 데이터를 사용해서 연습도 했단 말이죠…

근데 팀과 상의 후 결정은 Document DB의 MongoDB로 낙찰…

자세한 이야기는 저번 정리를 [참고](https://www.notion.so/NoSQL-16c8fa4b1ff84f86a321c87aa66e1504?pvs=21)

그래서 정규화라는 강력한 무기가 없어진 저는 MongoDB 스키마를 설계하기 위한 여정을 떠납니다…

과정

주의) 아래의 내용은 MongoDB 공식문서를 번역하고 심한 의역이 들어가있습니다.

과연 MongoDB의 스키마는 어떻게 설계할까요? MongoDB를 접하는 초보 개발자는 항상 궁금할 것입니다.

그리고 답은 바로바로… 상황에 따라 다르다 입니다.(장난하나…)

document DB는 SQL 보다 데이터 관계를 표현하는 방법이 더 많습니다.

스키마를 고려하기 위해 한 번 물어나봅시다.

  • 혹시 구현하고자 하는 애플리케이션의 읽기/쓰기 양이 굉장히 많나요?
  • 어떤 데이터를 빈번하게 접근하나요?
  • 성능 고려 사항은 무엇인가요?
  • 데이터 집합이 어떻게 성장하고 확장될 것인가요?

실제 세계의 사례와 함께 데이터 모델링의 기본을 토의해봅시다.

적절한 MongoDB 스키마 디자인이 확장 가능하고 빠르며, 저렴한 데이터베이스를 배포하는데 가장 중요한 부분인지 알고 계셨나요?(네…)

정말입니다.(알고 있다니까…) 그리고 스키마 디자인은 종종 MongoDB 관리에서 가장 관과되는 부분 중 하나입니다.

왜 MongoDB 디자인이 중요할까요? 여기서는 좋은 예시가 있는데, 제 경험 상 MongoDB를 접하는 사람 대부분이 MongoDB 스키마 디자인이 기존의 관계 데이터베이스 스키마 디자인하고 똑같다고 생각한다는 것 입니다. 그리고선 MongoDB가 제공하는 모든 장점을 최대한 활용하지 못하게 만드는 원인이죠. 먼저, 기존의 관계형 데이터베이스 디자인과 MongoDB 스키마 디자인을 비교해보겠습니다.

Schema Design Approaches – Relational vs. MongoDB

솔직히 MongoDB와 SQL스키마를 똑같이 설계하고 싶습니다.

데이터를 이전에 내가 그랬던 것 처럼 정돈되고 작은 테이블로 나누고 싶다는 생각이 드는 것은 아주 정상적입니다. 그러나 MongoDB를 처음 배우면서 이제까지 놓쳤던 MongoDB의 장점을 놓쳤다는 것을 알았습니다.

MongoDB와 기존의 관계 DB 스키마 디자인을 비교하는 건 좋은 일입니다.. 왜냐하면 MongoDB를 접하는 많은 사람들이 관계 DB로부터 넘어오기 때문이죠. 자 이제 두 디자인 패턴의 차이를 알아봅시다.

Relational Schema Design

관계형 DB를 디자인할 때 개발자들은 일반적으로 쿼리와는 독립적으로 스키마를 모델링합니다. 우선 어떤 데이터를 가져야할까를 고민하고 이를 통해 3번의 정규화를 거칩니다. 즉 데이터를 쪼개고 데이터의 중복을 없애는 것이 목적이죠. 실제 사례를 통해 봅시다.

Untitled (5)

여기선 사용자의 데이터가 다른 테이블로 분리되고, Professions와 Cars의 user_id라는 외래 키를 사용해서 서로 JOIN할 수 있다는 것을 볼 수 있습니다. 이런 경우에서 MongoDB는 어떻게 모델링을 할 수 있을까요?

MongoDB Schema Design

MongoDB 스키마 디자인은 관계 DB 스키마 디자인과 매우 다릅니다.

MongoDB 디자인에는 다음과 같은 특징이 있는데…

공식적인 프로세스, 알고리즘 그리고 규칙이… 없다!

MongDB 스키마를 디자인할 때 유일하게 중요하게 생각하는것은 애플리케이션에잘 맞는 스키마를 디자인하는 것입니다. 동일한 데이터를 사용하지만 쓰임새는 다른 두 개의 애플리케이션이 있다면 스키마도 매우 다를 수 있다는 것이죠. 스키마를 디자인할 때 고려해야하는 부분은 다음과 같습니다.

  • 데이터 저장
  • 좋은 쿼리 성능 제공
  • 합리적인 양의 하드웨어 필요

위의 데이터를 MongoDB처럼 디자인한다면 아마 이렇게 되겠네요.

{
    "first_name": "Paul",
    "surname": "Miller",
    "cell": "447557505611",
    "city": "London",
    "location": [45.123, 47.232],
    "profession": ["banking", "finance", "trader"],
    "cars": [
        {
            "model": "Bentley",
            "year": 1973
        },
        {
            "model": "Rolls Royce",
            "year": 1965
        }
    ]
}

데이터를 별도의 컬렉션이나 document로 분활하는 대신, MongoDB에서는 document 기반 디자인을 활용해 데이터를 유저 객체 내에 배열과 객체를 포함시킬 수 있습니다. 이제 애플리케이션을 위해 모든 데이터를 한 번에 불러올 수 있는 간단한 쿼리를 실행할 수 있습니다.

Embedding vs. Referencing

MongoDB에서 디자인은 두 가지 선택지가 있습니다. 데이터를 직접 포함시키든가, 아니면 $lookup 연산자를 통해 참조하든가(JOIN과 유사). 각 옵션의 장단점을 살펴봅시다.

Embedding

장점

  • 모든 정보를 단일 쿼리로 검색할 수 있다.
  • JOIN 혹은 $lookup을 사용하지 않을 수 있다.
  • 관련 정보를 원자적인 작업으로 업데이트할 수 있다.
    • 기본적으로 단일 document에 대한 모든 CRUD 작업은 ACID규칙을 준수합니다.
  • 여러 작업에 의해 트랜잭션이 필요한 경우 트랜잭션 연산자를 사용할 수 있습니다.
  • 트랜잭션은 MongoDB 4.0부터 사용 가능하지만, 너무 의존하는 것은 좋지 않을 수 있다.
    • 필요없는 오버헤드를 줄 수 있고 MongoDB의 장점 중 하나인 스케일 아웃 및 유연성을 제한할 수 있다.

단점

  • 큰 데이터에서 대부분의 필드가 관련이 없다면 더 많은 오버헤드를 초래할 수 있다.
  • 각 쿼리에 대해 보내는 데이터의 크기를 조절한다면 쿼리 성능을 높일 수 있다.
  • MongoDB는 단일 document에 대해 16-MB의 제한이 있다.
  • 하나의 document에 너무 많은 데이터를 포함시키면 이 제한에 걸리는 경우도 있다.

Referencing

또 다른 방법은 document의 고유한 ID를 사용해서 다른 document를 참고하고 $lookup 연산자를 사용해서 참조하고 연결하는 방법이 있습니다. 참조는 SQL의 JOIN과 유사하게 작동합니다. 이를 통해 데이터를 효율적으로 분활하여 확장 가능한 쿼리를 수행하게 하고 동시에 데이터 간의 관계를 유지할 수 있습니다.(굳이 관계를 포기할 필요가 없다…!?!?)

장점

  • 데이터를 분활하여 더 작은 document를 가질 수 있다. 즉 16-MB 제한에 걸리지 않을 수 있다.
  • 자주 사용하는 데이터만 접근할 수 있다.
  • 데이터의 중복을 줄일 수 있다.
    • 주의) 데이터의 중복이 더 좋은 디자인을 만들 수 있다면 적극 사용할 것!

단점

  • document들의 모든 데이터를 조회하기 위해선 $lookup(JOIN)연산이 필요…

Type of Relationships

좋습니다. MongoDB를 디자인하는 방법을 알아봤으니 SQL을 사용하던 사람들이 친숙할 모델링 관계를 살표봅시다. 먼저 실제 예제를 통해 알아봅시다.(여기서는 매우 기초만 다룸.)

또한 밑에서 설명하는 데이터와 완벽하게 똑같은 데이터를 사용하는 경우에도 시스템에 따라 스키마가 완전히 달라질 수 있다는 것을 알아둡시다. 만약 당신의 스키마에 대해 대화를 하고 싶다면 커뮤니티를 통해 대화해봅시다.

One-to-One

유저 document를 살펴봅시다. 예제에서는 한 명의 유저는 하나의 이름만을 가질 수 있습니다. 따라서 일대일 관계입니다. 이런 경우는 key value로 모델링할 수 있습니다.

jsx
{
    "_id": "ObjectId('AAA')",
    "name": "Joe Karlsson",
    "company": "MongoDB",
    "twitter": "@JoeKarlsson1",
    "twitch": "joe_karlsson",
    "tiktok": "joekarlsson",
    "website": "joekarlsson.com"
}

  • document 내에서 key-value를 포함시키는 것이 좋다.
  • 예를 들어 직원은 한 부서에서만 근무할 수 있다.

One-to-Few

이제는 유저에 관련된 데이터가 적은 양이 있을 때 입니다. 예를 들어 한 명의 유저는 여러 주소를 저장할 수 있는데 그 양이 매우 적습니다. 이 관계를 일대소 관계라고 합니다.

{
    "_id": "ObjectId('AAA')",
    "name": "Joe Karlsson",
    "company": "MongoDB",
    "twitter": "@JoeKarlsson1",
    "twitch": "joe_karlsson",
    "tiktok": "joekarlsson",
    "website": "joekarlsson.com",
    "addresses": [
        { "street": "123 Sesame St", "city": "Anytown", "cc": "USA" },  
        { "street": "123 Avenue Q",  "city": "New York", "cc": "USA" }
    ]
}

MongoDB에서는 아무런 규칙도 없다고 했는데 사실 거짓말입니당ㅎㅎ. 몇 가지 유용한 규칙을 두면 디자인하는 데 훨씬 수월할 것입니다.

💡 Rule 1 : 그럴듯한 이유가 없다면 그냥 embedding 방식을 사용하자

너무 크거나, 따로 접근하거나 그런 경우가 아니라면 즉 별 다른 일이 없다면 일반적으로 document 내에서는 embedding 방식을 사용하는게 좋습니다.

  • 일대소 관계에서는 embedding 방식이 좋다.

One-to-Many

이제는 온라인 쇼핑몰 시스템을 가정합시다. 이젠 각 제품을 구성하는 다양한 부품에 대해서 저장해야합니다. 그래야 수리가 가능하니까요. 자 그럼 어떻게 디자인할까요? 하나의 제품은 여러개의 부품으로 이루어져있으니 일대다 구조를 고려합니다.

사실 상품 목록을 표시하는 화면에서는 세부 부품에 대한 정보가 필요 없습니다. 하지만 그렇다고 없어도 되는 것은 아니죠. 따라서 스키마는 항상 유지되어야 합니다. 따라서 제품 컬렉션을 가지고 각 제품에 대응하는 세부 부품 정보를 가지고 있는 document에 연결할 수 있는 Object ID 배열을 가질 수 있습니다. 이러한 컬렉션은 필요에 의해서 같은 컬렉션에 저장될 수도, 다른 컬렉션에 저장될 수도 있습니다.

// Products
{
    "name": "left-handed smoke shifter",
    "manufacturer": "Acme Corp",
    "catalog_number": "1234",
    "parts": ["ObjectID('AAAA')", "ObjectID('BBBB')", "ObjectID('CCCC')"]
}
// Parts
{
    "_id" : "ObjectID('AAAA')",
    "partno" : "123-aff-456",
    "name" : "#4 grommet",
    "qty": "94",
    "cost": "0.94",
    "price":" 3.99"
}

💡 Rule 2 : 객체에 접근해야하는 필요성은 embedding하지 않는 것에 대한 강력한 이유입니다.

💡 Rule 3 : 되도록 이면 $lookup(JOIN)하지 않도록 합니다. 하지만 더 나은 디자인을 위해서라면 용감하게 질러봅시다…

One-to-Squillions

만약 엄~~~청 많은 하위 document가 있는 스키마가 있다면 그것은 일대수십억(?)스키마입니다.

뭔가 한국어로 하니까 되게 이상하네요… 일단 그렇다고 합니다.

서버 로깅 애플리케이션을 만든다고 합시다. 로깅을 얼마나 상세하게 할 지, 그리고 로그를 얼마 동안 저장할지에 따라 각 서버는 엄~~~청 많은 양의 데이터를 저장하는 경우도 있습니다.

MongoDB에서는 제한되지 않은 배열 내의 데이터를 추적하는 것은 위험할 수 있으며, 16-MB document 제한에 걸릴 수 있습니다. 어떤 호스트라도 배열에 ObjectID만 저장되어있지만 16-MB를 초과할 메세지를 만들 수 있습니다. 따라서 하드 제한에 걸리지 않고 이 관계를 유지할 수 있을지에 대해 생각해봅시다.

그래서 호스트와 로그 메세지 간의 관계를 추적하는 것 대신, 로그 메세지가 자신에게 연결된 호스트를 저장하는 것으로 해봅시다. 로그에 호스트 정보를 저장하면서 무제한 배열이 영향을 미치는 것에 대해 예방할 수 있습니다. 즉 주객전도(?)입니다. 내가 주인이고 노비가 엄~~~청 많이 있는데 엄~~~청 큰 노비 대장을 관리하는 것 보단 노비에 홍길동 이라고 이름표를 붙여 놓으면 노비 대장을 관리할 필요도 없고 누가 봐도 내 노비라는 것이 확인되는 것이죠. 그럼 어떻게 구성되는지 확인해봅시다.

// Hosts
{
    "_id": ObjectID("AAAB"),
    "name": "goofy.example.com",
    "ipaddr": "127.66.66.66"
}
// Log Message
{
    "time": ISODate("2014-03-28T09:42:41.382Z"),
    "message": "cpu is on fire!",
    "host": ObjectID("AAAB")
}

💡 Rule 4: 배열은 무작정 커지면 안 됩니다. 다수에 수백 개 이상의 document가 있는 경우 embedding 하면 안 되고, 다수 쪽에 수천 개 이상의 document가 있는 경우에는 ObjectID를 활용한 저장 방식도 좋지만은 않습니다. 많이 중복되지 않는(High cardinality) 배열은 embedding 방식을 피할 좋은 이유입니다.

Many-to-Many

드디어 마지막 패턴입니다.(번역하느라 힘드네요. 번역기 최대한 안 쓰고 했습니다. 지금보니까 약간 미련한 것 같기도 하고…). 이 패턴은 관계형 DB에서 많이 볼 수 있는 형태입니다. 할 일 애플리케이션을 만든다고 가정합시다. 한 명의 유저는 여러 작업을 가질 수 있고, 하나의 작업은 여러 사용자에게 할당될 수 있습니다.

제가 좋아하는 예를 들어 말하면 책에는 공동저자라는 개념이 있습니다. 하나의 책을 여러 명이 집필하는 것입니다. 즉 한 명의 저자는 여러 책을 쓸 수 있고(혼자 집필할 수도 있고, 여러 명이 집필할 수 있고), 하나의 책은 여러명이 참가해서 집필할 수 있습니다.

즉 서로를 여러 개 참조를 해야하는 상황이 발생합니다.

// Users
{
    "_id": ObjectID("AAF1"),
    "name": "Kate Monster",
    "tasks": [ObjectID("ADF9"), ObjectID("AE02"), ObjectID("AE73")]
}
// Tasks
{
    "_id": ObjectID("ADF9"),
    "description": "Write blog post about MongoDB schema design",
    "due_date": ISODate("2014-04-01"),
    "owners": [ObjectID("AAF1"), ObjectID("BB3G")]
}

여기서 볼 수 있듯이 각 유저는 연결된 작업의 하위 리스트를 가지고 있고, 각 작업은 본 작업을 가지는 여러 유저의 하위 리스트를 가지고 있는 구조가 됩니다.

Summary

길고 길었습니다… 정규화를 넘어서 데이터를 표현하는 방법은 매우 많습니다.

embedding 방식도 있고, 참조 방식도 있고… 이런 방식을 사용해서 각자의 애플리케이션에 딱 맞는 데이터베이스 쿼리를 수행할 수 있으면 좋겠습니다. 주의할 점은 지금의 내용은 매우 기초적인 부분만을 다뤘습니다.(제발…) 그러니 더 깊게 빠지고 싶다? 그렇다면 시리즈를 정독하십쇼

마무리 합니다.

💡 Rule 5 : MongoDB를 사용하는 방법은 전적으로 사용하는 애플리케이션에 따라 달라집니다. 데이터를 어떻게 접근하는지가 중요한 것이죠. 그에 맞게 구조화해야합니다.

기억합시다. 애플리케이션은 서로 다 다릅니다. 그렇기 때문에 스키마 디자인은 애플리케이션의 니즈를 충족해야합니다. 이 글을 설계의 시작점으로 잡고 목표를 달성할 수 있는 방법을 고민합시다.

찐 정리

💡

  1. 일대일 : key-value를 document안에 넣자
  2. 일대소 : embedding 방식을 사용하자
  3. 일대다 : embedding 방식을 사용하자
  4. 일대수천억(?) : 참조 방식을 사용하자
  5. 다대다 : 참조 방식을 사용하자

💡 Rule 1: 그럴듯한 이유가없으면 그냥 embedding 방식 사용하자 Rule 2: 객체로의 접근이 필요한 경우는 embedding을 사용하지 말자 Rule 3: 웬만하면 $lookup(JOIN)은 사용하지 말자… 근데 필요하다면 과감하게 질러보자 Rule 4: 수백 만개의 document가 있다면 embedding 하지말고, 수천 개의 document가 있다면 ObjectID 배열은 사용하지 말자. Rule 5: 애바애(애플리케이션 by 애플리케이션)이므로 정답은 없다. 애플리케이션의 요구에 따라 적절하게 스키마를 모델링하자

다시 말하지만 이건 너무 기초입니다.

더 자세한 부분을 원한다면 이 🔗[링크](https://www.mongodb.com/developer/products/mongodb/mongodb-schema-design-best-practices/)로 들어가면 공식 문서가 나오는데 맨 밑 부분에 또 링크타고 들어가는 부분을 안내해줍니다. 지옥으로 가는 길이죠. 참고할지 말지는… 미래의 저에게 맡기겠습니다.

후기

결국 중요한 것은 왜 사용하는 지를 아는 것 같습니다. 더 나아가 왜 이런 구조를 설계했는지를 설명해야 하는 것이겠죠.

사실 관계형 DB처럼 데이터를 설계하는 방법이 어느 정도 잡혀있는 것이라면 차라리 명분이라도 말할 수 있습니다. 설계하는 이유는 찾아보면 널렸으니까요. 하지만 너무 자유로운 형태의 DB는 오히려 초보 개발자에게는 불안함으로 다가옵니다. 왜 그렇게 디자인했는지 설명할 수 있어야 하는데 자신도 이게 최선인지 의문이 생기는 것이죠. 더 잘할 수 있을 것 같은데, 이게 과연 최선일까?, 이렇게 하면 너무 불합리한 구조가 아닐까? 라는 생각이 머리에서 떠나질 않지만 할 수 있는 것이라고는 시간에 맞춰 개발하기 위해 잠시 고민을 접어두는 것 뿐입니다. 그래서 이번 프로젝트에서는 데이터베이스에 대한 고민을 최우선의 과제로 가져가고 싶습니다. 물론 기간 안에 완성하는 것이 더 중요하겠지만요.

🗺️ MusicSpot

BE
iOS

🧘‍♂️ 데일리 스크럼

👀 주간 회고

📝 1주차 주간 회고
📝 2주차 주간 회고
📝 3주차 주간 회고
📝 4주차 주간 회고
📝 5주차 주간 회고
📝 6주차 주간 회고
Clone this wiki locally