MongoDB에서의 데이터 모델링 원칙
MongoDB에서의 데이터 모델링 원칙
MongoDB는 관계형 데이터베이스와 달리 스키마가 고정되지 않은 NoSQL 데이터베이스입니다.
데이터 모델링의 방식이 다소 유연하지만, 효율적인 성능과 확장성을 위해서는 몇 가지 원칙과 패턴을 따르는 것이 중요합니다.
1. 임베디드 문서 vs 참조 관계 (Embedding vs Referencing)
MongoDB에서는 데이터를 임베디드 문서(내장 문서)로 저장하거나 참조(레퍼런스)할 수 있습니다. 두 방식 모두 장단점이 있으며, 데이터 구조와 성능 요구사항에 따라 선택해야 합니다.
임베디드 문서 (Embedded Document): 관련된 데이터를 한 문서 내에 중첩된 형태로 저장하는 방식입니다.
- 장점: 데이터를 한 번의 조회로 가져올 수 있어 성능이 향상됩니다.
- 단점: 문서의 크기가 커질 수 있으며, 중첩된 데이터가 매우 크거나 복잡할 경우 업데이트와 관리가 어려워질 수 있습니다.
예시: 사용자와 그 사용자의 주소를 함께 저장하는 경우
{ "name": "Alice", "email": "alice@example.com", "address": { "street": "123 Main St", "city": "New York", "zip": "10001" } }
참조 관계 (Referencing): 관련된 데이터를 별도의 컬렉션에 저장하고, 참조를 통해 연결하는 방식입니다.
- 장점: 데이터가 중복되지 않으며, 독립적으로 관리할 수 있습니다.
- 단점: 데이터를 조인(join)해야 하므로 추가적인 조회가 필요하여 성능이 저하될 수 있습니다.
예시: 사용자와 주소를 각각 다른 컬렉션에 저장하고, 사용자 문서에서 주소 ID를 참조하는 방식
{ "name": "Bob", "email": "bob@example.com", "address_id": ObjectId("5f47a53b9b1e4f11b10f20d3") }
ObjectId
MongoDB에서 ObjectId(“5f47a53b9b1e4f11b10f20d3”)는 문서의 고유 식별자로 사용되는 데이터 타입입니다.
ObjectId는 MongoDB가 각 문서를 구별하기 위해 기본적으로 사용하는 식별자로, insert_one() 같은 삽입 명령을 실행하면 자동으로 생성됩니다. PyMongo에서 참조 구조를 사용할 때, 다른 컬렉션의 문서를 참조할 때 주로 이 ObjectId를 사용하게 됩니다.
ObjectId는 총 12바이트로 구성된 값이며, 다음과 같은 의미 있는 정보들을 포함하고 있습니다:
- 4바이트: 유닉스 타임스탬프로, ObjectId가 생성된 시간을 나타냅니다.
- 5바이트: 머신 ID로, ObjectId를 생성한 서버를 구분하는 값입니다.
- 3바이트: 프로세스 ID로, 특정 머신에서 ObjectId를 생성한 프로세스를 식별하는 값입니다.
- 3바이트: 증가하는 카운터로, 동일한 프로세스 내에서 고유한 ObjectId를 보장하기 위해 사용됩니다.
ObjectId 예시
ObjectId(“5f47a53b9b1e4f11b10f20d3”)의 구성
- 5f47a53b: 첫 4바이트는 유닉스 타임스탬프(예: 2020년 8월 28일)를 나타냅니다.
- 9b1e4f: 그 다음 5바이트는 서버 ID로, ObjectId가 생성된 서버를 식별합니다.
- 11b10f: 이 3바이트는 프로세스 ID로, 서버 내의 특정 프로세스를 구분합니다.
- 20d3: 마지막 3바이트는 카운터로, 해당 프로세스가 ObjectId를 생성한 순서를 나타냅니다
2. 일관성 vs 성능 (Consistency vs Performance)
MongoDB는 기본적으로 데이터베이스의 확장성과 성능에 중점을 두고 설계되었지만, 일부 상황에서는 데이터 일관성이 중요할 수 있습니다. 데이터 일관성을 고려하면서 성능을 유지하는 것이 중요합니다.
- 일관성: 데이터의 중복을 피하고 여러 컬렉션 간의 참조 무결성을 유지하려면 참조 모델을 사용하는 것이 적합합니다.
- 성능: 임베디드 문서를 사용하면 관련 데이터를 한 번에 가져올 수 있어 성능이 크게 향상됩니다.
일관성 요구가 높을 때는 데이터의 중복을 최소화하고 참조 관계를 이용하는 것이 좋습니다. 반면, 성능을 최우선으로 하는 경우에는 데이터 중복을 허용하고 임베딩을 적극적으로 활용할 수 있습니다.
3. 문서 크기와 제한 (Document Size and Limits)
MongoDB의 단일 문서 크기는 최대 16MB로 제한됩니다. 따라서 임베디드 문서를 사용할 때는 문서가 너무 커지지 않도록 주의해야 합니다.
- 큰 데이터를 다루는 경우에는 참조 모델을 고려하여 데이터의 크기를 줄일 수 있습니다.
- 컬렉션 단위로 데이터를 분할하는 것도 고려해야 합니다.
4. 데이터 중복 허용 (Allowing Data Duplication)
MongoDB에서는 데이터를 중복 저장하는 것을 허용하는데, 이는 관계형 데이터베이스와는 다른 중요한 차이점입니다. 데이터 중복은 특정 성능 요구를 만족시키기 위해 유용할 수 있습니다. 특히 읽기 성능을 최적화하기 위해 동일한 데이터를 여러 문서에 저장하는 패턴이 종종 사용됩니다.
- 읽기 성능을 향상시키기 위해 동일한 데이터가 여러 문서에 중복되어 저장될 수 있습니다.
- 하지만 중복된 데이터를 업데이트하는 경우에는 모든 문서를 동시에 수정해야 하므로 복잡성이 증가할 수 있습니다.
중복 저장은 단순 조회(read-heavy) 작업에서 성능을 극대화할 수 있지만, 데이터 일관성을 관리하는 비용이 커질 수 있습니다.
5. 패턴과 항목의 사용 (Patterns and Anti-Patterns)
MongoDB에서 효율적인 데이터 모델링을 위해 다음 패턴을 고려할 수 있습니다.
Bucket 패턴
Bucket 패턴: 시간 기반 데이터를 저장할 때, 일정 기간 동안의 데이터를 하나의 문서로 묶어 저장하는 방식입니다. 예를 들어, 로그 데이터를 시간 단위로 버킷화하여 저장하면 성능이 향상될 수 있습니다.
데이터 그룹화: 관련된 데이터를 “버킷(Bucket)”이라고 불리는 큰 문서에 묶어서 저장합니다. 각 버킷은 일정한 크기나 범위를 갖고, 그 안에 여러 항목을 저장하게 됩니다.
시간이나 범위 기반 버킷: 버킷을 생성할 때 주로 시간을 기준으로 그룹화합니다. 예를 들어, 한 달 동안 발생한 데이터를 하나의 버킷으로 묶거나, 특정 범위의 데이터를 하나의 버킷에 저장합니다. 이렇게 하면 매번 작은 데이터를 독립적인 문서로 저장하는 대신, 관련된 데이터를 함께 저장할 수 있습니다.
버킷의 고정 크기: 버킷의 크기(한 버킷에 담길 항목의 수)를 미리 정해두고, 그 크기를 넘어가면 새로운 버킷을 생성하는 방식입니다. 이를 통해 MongoDB의 16MB 문서 크기 제한을 넘지 않도록 관리할 수 있습니다
버킷 패턴을 이용한 그룹화 저장 방식 버킷 패턴을 사용하면 여러 개의 데이터를 한 문서에 묶어서 저장할 수 있습니다. 예를 들어, 한 달 동안의 데이터를 하나의 버킷에 저장하는 방식입니다.
from pymongo import MongoClient
from datetime import datetime
= MongoClient('mongodb://localhost:27017/')
client = client['sensordb']
db
# 센서 데이터를 저장할 컬렉션
= db['sensor_data']
sensor_data
# 버킷에 데이터를 추가하는 함수
def add_sensor_reading(sensor_id, timestamp, reading):
# 월 단위로 데이터를 저장
= timestamp.strftime("%Y-%m")
month
# 버킷(문서)이 존재하면 업데이트, 없으면 새로 생성
sensor_data.update_one("sensor_id": sensor_id, "month": month},
{"$push": {"readings": {"timestamp": timestamp, "reading": reading}}},
{=True
upsert
)
# 센서 데이터를 추가
"sensor_1", datetime(2024, 9, 1, 10, 0, 0), 23.5)
add_sensor_reading("sensor_1", datetime(2024, 9, 1, 10, 1, 0), 23.6)
add_sensor_reading("sensor_1", datetime(2024, 9, 1, 10, 2, 0), 23.7) add_sensor_reading(
결과
{
"year_month": "2024_09",
"logs": [
{"timestamp": "2024-09-01", "event": "login"},
{"timestamp": "2024-09-02", "event": "purchase"}
]
}
Tree 패턴
트리 구조의 데이터를 저장할 때, 부모-자식 관계를 문서 내에 임베딩하거나 참조하여 트리 구조를 유지하는 방법입니다. 트리 패턴은 카테고리나 댓글 시스템 같은 계층 구조 데이터를 모델링할 때 유용합니다.
Parent Reference (부모 참조)는 각 자식 노드가 부모 노드의 ID를 참조하는 방식입니다. 이 방식은 부모-자식 관계를 빠르게 쿼리할 수 있습니다. 각 자식 노드에 부모의 _id를 저장하여 계층적 관계를 유지합니다.
{
"_id": ObjectId("5f47a53b9b1e4f11b10f20d3"),
"name": "Category 1",
"parent_id": null // 최상위 노드
}
{
"_id": ObjectId("5f47a53b9b1e4f11b10f20d4"),
"name": "Category 1.1",
"parent_id": ObjectId("5f47a53b9b1e4f11b10f20d3") // Category 1의 자식
}
이 구조에서는 parent_id 필드를 통해 부모 노드를 참조합니다. 최상위 노드의 parent_id는 null로 설정됩니다. 부모에서 자식을 찾을 때는 parent_id로 자식들을 쿼리할 수 있습니다.
역정규화(Anti-Pattern)
일반적인 데이터베이스 설계에서 권장되는 정규화 규칙을 의도적으로 무시하거나, 데이터 중복을 허용하는 방식으로 데이터를 구조화하는 방법입니다.
정규화가 데이터를 여러 테이블로 나누어 데이터 중복을 방지하고 일관성을 유지하는 데 초점을 맞춘 반면, 역정규화는 성능 최적화를 위해 데이터 중복을 허용하거나, 데이터 구조를 단순화하여 읽기 성능을 개선하는 방법입니다.
역정규화의 특징은
- 데이터 중복 허용: 데이터를 여러 컬렉션에 나누어 저장하는 대신, 관련 데이터를 하나의 문서에 중복해서 저장함으로써 성능을 향상시킵니다.
- 일관성 대신 성능: 데이터 중복으로 인해 일관성을 유지하는 데는 신경 써야 하지만, 읽기 성능을 높이는 것이 더 중요할 때 사용합니다.
- 빠른 읽기 최적화: 읽기 성능을 높이기 위해 데이터가 자주 사용되는 형식으로 미리 변환되어 저장됩니다. 이를 통해 복잡한 조인 없이 데이터 접근이 가능합니다.
{
"_id": ObjectId("..."),
"name": "Alice",
"email": "alice@example.com",
"orders": [
{
"items": ["item1", "item2"],
"total": 100
},
{
"items": ["item3", "item4"],
"total": 150
}
]
}
위와 같이 고객 정보와 주문 정보를 한 문서에 중복 저장합니다. 이를 통해 별도의 조인 없이 고객과 관련된 모든 주문 데이터를 한 번에 조회할 수 있어 읽기 성능이 대폭 향상됩니다.
MongoDB는 조인 연산을 권장하지 않기 때문에, 역정규화를 통해 데이터 접근 속도를 높일 수 있습니다. MongoDB의 문서 지향 모델은 데이터 중복을 감수하더라도 성능을 높이기 위해 데이터를 한 곳에 묶어 저장하는 방식에 적합합니다.
6. 인덱스 전략 (Indexing Strategy)
효율적인 데이터 조회를 위해 인덱스를 설정하는 것은 매우 중요합니다. 인덱스가 적절히 설정되지 않으면 조회 성능이 크게 저하될 수 있습니다.
인덱스란 무엇인가?
인덱스는 데이터베이스의 성능을 최적화하기 위해 테이블이나 컬렉션에서 데이터를 빠르게 찾을 수 있게 해주는 데이터 구조입니다. MongoDB에서 인덱스는 컬렉션의 한 필드 또는 여러 필드에 대한 빠른 검색을 가능하게 합니다. 만약 인덱스가 없다면, 쿼리 시 전체 컬렉션을 스캔(전체 테이블 스캔)해야 하므로 성능이 크게 저하될 수 있습니다
MongoDB의 기본 인덱스
MongoDB는 기본적으로 각 컬렉션에 대해 _id 필드에 자동으로 인덱스를 생성합니다. 이 기본 인덱스는 데이터를 고유하게 식별하며, 이를 사용해 매우 빠르게 조회할 수 있습니다. 그러나 실무에서는 _id 필드 외의 다양한 필드에 대해 검색이나 정렬이 필요하기 때문에, 추가 인덱스 생성이 필수적입니다.
단일 필드 인덱스
단일 필드 인덱스는 컬렉션의 한 필드에 대해 생성됩니다. 가장 기본적이지만 매우 유용한 인덱스입니다. 예를 들어, age 필드에 대한 단일 필드 인덱스를 생성하면 age를 기준으로 데이터를 빠르게 조회할 수 있습니다.
= MongoClient("...")
client
= client.mydatabase
db = db.mycollection
collection 1 }) db.mycollection.create_index({ age:
복합 인덱스 (Compound Index)
복합 인덱스는 두 개 이상의 필드에 대해 인덱스를 생성하는 것입니다. 이는 두 가지 이상의 조건으로 자주 데이터를 조회할 때 매우 유용합니다. 예를 들어, age와 name 필드를 자주 사용하는 쿼리라면 다음과 같은 복합 인덱스를 생성할 수 있습니다.
1, name: 1 }) db.collection.create_index({ age:
TTL 인덱스
특정 시간이 지나면 데이터를 자동으로 삭제해야 할 경우, TTL(Time To Live) 인덱스를 사용할 수 있습니다. TTL 인덱스는 일정 시간이 지나면 해당 문서를 자동으로 제거합니다.
1 }, { expireAfterSeconds: 3600 }) db.collection.create_index({ createdAt:
MongoDB는 expireAfterSeconds로 설정된 시간 이후에 TTL 인덱스가 있는 필드를 검사하고, 해당 문서가 지정된 시간이 지나면 문서를 삭제합니다. 이 작업은 MongoDB의 백그라운드 작업으로 수행되며, 삭제가 즉시 이루어지는 것은 아닙니다. 주기적으로 TTL 인덱스를 확인하고 삭제 작업을 수행하므로, 설정된 시간이 지난 직후에 문서가 바로 삭제되지 않을 수 있습니다.
인덱스 확인 및 관리
현재 컬렉션에 설정된 인덱스를 확인하려면 다음 명령을 사용할 수 있습니다.
= MongoClient("...")
client = client.mydatabase
db
= db.mycollection
collection for index in collection.list_indexes():
print(index)
결론
MongoDB의 데이터 모델링은 유연하지만, 임베디드 문서와 참조 관계의 적절한 사용, 일관성과 성능 사이의 균형, 그리고 문서 크기 제한을 고려하여 설계해야 합니다. 또한 데이터 중복을 허용하되, 적절히 관리하여 읽기 성능을 최적화할 수 있습니다.
효율적인 MongoDB 데이터 모델링은 데이터의 성격과 애플리케이션 요구 사항에 따라 달라지며, 이러한 원칙들을 바탕으로 최적의 모델을 설계하는 것이 중요합니다.