본문 바로가기

빅데이터/nosql

NoSQL강의) mongoDB에서 data 모델링하는 방법. 예제포함.

MongoDB 주요 특징

Secondary Index

  ▪ 다른 NOSQL 보다 secondary index 기능이 발달되어 있음

 

샤드키 지정

  ▪  _id : 키 필드

  ▪  Shard Key <> _id

    - 대부분의 NOSQLRow Key = Shard Key

 

Document 기반

  ▪ JSON StyleDocument : BSON(Binary JSON)

 

Modelling 주요 적용 모델링 기법

  ▪ 비정규화(Denormalization)

  ▪ 집합(Aggregation)

 

기타 mongoDB에 대한 정보 → https://blog.voidmainvoid.net/239

 

NoSQL강의) mongoDB 개요 및 설명 한페이지에 끝내기(mapReduce, aggregate 예제 포함)

Humongous DB ▪ Document DB : BSON(Binary JSON) ▪ Auto Sharding ▪ Replica Set ▪ Index : Geospatial(위치정보 처리 index), Hashed, Unique, Spars, Compound - Embedded Document, Array 필드도 인덱싱..

blog.voidmainvoid.net


MongoDB document 패턴

1:1 패턴

{
  "emp_id" : 1001,
  "emp_name" : "홍길동", 
  "reg_number" : "111111-1111111"
}

Aggregation --> Embedded Document

1:N 패턴

Linked Document는 기존 관계형 데이터베이스 처럼 비정규화하지 않고, 두개의 테이블로 분리시키는 방법이다. 제약조건이 없다는 점을 제외하면 관계형 데이터베이스와 동일하다고 볼 수 있다.

방법 1) embedded : 자식 객체가 단독으로 사용되지 않고 부모객체 내에서만 사용될 때 사용.

- 예) 주문 정보와 주문 세부 항목 정보
- 한번의
Read로 필요한 정보 모두를 읽어옴 → 읽기 성능 향상

Strong Association

방법 2) linked : 자식객체가 부모객체와는 별개로 단독으로 사용될 때 적용

- 예) 상품 분류 정보와 상품 정보

-상품분류별 상품 정보들을 조회하려면 여러번 Read를 해야 함→ 읽기 성능 저하

-데이터 일관성이 상대적으로 중요할 때 사용

-Weak Association

 

Embed vs link 선택

Embedded document의 경우

{
  "_id" : ObjectId("506ebba74c935cb364575e95"),
  "name" : "이몽룡",
  "age" : 24,
  "phones" : [ "010-513-2372", "031-748-2461" ], 
  "addresses" : [
    { "city" : "경기", "zip" : 122333, "street" : "용인" },
    { "city" : "서울", "zip" : 354351, "street" : "노원" } 
  ]
}

Linked document의 경우

{ "_id" : 1, "name" : "음료", "desc" : "콜라, 사이다 등" } 
{ "_id" : 2, "name" : "한식", "desc" : "불고기, 식혜 등" } 
{ "_id" : 3, "name" : "분식", "desc" : "라면, 김밥 등" }

{ "_id" : 1001, "productname" : "콜라", "unitprice" : 1000, "categoryid" : 1 }
{ "_id" : 1002, "productname" : "김밥", "unitprice" : 2000, "categoryid" : 1 }
{ "_id" : 1003, "productname" : "김치전", "unitprice" : 4000, "categoryid" : 1 }
{ "_id" : 1004, "productname" : "떡볶이", "unitprice" : 2500, "categoryid" : 1 }

■ 참조하는 테이블이 여러개일 수 있다면, DBRef라는 MongoDB 내부의 표준적인 참조 기법을 이용할 수 있다.

 DBRef는 참조하는 컬렉션과 키 값을 함께 명시하는 참조방법. 이로써 하나의 Document가 여러개 컬렉션의 Document를 참조할 수 있게 된다.

N:M 패턴

▪  관계형 데이터베이스에서는 주로 관계 테이블을 정의하여 조인하지만, MongoDB에서는 배열 키를 이용할 수 있다.

  - 배열 필드의 각 값에도 인덱싱이 가능하다.

▪  ) 한 분류 코드에 여러 상품이 포함될 수 있으며, 동시에 한 상품이 여러 분류 코드에 포함될 수 있는 경우

참조하는 측의 컬렉션에서 배열키 필드 사용하여 인덱싱

 

방법 1) 단방향 참조

[Categories 컬렉션]
{ "_id" : 1, "name" : "한식", "desc" : "불고기, 식혜 등" }
{ "_id" : 2, "name" : "분식", "desc" : "라면, 김밥 등" }
{ "_id" : 3, "name" : "음료", "desc" : "식혜,콜라,사이다 등" }

[Products 컬렉션]
{
  "_id" : 1001, "productname" : "김밥", "unitprice" : 2000, 
  "categoryid" : [ 1, 2 ]
}
{
  "_id" : 1002, "productname" : "식혜", "unitprice" : 3000, 
  "categoryid" : [ 1, 3 ]
}

→ 단방향 참조

방법 2) 양방향 참조

[Categories 컬렉션]
{ "_id" : 1, "name" : "한식", "desc" : "불고기, 식혜 등", productid : [ 1001, 1002 ] } 
......

[ Products 컬렉션 ]
{ "_id" : 1001, "productname" : "김밥", "unitprice" : 2000, "categoryid" : [ 1, 2 ] }
{ "_id" : 1002, "productname" : "식혜", "unitprice" : 3000, "categoryid" : [ 1, 3 ] }

부모컬랙션에서 자식 컬렉션을 참조하는 방법

※ 어느 방향으로 query가 빈번히 일어나는지 확인하여 모델링한다. ☞ 읽기성능 향상

Tree 패턴

아래와 같은 hihierarchy일 경우 어떤 tree로 표현할 수 있을까?

a
|–b
   |–c 
   |-d 
|- e
   |- f

Embedded Tree

{
 _id : tree,
 name : "a",
 childs : [
   {
     name : "b", 
     childs ": [
       { name : "c" },
       { name : "d" } 
       ]
     }, {
     name : "e", 
     childs : [
       { name : "f" } 
       ]
   } 
 ]
}

  -  전체 트리를 하나의 Document안에 포함시켜 작성하는 방법

  -  트리 구조 전체를 액세스할 경우가 빈번한 경우에 유용함

  -  지나치게 복잡해질 우려가 있음

  -  Document 크기는 16MB로 제한

Linked Document

{ _id: "a" }
{ _id: "b", ancestors: [ "a" ], parent: "a" }
{ _id: "c", ancestors: [ "a", "b" ], parent: "b" }
{ _id: "d", ancestors: [ "a", "b" ], parent: "b" }
{ _id: "e", ancestors: [ "a" ], parent: "a" }
{ _id: "f", ancestors: [ "a", "e" ], parent: "e" }

  - 트리 구조를 분리된 Document에 포함시키는 방법
  - 특정 노드를 검색
(Query)할 일이 빈번한 경우에 유용
  - 
ancestors 정보와 parent 정보를 함께 저장(부모와 조상까지 참조하는 모델)하여 검색을 편리하게 함.

  - node가 이동하게 되면 변경해야할 요소가 많다. ex) 부모 node가 타 child node로 이동할 경우 재참조 필요

Dynamic Field 패턴

일반적으로 POJO를 지원하는 document를 적용할 경우

// Object Mapper 사용가능
{
  _id: "S001", 
  name : "홍길동", 
  courses: [
    { coursename : "국어", score : 90, instructor:"김샘" }, 
    { coursename : "수학", score : 80, instructor:"박샘" },
    ...
  ] 
}

Dynamic Field를 사용하여 field명에 데이터를 저장하는 방식으로 사용

// Object Mapper 사용 불가
{
  _id: "S001", 
  name : "홍길동", 
  courses: {
    "국어" : { score: 90, instructor:"김샘" },
    "수학" : { score: 80, instructor:"박샘" }, 
    ...
  },
  clist : ["국어", "수학" ]
}

■ 장점

  ▪  데이터 사이즈를 줄일 수 있음.

  ▪  Application 측에서 값을 액세스할 때 더욱 간단히 접근이 가능함. Dynamic Type 을 지원하는 언어인 경우 유리(:Javascript, Python)

    – doc.courses[1].score --> 90
    – doc.courses["국어"].score --> 90

■ 단점
  ▪ Java와 같은 언어에서 POJO 객체를 사용할 수 없음. (필드명에 값을 저장하기 때문에)
  ▪ Secondary Index 를 사용하지 못하므로 검색시 쿼리 조건으로 사용할 경우 문제가 있음.

    – secondary index 문제는 별도의 배열필드를 이용해 검색하도록 할 수 있다. 배열인덱스를 사용하면 됨.

MongoDB 모델링 시 고려사항 정리

DataBiz 중심의 설계

  ▪ Application의 쿼리 중심 설계를 의미함. ☞ 정규화 적인 고민도 필요.
  ▪ Biz 요구사항에 맞춰서 비정규화, 데이터 중복을 허용

 

Embedded VS Linked

  ▪ Embedded
    - 16MB 제한
    - 빈번한 업데이트, 크기가 증가하는 업데이트일 때는 권장하지 않음 --> 단편화

    - 읽기 속도 향상 : 한번의 쿼리로 조회

  ▪ Linked
    - 더 복잡하지만 유연한 데이터 구조

    - 데이터 크기 제한 없음
    - 상대적으로 강한 일관성 제공 가능

 

되도록 단일 쿼리로 조회할 수 있도록 할 것

  ▪ 여러번 쿼리 하는 것보다 읽기 속도가 향상 --> Embedded 무엇이 우선인가?

  ▪ 뭣이 중헌디 : 높은 일관성? 읽기 성능? 쓰기 성능

 

데이터 액세스 패턴

  ▪ 읽기/쓰기 비율
  ▪ 쿼리/업데이트의 타입
  ▪ 데이터의 Life Cycle
  ▪ 크기가 증가하는 업데이트를 수행하는가?
  ▪ 분석적 작업을 수행하는가? (Map/Reduce, Aggregation)


MongoDB 모델링 예제 - 도서관 관리 애플리케이션

1단계 : 요구사항

Entity
  - 출판사(publishers) - 도서(books)
  - 멤버(members)

▪ 관계
  - 출판사는 여러개의 도서를 출판한다.
  - 한 멤버는 여러개의 도서를 대여한다.
  - 한 도서는 여러 멤버에게 대여될 수 있다.

  - 멤버와 도서는 N:M 관계

 

2단계 : 쿼리 결과 디자인 및 고려사항

고려사항
  - 읽기 성능이 우선이며, 쓰기 성능은 중요하지 않다.

  - 고도의 일관성이 요구되지 않는다.

조회 화면

  - 도서 정보 조회 화면
    • 도서 정보 조회시 출판사이름과 현재 대여 중인 멤버의 이름도 함께 조회된다.

    • 도서 대여 정보조회시 도서 정보와 멤버의 정보가 한번에 조회된다.
    • 이 화면의 읽기 비율이 높다.

  - 대여 내역 목록 화면
    • 대여 내역만을 별도로 조회할 수 있어야 한다.
    • 도서에 대한 과거의 대여 내역을 알 수 있어야 한다.

  - 출판사 정보 조회 화면

  - 멤버 정보 조회 화면
    • 현재 멤버의 도서 대여 내역을 조회 할 수 있어야 한다.

▪ 쓰기 화면

  - 출판사 등록 화면, 도서 등록 화면, 멤버 등록 화면, 도서 대여 화면, 도서 반납 화면

관계형 데이터베이스에서는 join 쿼리를 사용하지만, MongoDB는 Join하지 않는다는 점에 주목 
자주 조회되는 화면은 한번의 쿼리로 데이터를 가져올 수 있도록 한다. 
조회빈도가 낮은 화면이거나 데이터량이 큰 경우는 2번의 쿼리로 나누어서 가져올 수 있도록 한다.

3단계 : 컬렉션 추출

publishers collection

{
  "_id" : 124522,
  "pub_name" : "구글출판사", 
  "pub_address" : "서울시 강남구 역삼동"
}

members collection

{
  "_id" : "gdhong",
  "memb_name" : "홍길동",
  "memb_type" : "모범",
  "address" : "경기도 성남시 분당구 이매동"
}

books collection

 // 대여 가능 상태일 경우
 {
  "_id" : 27462,
  "author" : "성춘향",
  "title" : "nosql 데이터 모델링", 
  "price" : 20000,
  "pub_id" : 124522, 
  "pub_name" : "구글출판사", 
  "available" : 1
}
// 대여 상태일 경우
{
  "_id" : 27462,
  "author" : "성춘향",
  "title" : "nosql 데이터 모델링", 
  "price" : 20000,
  "pub_id" : 124522, 
  "pub_name" : "구글출판사", 
  "available" : 0,
  "lended" : {
     "memb_id" : 124522, 
     "memb_name" : "홍길동", 
     "issuedate" : "2015-05-02",
     "duedate" : "2015-05-12"
  } 
}

lending collection

{
  "_id" : ObjectId("....."), 
  "memb_id" : " gdhong", 
  "memb_name" : "홍길동", 
  "book_id" : 27462,
  "title" : "nosql 데이터 모델링", 
  "pub_name" : "구글출판사", 
  "issuedate" : "2015-05-02", 
  "duedate" : "2015-05-12", 
  "returndate" : "2015-05-11" // returndate 필드가 존재하지 않으면 대여중인 상태
}

옵션 : 출판사, 도서를 더 빠르게 조회하는 방법

방법 1 : embedded 로 내용 추가

{
  "_id" : 124522, 
  "pub_name" : "구글출판사", 
  "pub_address" : "서울시 강남구 역삼동", 
  "books" : [
    { "book_id" : 211, "title" : "hadoop Essential", "price" : 25000 },
    { "book_id" : 224, "title" : "mongodb 완벽 가이드", "price" : 27000 }, 
    ......
  ] 
}

→ 출판사의 도서가 너무 많아지면 document 최대 크기(16MB)가 넘을 수도 있고 혹은 단편화(Fragmentation)이 발생되어 쓰기 성능을 떨어트림.

 

방법 2 : 도서 ID를 배열값으로 참조

{
  "_id" : 124522, 
  "pub_name" : "구글출판사",
  "pub_address" : "서울시 강남구 역삼동",
  "book_ids" : [ 211, 224 , ... ] 
}

추가적으로 고려할만한 사항

인덱스

▪  MongoDB_id 필드뿐만 아니라 Array, Embedded Document 내의 필드에도 인덱스 설정이 가능하다.

▪  쿼리시에 조건절에서 사용되거나 정렬의 기준이 되는 필드에 대해 인덱스를 설정한다.
  - db.lendings.ensureIndex({ book_id : 1 })
  - db.lendings.ensureIndex({ memb_id : 1 })

큰 사이즈의 인덱스

너무 많은 필드가 인덱싱되는 경우 큰 사이즈의 인덱스가 생성되고, 이로 인해 오히려 성능이 저하될 수 있음.

해결책 :
  - 항상 검색할 때 사용하는 필드를 concat한 값에 대해 해시값을 생성한 후 _id 필드값으로 사용한다.
  - SHA-1 알고리즘의 경우 경우의 수가 2^160 이므로 충돌회피가 가능하다.

※ 많은 필드가 인덱싱되면 인덱스 사이즈가 지나치게 커지므로 이로 인해 성능저하가 발생

 MongoDB는 다른 nosql에 비해 Index가 강점이다. 관계형 데이터베이스 수준의 인덱스 기능을 제공. 해결을 위해 해쉬 사용 가능

▪  조회시에 항상 조회 조건으로 사용하는 필드값들을 concat한 값으로 해시를 생성하여 그 값을 _id 필드 값으로 설정한다.

▪  조회하는 기능의 애플리케이션에서 해시를 생성하여 _id필드로 조회한다. 몇십억건 수준내에서는 충돌회피가 가능하다.

  -  MD5 : 128bit

  -  SHA-1 : 160bit

  -  SHA-256 : 256bit

  Example) a, b, c field 조합으로 검색을 하고 싶을때 a+b+c field의 hash(MD5 등)값을 _id 필드로 지정 및 key로 지정 속도향상 기대 가능

샤딩 환경 고려

▪ 데이터량이 아주 많은 컬렉션인 경우 샤딩을 고려할 수 있다.

※ 기본적으로 node가 4개 이상일때 성능 향상을 기대할 수 있음

▪ 샤딩은 샤드키 설정이 중요하다.
  -  증가하는 샤드키는 대량의 쓰기시에 핫스팟을 유발시킴. : ex) Timstamp, ObjectId
  -  랜덤한 샤드키는 Range Query시에 여러 서버에 쿼리를 요청함. 쓰기 부하는 골고루 분산됨 ex) Hash값을 이용한 샤딩
  -  카디널리티가 중요함 : 골고루 분산되게끔...
  -  샤드키를 이용한 쿼리가 가능하도록 설계함.
  -  샤드키는 미리 인덱싱되어 있어야 함.
▪ 예제) lendings collection(대여정보 저장)
  - _id 필드 : 골고루 분산되긴 하지만 샤드키를 사용한 쿼리가 이루어지지 않음.
  - 도서 대역 목록을 조회할 때, issuedate와 사용자명으로 쿼리할 일이 많다면?
    - issuedate + memb_id : 복합 샤드키를 고려할 수 있음.(복합샤드키는 순서가 중요)