이전 포스팅에서는 MongoDB 모델링에 대한 소개와 함께 OLTP 성 모델링을 어떻게 하면 좋을지에 대한 내용이었다.
이번에는 더 다양한 사례를 정리한다.
예측할 수 없는 행동 게임 로그 모델링
게임 로그를 예로 들어, 예측할 수 없는 사용자 행동을 효과적으로 저장하고 분석할 수 있는 NoSQL 스키마 모델링 방법에 대해 알아보자
문제점: 비효율적인 배열 구조
각 사용자들의 행동 패턴을 분석하기 위해 하루치 모든 행동을 기록한다고 가정해보자.
그러면 아래와 같은 형태의 스키마를 설계할 수 있다.
{
"time": "2024-12-13",
"visits": [], // 지역 방문 로그
"quests": [], // 퀘스트 로그
"trades": [], // 아이템 거래 로그
"...": []
}
역정규화 이념을 따라 만들었지만, 이 방식의 문제는 다음과 같다.
- 배열 문제: 일부 배열은 비어 있을 수 있고, 반면에 특정 배열은 지나치게 커질 수 있다.
- 사용자가 거래를 하지 않는다면 의미없는 필드도 발생하고, 한 배열에 너무 많은 데이터가 쌓일 수 있다.
- 인덱스 조합 증가: 행동 유형별(방문, 퀘스트, 거래 등등..)로 데이터를 나누면 복잡한 인덱스 조합이 필요할 수 있다.
Attriube 패턴으로 해결
{
user: "noose",
loginTime: "2024-11-04T15:00:00",
logoutTime: "2024-11-04T20:00:00",
actions: [
{ action: "visit", value: "Henesys", date: "2024-11-04T15:10:00" },
{ action: "visit", value: "Henesys Shop", date: "2024-11-04T15:14:00" },
{ action: "visit", value: "Henesys Market", date: ".." },
{ action: "trade", value: "gun", type: "buy", quantity: 10, price: 500, date: ".." },
{ action: "trade", value: "gun", type: "sell", quantity: 10, price: 500, date: ".." },
{ action: "quest", value: "chapter1", reward: 5000, date: ".." },
...
]
}
실제로 쿠팡 같은 쇼핑몰에서는 판매 물품마다 속성이 정의되어있다.
물품마다 각각 가지는 옵션이 다 다를 것이다.
- 무게: mg, kg, ...
- 용량: mL, L
- 색상: red, blue, green, black, ...
상품마다 싱글 필드가 다 다를 수 있는 경우 동일한 패턴으로 확장성있게 표현이 가능하다.
// 싱글 필드의 문제
{
...
size: "100ml",
color: "red",
},
{
...
memory: "10GB",
color: "red",
}
// Attribute 패턴 개선
{
...
spec: [
{ key: "size", value: 100, unit: "ml" },
{ key: "color", value: "red" }
]
},
{
...
spec: [
{ key: "memory", value: 10, unit: "GB" },
{ key: "color", value: "black" }
]
}
시간에 따라 달라지는 로그성 수집 상황
IOT 센서 데이터 수집
각 층별로 센서로 수집된 온도 데이터를 1분마다 MongoDB에 저장한다고 가정해보자.
이때 발생할 수 있는 문제와 해결 방안을 살펴보자.
문제점: 문서의 폭발적 증가
1분마다 데이터를 도큐먼트로 저장하게 되면 양이 과도하게 증가하여 저장소와 인덱싱 효율성이 저하될 수 있다.
해결책: 버킷 패턴 (Bucket Pattern)
버킷 패턴은 여러 개의 데이터를 하나의 문서로 묶어 저장하는 방법이다.
예시를 통해 알아보자
{
"sensorId": 1,
"startDate": "14:00",
"endDate": "15:00",
"measurements": [
{ "timestamp": "14:00", "temperature": 24 },
{ "timestamp": "14:01", "temperature": 24 },
...
{ "timestamp": "14:39", "temperature": 23 }
],
"totalTemperature": 25
}
이러면 한시간단위로 도큐먼트가 생성되고 도큐먼트 내에 배열로 1분마다 온도를 저장할 수 있게된다.
버킷 패턴의 장점
- 문서 수 감소: 여러 데이터를 하나의 문서로 묶음으로써 저장소 효율성을 높일 수 있다.
- 인덱스 크기 감소: 각 데이터 포인트마다 인덱스를 생성하지 않아도 되므로 저장 공간을 절약할 수 있다.
- 통계 연산 효율화: totalTemperature 와 같은 필드를 미리 계산해 저장하면 통계 연산 시 CPU 부하도 줄일 수 있다.
문제점: 다양한 집계 주기 요구사항
만약 1시간 단위가 아닌 30분 단위로 데이터를 집계해야 하는 경우, 기존 버킷 패턴으로는 유연한 처리가 어려울 수 있다.
해결책: MongoDB의 타임 시리즈 컬렉션 (Time Series Collections)
MongoDB 5.0 이상에서는 타임 시리즈 데이터를 효율적으로 처리하기 위한 전용 컬렉션을 제공하고있다.
- 자동 버킷화: 데이터를 시간 단위로 자동으로 그룹화하여 저장한다.
- 클러스터드 인덱스 지원: 데이터를 물리적으로 인접하게 저장하여 조회 성능을 향상시킨다.
- 스토리지 절약: 데이터 저장 공간을 최적화한다.
- 유연한 쿼리: setWindowFields 같은 연산자를 활용하여 다양한 시간 범위의 데이터를 손쉽게 집계할 수 있.
// 타임 시리즈 컬렉션 생성
db.createCollection("sensorData", {
timeseries: {
timeField: "timestamp",
metaField: "sensorId",
granularity: "minutes"
}
});
// 데이터 삽입 예시
db.sensorData.insert({
timestamp: ISODate("2023-12-13T14:00:00Z"),
sensorId: 1,
temperature: 24
});
// 데이터 집계 예시
db.sensorData.aggregate([
{
$setWindowFields: {
partitionBy: "$sensorId",
sortBy: { timestamp: 1 },
output: {
avgTemperature: { $avg: "$temperature" }
}
}
}
]);
버킷 패턴은 대량의 데이터를 효율적으로 저장하고 처리하기 위한 강력한 방법이다.
MongoDB의 타임 시리즈 컬렉션은 더욱 효율적이고 유연한 대안을 제공하여 데이터 집계와 조회를 간소화할 수 있다.
배달 어플 주문 리뷰 서비스
기본 데이터 모델
배달 어플리케이션에서 한 음식점의 데이터를 어떻게 모델링할지 고민해보자.
기본적으로는 음식점(shop)과 리뷰(reviews)를 연결할 수 있다.
{
"id": 1,
"name": "김치찌개집",
"reviews": [
{"id": 1000, "user": "noose", "review": "good", "rating": 10},
{"id": 1001, "user": "john", "review": "delicious", "rating": 9},
...
]
}
구조를 보면 음식점에 대한 리뷰를 배열 형태로 저장하고있다.
이렇게 기본적으로 모든 리뷰를 한 음식점 문서에 다 저장하는 방식은 구현이 간단하지만, 몇 가지 문제가 발생할 수 있다.
문제점: Document 크기 및 성능 저하
MongoDB는 한 Document 크기가 16MB를 초과할 수 없습니다. 음식점에 대한 리뷰가 수백, 수천 개 이상 축적되면 문서 크기가 커져 성능이 저하될 수 있다. 이 문제를 해결하려면 문서 크기를 줄여야 한다.
상위 리뷰만 유지
reviews 배열을 그대로 유지하되, 각 음식점마다 리뷰 수를 제한하여 상위 리뷰만 유지하는 방법을 사용할 수 있다.
예를 들어, 음식점에 대한 리뷰 중 상위 10개만 저장하는 방식이다.
이렇게 하면 한 Document 크기를 줄일 수 있으며, 자주 조회되는 중요한 리뷰들만 효율적으로 관리할 수 있게된다.
Working Set과 Working Set Size
working set은 메모리에 상주하는 데이터의 집합을 말한다.
효율적으로 데이터를 관리하려면 working set을 줄이는 것이 중요하다.
앞서 상위 리뷰들만 Dcoument에 저장하는 방식으로 Document 내 배열을 줄일 수 있다고 말했다.
이 워킹셋만 줄여도 다음과 같은 효과를 가져온다.
- 쿼리 성능 개선: 적은 수의 리뷰만 저장되므로, 조회 성능이 향상
- 메모리 최적화: 불필요한 리뷰 데이터를 메모리에 로드하지 않으므로, 메모리 사용이 효율적
- 캐시 데이터 워킹셋 감소
사용자 패턴을 봤을 때, 상위 리뷰를 보고 이후 리뷰는 보지 않는 경우가 많다.
만약 추가 리뷰를 보고 싶다면 별도 API를 분리하여 리뷰들만 모아둔 컬렉션을 통해 조회하는 것이 좋다.
카테고리가 많은 쇼핑몰 서비스
복잡한 계층 구조를 갖는 서비스(쇼핑몰)에서 카테고리와 같은 다중 계층 데이터를 처리할 때, 하이어아키 구조를 모델링하는 것은 중요한 문제이다.
일반적으로, RDBMS에서는 재귀적으로 조인하거나 애플리케이션에서 계층 구조를 만드는 방식으로 구현하는데, 이는 다소 복잡하고 성능에 부담을 줄 수 있습니다.
MongoDB에서는 $graphLookup를 활용해 쉽게 계층 조회를 할 수 있다.
{
"id": 1,
"name": "패션",
"parent_id": null
}
{
"id": 2,
"name": "의류",
"parent_id": 1
}
{
"id": 3,
"name": "티셔츠",
"parent_id": 2
}
parent_id를 통해 계층 관계를 확인할 수 있다.
db.categories.aggregate([
{
$graphLookup: {
from: "categories",
startWith: "$id",
connectFromField: "id",
connectToField: "parent_id",
as: "subcategories"
}
}
]);
graphLookup을 사용하면 특정 카테고리와 관련된 모든 하위 카테고리를 쉽게 조회할 수 있다.
카테고리뿐만 아니라 계약, 결재, 커뮤니티 도메인에서도 회사 결재선이나 SNS 관계와 같은 그래프 구조를 다룰 때도 graphLookup이 유용하다.
스키마 버전 관리 (Schema Versioning)
MongoDB에서 데이터의 스키마가 변경될 수 있기 때문에, 스키마 버전 관리가 필요하다.
스키마 버전 관리 패턴(Schema Versioning Pattern)은 이를 해결하는 방법으로, 각 문서에 schema_version 필드를 추가하여 스키마 변경을 관리할 수 있다.
{
"name": "김치찌개집",
"schema_version": "2",
"reviews": [
{"id": 1000, "user": "noose", "review": "good", "rating": 10}
]
}
애플리케이션에서 schema_version을 확인하여 적절한 방식으로 데이터를 처리하면,
스키마 변경이 있을 때 호환성 문제를 해결할 수 있다.
예를 들어, 스키마 버전이 "2"라면 reviews 필드에 새로운 형식이 적용된 경우, 버전에 맞게 데이터를 변환하여 응답하는 방법으로 처리할 수 있다
이전 포스팅과 함께 MongoDB의 다양한 모델링 기법을 살펴보았다.
향후 MongoDB를 활용한 데이터 모델링 기회가 온다면, 정리한 기법들을 참고하여 효율적이고 확장 가능한 시스템을 구축할 수 있기를 기대한다.