1. RDB 와 MongoDB
RDB | MongoDB |
Database | Database |
Table | Collection |
Tuple / Row | Document |
Column | Key / Field |
Table Join | Embedded Documents |
Primary Key | Primary Key (_id) |
2. 특징과 장단점
2-1. 특징
- Document-oriented storage : MongoDB는 database > collections > documents 구조로 document는 key-value형태의 BSON(Binary JSON)으로 되어있다.
- Full Index Support : 다양한 인덱싱을 제공한다.
- Single Field Indexes : 기본적인 인덱스 타입
- Compound Indexes : RDBMS의 복합인덱스 같은 거
- Multikey Indexes : Array에 미챙되는 값이 하나라도 있으면 인덱스에 추가하는 멀티키 인덱스
- Geospatial Indexes and Queries : 위치기반 인덱스와 쿼리
- Text Indexes : String에도 인덱싱이 가능
- Hashed Index : Btree 인덱스가 아닌 Hash 타입의 인덱스도 사용 가능
- Replication& High Availability : 간단한 설정만으로도 데이터 복제를 지원. 가용성 향상.
- Auto-Sharding : MongoDB는 처음부터 자동으로 데이터를 분산하여 저장하며, 하나의 컬렉션처럼 사용할 수 있게 해준다. 수평적 확장 가능
- Querying(documented-based query) : 다양한 종류의 쿼리문 지원. (필터링, 수집, 정렬, 정규표현식 등)
- Fast In-Pace Updates : 고성능의 atomic operation을 지원
- Map/Reduce : 맵리듀스를 지원.(map과 reduce 함수의 조합을 통해 분산/병렬 시스템 운용 지원, 하둡처럼 MR전용시스템에 비해서는 성능이 떨어진다)
- GridFS : 분산파일 저장을 MongoDB가 자동으로 해준다. 실제 파일이 어디에 저장되어 있는지 신경 쓸 필요가 없고 복구도 자동이다.
- Commercial Support : 10gen에서 관리하는 오픈소스
2-2. 장점
쌓아놓고 삭제가 없는 경우가 제일 적합하다. (ex. 로그데이터, 세션 등)
- Flexibility : Schema-less라서 어떤 형태의 데이터라도 저장할 수 있다.
- Performance : Read & Write 성능이 뛰어나다. 캐싱이나 많은 트래픽을 감당할 때 써도 좋다.
- Scalability : 애초부터 스케일아웃 구조를 채택해서 쉽게 운용가능하다. Auto sharding 지원
- Deep Query ability : 문서지향적 Query Language 를 사용하여 SQL 만큼 강력한 Query 성능을 제공한다.
- Conversion / Mapping : JSON형태로 저장이 가능해서 직관적이고 개발이 편리하다.
2-3. 단점
정합성이 떨어지므로 트랜잭션이 필요한 경우에는 부적합하다. (ex. 금융, 결제, 회원정보 등)
JOIN이 없다. join이 필요없도록 데이터 구조화 필요- memory mapped file으로 파일 엔진 DB이다. 메모리 관리를 OS에게 위임한다. 메모리에 의존적, 메모리 크기가 성능을 좌우한다. 2-4를 참고하자.
- SQL을 완전히 이전할 수는 없다.
- B트리 인덱스를 사용하여 인덱스를 생성하는데, B트리는 크기가 커질수록 새로운 데이터를 입력하거나 삭제할 때 성능이 저하된다. 이런 B트리의 특성 때문에 데이터를 넣어두면 변하지않고 정보를 조회하는 데에 적합하다.
2-4. 메모리 크기와 MongoDB 성능 이슈
참조문서 : http://bcho.tistory.com/746
MongoDB는 데이터를 write할때, 논리적으로 memory 공간에 write하고 일정 주기에 따라, 이 메모리 block들을 주기적으로 disk로 write한다. 즉 이 디스크 writing 작업은 OS에 의해서 이루어 진다.
실제 메모리(RAM) 사이즈가 작더라도 OS의 가상메모리를 쓸 수 있다. 가상메모리는 block단위로 나뉘어지는데, 이 Block들은 Disk의 block에 mapping이 되고, 이 block들의 집합이 하나의 데이터 파일이 된다.
만약 RAM에 해당 데이터 블록이 없다면, page fault가 발생하게 되고, disk에서 그 데이터 블록을 로딩하게 된다. 물론 그 데이타 블록을 로딩하기 위해서는 다른 데이터 블록을 disk에 써야 한다. 즉, page fault가 발생하면, page를 memory와 disk 사이에 switching하는 현상이 일어나기 때문에, disk IO가 발생하고, 성능 저하를 유발하게 된다.
고로, 메모리가 성능을 크게 좌우한다는 것은 page fault와 관련이 있다. page fault시 disk로 write되는 데이터는 LRU 로직에 의해서 결정이 된다. 그래서 자주 안쓰는 데이터가 disk로 out되는데, 일반적인 application에서 자주 쓰는 데이터의 비율은 그리 크지 않다. (예를들어 게시판이나 블로그를 생각해보면, 앞의 1~10 페이지 정도를 사람들이 많이 본다.)
이렇게 자주 조회되는 데이터를 Hot Data라고 하는데,이 Hot Data들이 집중되서 메모리에 올라가도록 key 설계를 하는 것이 핵심이다. 쓸 데 없이 전체 데이터를 scan하는 등의 작업을 하게 되면, 무조건 page fault가 발생하기 때문에, table scan등이 필요한 시나리오는 별도의 index table(summary table)을 만들어서 사용하는 등의 전략이 필요하다.
3. 주요 용어
3-1. document
먼저, document는 key-value쌍으로 이루어져 있다. value에는 또 다시 document가 들어갈 수 있다. 또한 동적 스키마를 갖고있어서, 같은 Collection(테이블) 안에 있는 document끼리 다른 스키마를 가질 수 있다.(스키마 프리)
{
"_id": ObjectId("5099803df3f4948bd2f98391"),
"username": "ljh",
"language": {
java: 10,
go: 3
}
}
3-2. Primary Key (ObjectId)
ObjectId는 12bytes의 16진수 값으로서, 각 document의 유일성을 보장한다.
첫 4bytes 는 현재 timestamp, 다음 3bytes는 machine id, 다음 2bytes는 MongoDB 서버의 프로세스id, 마지막 3bytes는 순차번호이다.
MongoDB에서 컬렉션에 저장된 각 문서에는 기본 키 역할을 하는 고유한 _id 라는 필드가 필요하다.
물론 ObjectID 타입이 아닌 일반 String이나 Int 도 사용가능하다.
다만 IndexSize의 크기가 달라지기 때문에 String 형태는 되도록 피해야한다.
3-3. Collection
Collection은 Document의 그룹이며, Document들이 Collection 내부에 위치하고 있다.
3-4. Database
Database는 Collection들의 물리적인 컨테이너이다. 각 Database는 파일시스템에 여러파일들로 저장된다.
4. FMP AdInformation Collection Scheme
4-1.MongoDB
//Copy from NoSQLBooster for MongoDB free edition. This message does not appear if you are using a registered version.
{
"_id": "5df03e1ac471d025873d13a0",
"memberID": 2019121108150001,
"schemaVersion": "0.0.4",
"adChannel": "FACEBOOK",
"fbCampaign": {
"_id": "5df03e1ac471d025873d139d",
"CampaignId": "23843996397020395",
"name": "캠페인",
"objective": "TRAFFIC",
"budgetOptimization": true,
"budgetOption": "DAILY",
"budgetAmount": 500000,
"expenseOptoin": false,
"bidStrategy": "LOWEST",
"adSet": [
{
"_id": "5df03e1ac471d025873d139e",
"AdSetId": "23843996397090395",
"name": "광고셋",
"target": {
"locations_Include": [
{
"key": "KR",
"name": "대한민국",
"category": "country"
}
],
"minAge": 14,
"maxAge": 20,
"genders": "ALL",
"device": {
"deviceOption": "ALL",
"mobileOption": "ALL",
"wifiOnly": true
},
"paper": {
"facebookPapers": [
"feed"
],
"instagramPapers": [
"stream"
],
"fMsgPapers": [
"messenger_home"
]
}
},
"optimizationOption": "CLICK",
"adSetSchedule": "TODAY",
"scheduleReserve": "ALLTIME",
"dailySchedule": [],
"dailyStrSchedule": "[]",
"ad": [
{
"_id": "5df03e1ac471d025873d139f",
"AdId": "23843996397370395",
"name": "광고 이름",
"fbPageId": 101460131355387,
"adCreateType": "CREATE",
"mediaType": "UNIFY",
"mediaFileType": "IMAGE",
"basicSentence": "테스트",
"title": "테스트",
"description": "테스트",
"websiteUrl": "https://test.com",
"behaviorInduced": "VALUE_SIGN_UP",
"urlParams": "",
"activeStatus": "DELETED",
"effectiveStatus": "DELETED",
"processStatus": "APPROVAL",
"AccessToken": "EAAkBKfU9KGgBAMHXeV54giBUODNgrbpiaOy7s354qvZA2VZAiacZBzTaQ0ZBIR2rXkhrWDoMFUHjZC90CY7kSEGFQCH7LrPIoHh3HZAJhVpTZBQsf9mu0LUx4ISYhzo7M0S7QJHz2WilmEni2hgUBcsVRkOJAfTIk0mOZArO2Sn4IlxZCwpO8VD0t",
"registInfo": {
"_id": 2019121108150001,
"type": "TENPING",
"time": "2019-12-11T00:53:46.668Z"
},
"updateInfo": {
"_id": 2019121108150001,
"type": "TENPING",
"time": "2019-12-11T00:53:46.668Z"
},
"statusLogInfo": {
"adId": "5df03e1ac471d025873d139f",
"logType": "SUCCESS",
"RegDate": "2019-12-11T00:53:53.032Z"
}
}
],
"activeStatus": "DELETED",
"effectiveStatus": "DELETED",
"registInfo": {
"_id": 2019121108150001,
"type": "TENPING",
"time": "2019-12-11T00:53:46.668Z"
},
"updateInfo": {
"_id": 2019121108150001,
"type": "TENPING",
"time": "2019-12-11T00:53:46.668Z"
},
"statusLogInfo": {
"adsetId": "5df03e1ac471d025873d139e",
"logType": "SUCCESS",
"RegDate": "2019-12-11T00:53:48.891Z"
}
}
],
"activeStatus": "DELETED",
"effectiveStatus": "DELETED",
"registInfo": {
"_id": 2019121108150001,
"type": "TENPING",
"time": "2019-12-11T00:53:46.668Z"
},
"updateInfo": {
"_id": 2019121108150001,
"type": "TENPING",
"time": "2019-12-11T00:53:46.668Z"
},
"statusLogInfo": {
"campaignId": "5df03e1ac471d025873d139d",
"logType": "SUCCESS",
"RegDate": "2019-12-11T00:53:47.829Z"
}
},
"registInfo": {
"_id": 2019121108150001,
"type": "TENPING",
"time": "2019-12-11T00:53:46.668Z"
},
"updateInfo": {
"_id": 2019121108150001,
"type": "TENPING",
"time": "2019-12-11T00:53:46.668Z"
},
"_class": "kr.tenping.core.model.mongo.AdInformation"
}
4-2.RDB
5.MongoDB Scheme 설계 원칙
5-1.기초
다음 세 가지 방법으로 관계를 작성할 수 있다.
- One to Few 하나 당 적은 수
- One to Many 하나 당 여럿
- One to Squillions 하나 당 무지 많은 수
각각 방법은 장단점을 갖고 있어서 상황에 맞는 방법을 활용해야 하는데 One-to-N에서 N이 어느 정도 규모/농도 되는지 잘 판단해야 한다.
One-to-Few
// person
{
name: "Edward Kim",
hometown: "Jeju",
addresses: [
{ street: 'Samdoil-Dong', city: 'Jeju', cc: 'KOR' },
{ street: 'Albert Rd', city: 'South Melbourne', cc: 'AUS' }
]
}
하나 당 적은 수의 관계가 필요하다면 위 같은 방법을 쓸 수 있다.
쿼리 한 번에 모든 정보를 갖을 수 있다는 장점이 있지만, 내포된 엔티티만 독자적으로 불러올 수 없다는 단점도 있다.
One-to-Many
// 편의상 ObjectID는 2-byte로 작성, 실제는 12-byte
// parts
{
_id: ObjectID('AAAA'),
partno: '123-aff-456',
name: 'Awesometel 100Ghz CPU',
qty: 102,
cost: 1.21,
price: 3.99
}
// products
{
name: 'Weird Computer WC-3020',
manufacturer: 'Haruair Eng.',
catalog_number: 1234,
parts: [
ObjectID('AAAA'),
ObjectID('DEFO'),
ObjectID('EJFW')
]
}
부모가 되는 문서에 배열로 자식 문서의 ObjectID를 저장하는 방식으로 구현한다.
이 경우에는 DB 레벨이 아닌 애플리케이션 레벨 join으로 두 문서를 연결해 사용해야 한다.
// category_number를 기준으로 product를 찾음
> product = db.products.findOne({catalog_number: 1234});
// product의 parts 배열에 담긴 모든 parts를 찾음
> product_parts = db.parts.find({_id: { $in : product.parts } } ).toArray() ;
각각의 문서를 독자적으로 다룰 수 있어 쉽게 추가, 갱신 및 삭제가 가능한 장점이 있지만 여러번 호출해야 하는 단점이 있다. join이 애플리케이션 레벨에서 처리되기 때문에 N-to-N도 쉽게 구현할 수 있다.
One-to-Squillions
이벤트 로그와 같이 엄청나게 많은 데이터가 필요한 경우, 단일 문서의 크기는 16MB를 넘지 못하는 제한이 있어서 앞서와 같은 방식으로 접근할 수 없다. 그래서 부모 참조(parent-referencing) 방식을 활용해야 한다.
// host
{
_id : ObjectID('AAAB'),
name : 'goofy.example.com',
ipaddr : '127.66.66.66'
}
// logmsg
{
time : ISODate("2015-09-02T09:10:09.032Z"),
message : 'cpu is on fire!',
host: ObjectID('AAAB') // Host 문서를 참조
}
다음과 같이 Join한다.
// 부모 host 문서를 검색
> host = db.hosts.findOne({ipaddr : '127.66.66.66'}); // 유일한 index로 가정
// 최근 5000개의 로그를 부모 host의 ObjectID를 이용해 검색
> last_5k_msg = db.logmsg.find({host: host._id}).sort({time : -1}).limit(5000).toArray()
5-2.숙련
앞서 살펴본 기초 방법과 함께, 양방향 참조와 비정규화를 활용해 더 세련된 스키마 디자인을 만들 수 있다.
양방향 참조 Two-Way Referencing
// person
{
_id: ObjectID("AAF1"),
name: "Koala",
tasks [ // task 문서 참조
ObjectID("ADF9"),
ObjectID("AE02"),
ObjectID("AE73")
]
}
// tasks
{
_id: ObjectID("ADF9"),
description: "Practice Jiu-jitsu",
due_date: ISODate("2015-10-01"),
owner: ObjectID("AAF1") // person 문서 참조
}
One to Many 관계에서 반대 문서를 찾을 수 있게 양쪽에 참조를 넣었다.
Person에서도 task에서도 쉽게 다른 문서를 찾을 수 있는 장점이 있지만 문서를 삭제하는데 있어서는 쿼리를 두 번 보내야 하는 단점이 있다.
이 스키마 디자인에서는 단일로 atomic한 업데이트를 할 수 없다는 뜻이다.
atomic 업데이트를 보장해야 한다면 이 패턴은 적합하지 않다.
Many-to-One 관계 비정규화
앞서 Many-to-One에서 필수적으로 2번 이상 쿼리를 해야 하는 형태를 벗어나기 위해, 다음과 같이 비정규화를 할 수 있다.
// products - before
{
name: 'Weird Computer WC-3020',
manufacturer: 'Haruair Eng.',
catalog_number: 1234,
parts: [
ObjectID('AAAA'),
ObjectID('DEFO'),
ObjectID('EJFW')
]
}
// products - after
{
name: 'Weird Computer WC-3020',
manufacturer: 'Haruair Eng.',
catalog_number: 1234,
parts: [
{ id: ObjectID('AAAA'), name: 'Awesometel 100Ghz CPU' }, // 부품 이름 비정규화
{ id: ObjectID('DEFO'), name: 'AwesomeSize 100TB SSD' },
{ id: ObjectID('EJFW'), name: 'Magical Mouse' }
]
}
애플리케이션 레벨에서 다음과 같이 사용할 수 있다.
// product 문서 찾기
> product = db.products.findOne({catalog_number: 1234});
// ObjectID() 배열에서 map() 함수를 활용해 part id 배열을 만듬
> part_ids = product.parts.map( function(doc) { return doc.id } );
// 이 product에 연결된 모든 part를 불러옴
> product_parts = db.parts.find({_id: { $in : part_ids } } ).toArray();
비정규화로 매번 데이터를 불러오는 비용을 줄이는 장점이 있다. 하지만 part의 name을 갱신할 때는 모든 product의 문서에 포함된 이름도 변경해야 하는 단점이 있다. 그래서 비정규화는 업데이트가 적고, 읽는 비율이 높을 때 유리하다. 업데이트가 잦은 데이터에는 부적합하다.
One-to-Many 관계 비정규화
// parts - before
{
_id: ObjectID('AAAA'),
partno: '123-aff-456',
name: 'Awesometel 100Ghz CPU',
qty: 102,
cost: 1.21,
price: 3.99
}
// parts - after
{
_id: ObjectID('AAAA'),
partno: '123-aff-456',
name: 'Awesometel 100Ghz CPU',
product_name: 'Weird Computer WC-3020', // 상품 문서 비정규화
product_catalog_number: 1234, // 얘도 비정규화
qty: 102,
cost: 1.21,
price: 3.99
}
앞과 반대로 비정규화를 하는 방법인데 이름 변경 시 Many-to-One에 비해 수정해야 하는 범위가 더 넓은 단점이 있다. 앞에서 처리한 비정규식과 같이 업데이트/읽기 비율을 고려해서 이 방식이 적절한 패턴일 때 도입해야 한다.
One-to-Squillions 관계 비정규화
Squillions를 비정규화한 결과는 다음과 같다.
// logmsg - before
{
time : ISODate("2015-09-02T09:10:09.032Z"),
message : 'cpu is on fire!',
host: ObjectID('AAAB')
}
// logmsg - after
{
time : ISODate("2015-09-02T09:10:09.032Z"),
message : 'cpu is on fire!',
host: ObjectID('AAAB'),
ipaddr : '127.66.66.66'
}
> last_5k_msg = db.logmsg.find({ipaddr : '127.66.66.66'}).sort({time : -1}).limit(5000).toArray()
사실, 이 경우에는 둘을 합쳐도 된다.
{
time : ISODate("2015-09-02T09:10:09.032Z"),
message : 'cpu is on fire!',
ipaddr : '127.66.66.66',
hostname : 'goofy.example.com'
}
코드에서는 이렇게 된다.
// 모니터링 시스템에서 로그 메시지를 받음.
logmsg = get_log_msg();
log_message_here = logmsg.msg;
log_ip = logmsg.ipaddr;
// 현재 타임 스탬프를 얻음
now = new Date();
// 업데이트를 위한 host의 _id를 찾음
host_doc = db.hosts.findOne({ ipaddr: log_ip },{ _id: 1 }); // 전체 문서를 반환하지 말 것
host_id = host_doc._id;
// 로그 메시지, 부모 참조, many의 비정규화된 데이터를 넣음
db.logmsg.save({time : now, message : log_message_here, ipaddr : log_ip, host : host_id ) });
// `one`에서 비정규화된 데이터를 push함
db.hosts.update( {_id: host_id }, {
$push : {
logmsgs : {
$each: [ { time : now, message : log_message_here } ],
$sort: { time : 1 }, // 시간 순 정렬
$slice: -1000 // 마지막 1000개만 뽑기
}
}
});
5-3.정리
장미빛 MongoDB를 위한 6가지 원칙은 다음과 같다.
- 피할 수 없는 이유가 없다면 문서에 포함할 것.
- 객체에 직접 접근할 필요가 있다면 문서에 포함하지 않아야 함.
- 배열이 지나치게 커져서는 안됨. 데이터가 크다면 one-to-many로, 더 크다면 one-to-squillions로. 배열의 밀도가 높아진다면 문서에 포함하지 않아야 함.
- 애플리케이션 레벨 join을 두려워 말 것. index를 잘 지정했다면 관계 데이터베이스의 join과 비교해도 큰 차이가 없음.
- 비정규화는 읽기/쓰기 비율을 고려할 것. 읽기를 위해 join을 하는 비용이 각각의 분산된 데이터를 찾아 갱신하는 비용보다 비싸다면 비정규화를 고려해야 함.
- MongoDB에서 어떻게 데이터를 모델링 할 것인가는 각각의 애플리케이션 데이터 접근 패턴에 달려있음. 어떻게 읽어서 보여주고, 어떻게 데이터를 갱신한 것인가.