- Published on
mongodb structure 에 대해서
- Authors
- Name
- 길재훈
mongodb structure
지속적으로
backend지식을 습득하기 위해 공부중이다.
현재DB를mongodb사용하며, 그에 따른API를 만들기 위해서는Document structure에 대한 지식이 필요할것 같다.
Schema-less Or not
Mongodb 는 사실 Schema 가 존재하지 않으며, Document 라는 개념으로 존재한다.
Schema은DB에서 구조설계할때, 개념을 구조적으로 만든것이다. 그러므로 한번 구조를 정해 놓으면, 이후 변경하기기 어렵다.
Document 의 특징은 매우 유연하게 구조를 만들 수 있다는것이다. 마치 Javascript 의 객체에 Property 를 추가하거나, 삭제하는등 마음대로 작성 가능하다.
이러한 특징은 장점이기도 하며, 단점이기도 하다
구조적으로 만들기 위해서는
Schema의 개념이 필요한건 사실이다.Document구조가 유연하다면, 예상치 못한Data가 삽입될 수도 있기에, 어느정도 강제하는것 역시 좋은 방법이다.이로 인해
Mongoose라는ORM이 탄생했으며,Schema를 지원해준다.사실 무엇이 맞고 아니라고 생각하기는 어려운 것 같다.
> use shop
> db.products.insertOne({ name: "A book", price: 12000})
> db.products.insertOne({ title: "Keyboard", price: 50000, available: true})
위처럼 porducts collection 에 document 삽입시 정해진 형태없이, 원하는 값을 넣을 수 있다.
위처럼 작성된다면, 혼돈의 카오스로 입장하는 지름길이다. 정해진 틀에서, 추가적인 field 가 작성되도록 하는 형태가 중요한듯 싶다.
> use shop
> db.products.insertOne({ name: "A book", price: 12000})
> db.products.insertOne({ name: "Keyboard", price: 50000, "details": { slider: "kail red" }})
이렇게, name 과 price 는 공통이며, details 만 추가된 형태로 작성된다.
만약 Scehma 로 작성되었다고 가정하고 만들어보면 이러한 형태를 띌것이다.
> use shop
> db.products.insertOne({ name: "A book", price: 12000, details: null})
> db.products.insertOne({ name: "Keyboard", price: 50000, details: { slider: "kail red" }})
상단에는 details 가 존재하지만, 없기때문에 null 값을 준다.
정해진 형태라기 보다는, 유연하게 새로운 field 를 줄수 있는것이 mongodb 의 document 구조이다.
data types
data 구조를 알기 전에 Mongodb 에서 사용되는 Type 들만 살펴본다.
- Text
> db.products.insertOne({name: "MacBook"}) // Text
- Boolean
> db.products.insertOne({aviliable: true}) // Boolean
- Number
mongosh에서 저장되는 숫자의type은default로double이다.이는
javascript가64bit floating point로 작동되는것에 기인한 것으로 생각한다.
Internger (int32)
> db.products.insertOne({quantity: Int32(4)}) // Int
NumberLong (int64)
> db.products.insertOne({quantity: Long("1")}) // Long
NumberDecimal
> db.products.insertOne({price: 12.99}) // Double
> db.products.insertOne({price: Decimal128(12.99)}) // Decimal128
- ObjectId
> db.products.insertOne({_id: ObjectId("6114216907d84f5370391919")}) // ObjectId
- ISODate
> db.products.insertOne({insertedAt: new Date()}) // ISODate
- Timestampe
> db.products.insertOne({insertedAt: new Timestampe()}) // Timestampe
- Embedded Document
> db.products.insertOne({detailes: { ... } }) // Embedded Document
- Array
> db.products.insertOne({nums: [1, 2, 3]}) // Array
Mongodb 는 위의 BSON 타입을 사용하여 Document 를 구성한다.
Nested / Embedded Document VS References
Nested / Embedded document
{
_id: ObjectId("6114216907d84f537039191a")
username: "Jhon",
address: {
_id: ObjectId("6114216907d84f537039191b"),
street: "N Street",
city: "Seoule",
}
}
Refernces
{
_id: ObjectId("6114216907d84f537039191a")
username: "Jhon",
addressId: ObjectId("6114216907d84f537039191b"),
}
address: {
_id: ObjectId("6114216907d84f537039191b"),
street: "N Street",
city: "Seoule",
}
SQL 에서는 다른 Table 과 연관시키기 위해서 그 Table 을 Join 한다
Document model 은 SQL 처럼 Join 하는 구조가 아니다. Document 는 위의 Embedded document 를 사용하여, Nested 형태를 만들수도 있으며, 다른 Document의 ObjectId 를 할당하는 구조로도 사용가능하다.
ObjectId 로 참조하는 방식을 Refernces 라고 하는데, 이는 Document 를 ObjectId 로 연관시키는 방법이다.
주의할점
위의
addressId는ObjectId를 가진Document가 아닌 정말ObjectId만을 가진다.
현재 내가 알기로는 Mongodb 차체에서, 해당 ObjectId 의 Document 를 넣기 위해서는, ObjectId 를 사용하여 Query 한후, 재할당해주는 방식으로 처리하는걸로 알고 있다.
이러한 불편함을 제거하기 위해
Mongoose에서는Populate라는 기능을 제공해준다.
그럼에도 불구하고 ObjectId 를 넣는 이유는 무엇일까? 첫째로, Embedded document 는 ObjectId 보다 크기가 클수 밖에없다.
만약, Embadded document 를 가진 배열을 가진 field 가 있다고 가정하자.
{
_id: ObjectId("6114216907d84f537039191a")
username: "Jhon",
addresses: [
{
_id: ObjectId("6114216907d84f537039191b"),
street: "N Street",
city: "Seoule",
},
{
_id: ObjectId("6114216907d84f537039191c"),
street: "N Street",
city: "Busan",
},
],
}
위는 Embadded document 를 가져도 별 문제가 되지는 않는다. 배열의 수가 많지 않기 때문이다.
하지만, document 수가 기하급수적으로 많은 상황이라면 어떻게 될까? mongodb 에서 document 의 사이즈는 default 로 16MB 인것으로 알고 있다.
많은 docuemnt들을 전부 담았을시 16MB 를 초과하게 되면 더이상 해당 document 사용이 불가능한것이다.
그럴때는 ObjectId 를 담는것이 현명하다.
ObjectId를 담은addresses배열
{
_id: ObjectId("6114216907d84f537039191a")
username: "Jhon",
addresses: [
_id: ObjectId("6114216907d84f537039191c"),
_id: ObjectId("6114216907d84f537039191d"),
_id: ObjectId("6114216907d84f537039191e"),
...
],
}
이렇게 하면, 각 Object 를 담았을때 보다 사이즈가 많이 줄어들수 있다.
바로 이러한 이유 (내가 알고있는 이유일 뿐이다...) 로 인해,
더 효율적인 관리를 할 수 있도록 ObjectId 를 담는 상황이 발생할 수 있다.
실제로, 데이터수가 많지 않으면, Embadded Document 방식으로 하는것을 추천하기도 한다.
이는 어떠한 상황인지에 따라 달라질 수 있는 부분으로, 설계시 많은 경험을 바탕으로 만들어지는 부분인것 같다.
One to One relationship
한 사람당, 하나의 직업을 가진다고 가정해보자. 그럼 1:1 relationship 이 형성된다
이를 Mongodb 에서 구현해보면 다음과 같은 구조가 된다.
Persons Collection
> db.persons.insertOne({
_id: ObjectId("6114216907d84f537039180a"),
name: "Jhon",
email: "Jhon@email.com",
job: ObjectId("6114216907d84f537039191d"),
})
Jobs Collection
> db.jobs.insertOne({
_id: ObjectId("6114216907d84f537039191d"),
name: "Programmer",
})
위는 Person 이 하나의 Job 을 가진 구조로 볼 수 있다. 위는 ObjectId 로 참조한 것이다.
만약 Person 에서 Job document 를 조회하려면 어떻게 할까? 이를 shell 에서 처리하려면 다음처러 처리 가능하다
> var jobId = db.persons.findOne({_id: "6114216907d84f537039180a"}).job
> db.jobs.findOne({_id: jobId})
{
_id: ObjectId("6114216907d84f537039191d"),
name: "Programmer",
}
이는 ObjectId 를 통해 사용한 References 방법이며, 이렇게 사용안하고 Embadded 시켜도 상관없다.
Persons Collection & Embadded Document
> db.persons.insertOne({
_id: ObjectId("6114216907d84f537039180a"),
name: "Jhon",
email: "Jhon@email.com",
job: {
_id: ObjectId("6114216907d84f537039191d"),
name: "Programmer",
}
})
위의 예시는 Embadded 시킨 예시이다. 위 같은경우(값이 많지 않은 객체 및 참조 jobs 의 종류가 많지 않은경우)는 Embadded 시키는것이 더 나은 방법일수 있지만,이는 어떻게 사용되느냐에 따라 다른 부분이기에 경험을 통해 만들어주는게 중요한듯 싶다.
위는 가상의 상황을 만든것으로 1:1 관계를 생각하고 가정한것이다.
One to Many
One to Many 의 상황은 게시판에서 볼 수 있을듯 싶다.
간단한 상황을 가정하고 생각한다면, 1개의 Post 안에 여러개의 comments 들이 있을 수 있다.
Post document
> db.posts.insertOne({
title: "Awasome!!",
author: "Jhoon",
description: "...",
comments: [
ObjectId("6114216907d84f537039195a"),
ObjectId("6114216907d84f537039195b"),
]
})
{
_id: ObjectId("6114216907d84f537039191a"),
title: "Awasome!!",
author: "Jhoon",
description: "...",
comments: [
ObjectId("6114216907d84f537039195a"),
ObjectId("6114216907d84f537039195b"),
],
}
Comments document
> db.comments.insertOne({
author: "Mark",
comment: "wow!!"
})
{
_id: ObjectId("6114216907d84f537039195a"),
author: "Mark",
comment: "wow!!"
}
> db.comments.insertOne({
author: "Jim",
comment: "powerful!!"
})
{
_id: ObjectId("6114216907d84f537039195b"),
author: "Jim",
comment: "powerful!!"
}
여기서 Comments 가 얼마나 참조될지 예상이 되지 않아서, 일단 Comments Collection 을 만들고, 해당하는 Document id 를 가져오는 방식으로 작성했다.
이 역시, Comments 의 갯수가 많지 않다면, Embadded 형태로 만들어도 상관은 없다.
만약, References 방식으로 작성한다면 City 안에 있는 Citizen 의 형태가 적절해 보인다.
City 는 1개이고, Citizen 은 수만명이기 때문이다. 이렇게 많은 수의 사람을 City 에 담아야 한다면 당연하게도 References 방식을 채택하여 구조를 만들어야만 할 것이다.
Many to Many
Many to Many relationship 은 보통 구매자 와 상품 의 관계에 비유할 수 있다.
Customer 는 여러개의 Product 를 가질 수 있고, Product 는 여러 Customer 를 가질수 있다.
Customer Document
> db.customers.insertOne({
name: "Jhoon",
})
{
_id: ObjectId("6114216907d84f537039195a"),
name: "Jhoon",
}
Product Document
> db.customers.insertOne({
title: "Keyboard"
})
{
_id: ObjectId("6114216907d84f537039198a"),
title: "Keyboard",
}
이렇게 각 Customer 와 Product Document 를 만들고,
다음의 주문에 넣어주면, Many to Many relationship 을 만들 수 있다.
db.orders.insertOne({
customerId: ObjectId("6114216907d84f537039195a"),
products: [
{
productId: ObjectId("6114216907d84f537039198a"),
quantity: 1
}
]
})
과연 이것이 옳은것일까? Mongodb 에서는 Embedded Document 가 존재한다.
굳이 Orders 를 빼서 처리할 필요없이 customer 에 oders 라는 배열을 만들고, 각 객체를 담아주는 방식으로도 구현 가능하다.
> db.customers.updateOne({_id: ObjectId("6114216907d84f537039195a"), {
$set: {
orders: [
{
productId: ObjectId("6114216907d84f537039198a"),
quantity: 1
},
... and so on
]
}
}
})
{
_id: ObjectId("6114216907d84f537039195a"),
name: "Jhoon",
orders: [
{
productId: ObjectId("6114216907d84f537039198a"),
quantity: 1
},
... and so on
]
}
이렇게 Embedded 형태로 구현할수도 있다, References 방식으로 구현할지 Embedded 방식으로 구현할지는 선택사항인듯 싶다.
여기서 추가로 덧붙혀서 이야기 하지면,
위의 로직상 Embedded 로 만드는데, productId 를 사용하여 References 할 필요성이 있는가 하는점이다.
References 방식으로 처리하는것은, Javascript 의 객체 메모리 참조와 같은 형태로 구현되는 방식이라고 생각하면 된다.
즉, ObjectId 값을 사용하여, 만들어진 Document 를 공유한다는 것이다.
Many to Many relationship 같은 경우, ObjectId 를 통한 References 방식을 사용하는것이 좋다.
서로가 서로를 지속 참조하는 형태이므로, 중간에 Product 의 정보가 변경되었을시, References 로 참조가 아닌경우에는 모든 Product 를 조회하고 값을 수정해야 할것이다.
하지만, References 를 통해 참조하므로, 간단하게 해당 Product 의 정보만 수정하면, 해당 ObjectId 를 가진 Document 는 수정된 Product 를 Query 하면 간단하게 문제가 해결되는 장점이 있다.
물론,
data변경이 전혀 없는, 확정적인data같은 경우References를 통해 만들지 않아도 될것이다.
보통 이러한 Structure 를 만들때, 일반적으로 많이 사용되는 패턴은 다음과 같다고 이야기한다.
One to One
-> EmbeddedOne to Many
-> EmbeddedOne to Many
-> References
위처럼 작성하는 것이 일반적이기는 하지만, Database 구조상 언제든지 변경도리수 있으며, 상황에 따라 변경되어야 하는 부분이므로, 절대적인 상황은 아니다.
이러한 설계는 많은 공부와 경험이 축적되어야 좋은 설계를 할 수 있을듯 싶다.