Database/MongoDB

MongoDB - 3. Data Model

FreeEnd 2021. 10. 12. 11:18
반응형

데이터 베이스 스키마 모델링

 우리는 어플리케이션 개발시 데이터베이스는 거의 필수로 사용하고 있다. 데이터베이스를 사용하기로 결정한 뒤, 구성하기 위해서는 다양한 고민이 필요하다. 다음은 데이터베이스를 구성할 때 일반적으로 고민 하는 몇개의 판단이다.

 

 첫번째, 데이터 베이스에 대한 선택이다.

MongoDB 가 어플레케이션에서 사용하기에 가장 편한 구조인 Document 형을 사용하고, 대량의 데이터를 빠르게 찾고, Insert 를 할 수 있는 장점이 많은 데이터베이스 이지만, 관계형 데이터를 사용할 수 없고, 트랜잭션 처리가 힘든 단점이 있다. 때문에, 데이터베이스를 선택할때 이 장점과 단점을 고려해 알맞게 선택 해야 한다.

 

 두번째, 데이터베이스에 대한 사용성이다.

 MongoDB 는 realation 을 지원하지 않기 때문에 각 테이블간 join을 할수 없어 원하는 데이터에 대한 조회가 쉽지 않다. 또한, 트랜잭션시 rolback, commit 등을 지원하지만, 고성능이 필요한 MongoDB 특성상 권장 하지는 않아 사용시 많은 고민이 필요하다. 

 

 이렇게 데이터베이스에 대해 선택이 끝나면 그 다음에는 데이터를 어떻게 구성할 지에 대한 고민을 한다.

 일반적으로 관계형 데이터베이스 (Relational Database, 이하 RDBMS) 는 데이터를 관리 하고자 할때는 주어진 요건을 모두 정리해 데이터를 사용영역별로 나누고, 관계를 정의 해 데이터를 정규화 한다. 이러한 정규화 과정은 데이터에 대한 무결성을 이룰수 있으며, 다양한 질의를 사용해 원하는 데이터를 다양한 구조로 얻어 낼 수 있다. 

 Document DB 인 MongoDB는 RDMS 와 다르지 않다. 단지, 관계형 데이터 를 쓰지 않을 뿐이지, 데이터를 사용하기 위해 관련 데이터를 묶고, 어떤 타입의 데이터를 사용할 것인지, Document 내에서 데이터의 구조는 어떻게 구성할것인지에 대한 고민은 꼭 필요하다. PK는 어떻게 구성할 것인지, 데이터가 얼마동안 유지될 것인지, 로그성 데이터인지 등 많은 판단이 필요하다.

 

 이번 단원에서는 스키마 모델링시에 필요한 관계와 구조에 대해 알아 보자.

 

 

ORM (Object-relational mapping) 는 필요 없다.

 RDMS 의 경우, 데이터를 조회하고 사용하기 위해 JPA나 Hibernate 등이 이용된다. 이 ORM 은 SQL 을 자동으로 만들어 주거나, 대이터를 Object 에 매핑을 대신 해줘 어플리케이션의 개발 속도를 높혀 준다.

 그러나, MongoDB 는 ORM 툴에 대한 필요성이 낮다. Document가 이미 Object 형의 데이터 구조를 그대로 담고 있기때문이다. 

 

 

고유 키 관리

Primary Key - _id

 _id 는 Document 의 Primary Key 이며, 당연하지만 collection 에서 유일한 키로 사용된다. 일반적으로 _id 는 지정하지 않고 저장 하는게 대부분이며, 이때 Object 로 Database 에 유일한 고유 코드를 자동으로 할당한다.

 

 

 

Unique Index

 Document 에 PK 는 이미 _id 가 담당 하고 있다. 하지만 _id는 사람이 읽거나 사용하기 불편하다.

 아래 예제와 같이 item Document 의 고유 값은 ObjectId 를 이용해 조회 해야 하지만, ObjectId("61642a98b177ff22016eb51d") 같은 형식으로 한눈에 확인 하기 힘들다. 이때 직관적으로 itemId 를 Unique 한 값으로 관리 할 수 있게 지정 가능하다. 

 이 Unique Index 를 사용해 설정하는 방식인데,  이 Unique Index 를 사용해 설정하는 방식인데, 해당 인덱스를 설정 한 뒤, 동일한 키를 입력하고자 시도 한다면 오류가 발생 한다.

 

> db.item.find();
{ "_id" : ObjectId("61642a98b177ff22016eb51d"), "itemId" : "1000000000001" }
{ "_id" : ObjectId("61642a9fb177ff22016eb51e"), "itemId" : "1000000000002" }
{ "_id" : ObjectId("61642aa2b177ff22016eb51f"), "itemId" : "1000000000003" }
{ "_id" : ObjectId("61642aa3b177ff22016eb520"), "itemId" : "1000000000004" }
{ "_id" : ObjectId("61642aa5b177ff22016eb521"), "itemId" : "1000000000005" }
{ "_id" : ObjectId("61642aa6b177ff22016eb522"), "itemId" : "1000000000006" }
{ "_id" : ObjectId("61642aa8b177ff22016eb523"), "itemId" : "1000000000007" }

#index 생성
> db.item.createIndex({"itemId":1}, {unique: true})
{
        "numIndexesBefore" : 1,
        "numIndexesAfter" : 2,
        "createdCollectionAutomatically" : false,
        "ok" : 1
}

#동일한 값 입력 시도시 duplicate key error collection 발생
> db.item.insert({itemId:"1000000000001"})
WriteResult({
        "nInserted" : 0,
        "writeError" : {
                "code" : 11000,
                "errmsg" : "E11000 duplicate key error collection: ssg.item index: itemId_1 dup key: { itemId: \"1000000000001\" }"
        }
})

 

관계 구조

Sub-Docuemnt (중첩 도큐멘트)

 Sub-Docuemnt 는 document 하위에 value 값이 아닌 document 를 추가 하는 방식이다. 이전 장에서 봤던 "update({key:value}, {$set : {document}})" 형식으로 입력한 구조이다.

 이  Sub-Docuemnt 일반적으로 이 Document 가 가지고 있는 다양한 속성 정보를 관리할 때 사용된다.

 

{
        "_id" : ObjectId("61642a98b177ff22016eb51d"),
        "itemId" : "1000000000001",
        "mltgItemNm" : {
                "kor" : "korean itemNm",
                "eng" : "english itemNm"
        },
        "tag" : [
                "child",
                "toy"
        ]
}

 

 

ONE TO MANY (1:N, 일대다)

 일대다 구조는 한 Document 가 여러개의 다른 데이터를 가지는 경우, 혹은 다른 Document 의 key 를 여러개 참조 하는 구조이다. 위 중첨 도큐멘트의 mltgItemNm 이 관계를 가지며, 다음과 같이 color 속성도 이 관계를 가진다.

  color 의 경우, 하나의 상품은 1개의 color 값만 갖을 수 있고, color 의 경우, 매핑된 여러개의 상품이 존재 하므로 color 의 입장에서 일대다 구조이다.

 

#item 
{
        "_id" : ObjectId("61642a98b177ff22016eb51d"),
        "itemId" : "1000000000001",
        "color" : ObjectId("61643031c51b2db8e27dfe88")
}
{
        "_id" : ObjectId("61642aa2b177ff22016eb51f"),
        "itemId" : "1000000000002",
        "color" : ObjectId("61643031c51b2db8e27dfe88")
}

#color
{
        "_id" : ObjectId("61643031c51b2db8e27dfe88"),
        "colorNm" : "red"
}

 

MANY TO MANY (N:N, 다대다)

 다대다의 구조는 상호 N개 매핑 가능한 구조를 말한다. 다음과 같이, 한 상품은 여러개의 색상을 갖을 수 있고, 역시 컬러도 여러개의 상품에 대한 참조 관계가 발생 하기 때문에 다대다 구조가 발생한다.

 

#item 
{
        "_id" : ObjectId("61642a98b177ff22016eb51d"),
        "itemId" : "1000000000001",
        "color" : [ ObjectId("61643031c51b2db8e27dfe88")
                  , ObjectId("61643035c51b2db8e27dfe89")
                  ]
}
{
        "_id" : ObjectId("61642aa2b177ff22016eb51f"),
        "itemId" : "1000000000002",
        "color" : [ ObjectId("61643031c51b2db8e27dfe88")
                  , ObjectId("61643035c51b2db8e27dfe89")
                  ]
}

#color
{
        "_id" : ObjectId("61643031c51b2db8e27dfe88"),
        "colorNm" : "red"
}
{
        "_id" : ObjectId("61643035c51b2db8e27dfe89"),
        "colorNm" : "blue"
}

 

이러한 관계에서, color 입장에서 자신을 참조하는 상품 리스트를 조회 하기 위해선 다음과 같이 질의 한다.

$in - 배열 연산자.

$in 연산자는 주어진 배열 안에 속하는 모든 값을 조회 하는 연산자이다.

 아래 예제는 _id 값에 item의 color 값에 존재 하는 모든 color key 이므로, color 값을 참조하는 모든 상품이 조회 된다.

# 테스트 실패, 곧 수정하겠습니다.
> db.color.find({_id: {$in: item['color']}})

 

 

정규화 - 반정규화

위 color 예제와 같이, 단순히 color 명만 존재 하는 color 컬렉션의 경우에는, 일반적으로 다른 컬렉션을 생성하지 않고, item document 에 color 값을 직접 입력 하는 편이 더 효과 적이다. collect 생성시에 collection 을 관리 하는 헤더값도 필요하게 되고, 다른 collection 을 참조하는 관계형 데이터 조회시의 비용을 줄일수 있기 때문이다.

 하지만 예를 들어 상품 - 주문내역 - 사용자 등과 같이 명확하게 데이터적으로 분리가 되어 있고, 각 분리된 데이터 모델 별로 관리 해야 하는 데이터가 명확하다면 Document 를 분리 함이 옳다.

 

 

데이터 베이스 관리 단위

Database - 데이터베이스

 전 단원에서도 말 했다 시피, 데이터베이스는 컬렉션의 모음이다. 컬렉션들과 이들을 이루는 인덱스를 모아 관리

하는 네임스페이스 단위이기도 하다.  데이터 베이스의 생성은, 최초 Document 가 insert 되는 시점에 생성 된다. 데이터베이스 생성은 이전 단원에서 다뤘으니 추가 설명은 생략 하겠다. 

 데이터 베이스의 삭제는 다음과 같다. 참고로 DB 삭제 후에는 되살릴수 없다.

> use ssg
> db.dropDatabase();

 

 데이터베이스의 상태는 다음과 같이 조회 한다.

> use ssg
> db.stats()
{
        "db" : "ssg",
        "collections" : 2,
        "views" : 0,
        "objects" : 9,
        "avgObjSize" : 66.33333333333333,
        "dataSize" : 597,     			# BSON 객체 실제 크기
        "storageSize" : 57344,			# 데이터 증가를 고려한 여분 공간을 포함한 전체 크기
        "freeStorageSize" : 16384,
        "indexes" : 3,
        "indexSize" : 94208,			# index 사이즈
        "indexFreeStorageSize" : 32768,
        "totalSize" : 151552,
        "totalFreeStorageSize" : 49152,
        "scaleFactor" : 1,
        "fsUsedSize" : 13143293952,
        "fsTotalSize" : 75125227520,
        "ok" : 1
}

 

 MongoDB 3.0 이전까지는 Namespace 파일( ns) 의 크기가 16MB 고정으로 약 26,000 개까지 인덱스, 컬렉션을 생성 하할 수 있었지만, 그 이후 버젼에서는 해당 제한이 없다.

 

 Database 의 데이터 양을 항상 확인하여, 스토리지 공간이 부족하지 않은지 항상 체크 해야 한다. 스토리지의 경우 stats() 명령어로 현재 DB 사이즈와 스토리지 사이즈로 가늠이 가능하지만, RAM 은 시스템상에서 모니터링 해야 한다.

RAM 은 MongoDB가 write 가 먼저 일어나고 Document 조회시에도 사용 되기 때문에, RAM 이 부족해지만 심각한 성능 저하게 일어날 수 있다. 때문에 필히 충분한 메모리가 확보 되었는지 체크가 필요하다.

 

Collection - 컬렉션

Collect 은 Document 의 집합이다. 컬렉션은 Database 와 마찬가지로, Document 가 최초 생성될때 함께 생성된다. 하지만 명시적으로 직접 생성하는 명령어도 존재 한다. 

 

> use ssg
> db.createCollection("images", {size:20000}

images 라는 컬렉션을 생성하고, 기본 사이즈를 20000 바이트로 지정하였다. 컬렉션 명은 영문자, 숫자, 점(.) 을 이용해 상성 가능하지만, 사용편의성과 직관성을 위해 일반적으로 첫문자는 영문자를 사용하며 점(.) 은 사용하지 않는다.

또, 컬렉션의 전체 이름은 128자 이하로 생성한다.

'

Capped Collection (캡드 컬렉션)

 캡드 컬렉션은 고정된 크기를 갖는 컬렉션을 말한다. 컬렉션의 사이즈는 고정되어 있고, 해당 공간을 모두 사용하였을 경우, 가장 오래된 데이터 가 삭제되고, 신규 데이터가 추가된다. 이는 로깅이나 특정양이 데이터를 유지해야 하는 모델에서 주로 사용된다. 

 생성하는 방법은 Collection 생성시, capped 매개변수를 true 로 전달 한다.

> use ssg
> db.createCollection("images", {
... capped: true,
... autoIndex: true,
... size: 6142800,
... max: 10000
... })

캡드 컬렉션은 데이터를 임의로 UPDATE 나 DELETE 를 할 수 없다. 이는 CAPPED 가 주어진 목표 사이즈에 따라 처리가 되기 떄문이다.

 

TTL Collection (Time To Live Collection)

 TTL 은 개발자라면 모두 알다 싶이, 특정기간동안에만 유지되는 데이터를 말한다. MongoDB 에서도 이 기능을 제공한다. 사실 TTL 은 Unique 와 마찬가지로 Index로 구현된다. TTL 기준이 되는 속성 을 지정하고, expire 가 될 소요 시간을 초 단위로 입력한다. 다음 예제는 상품 등록후 600 초후 삭제 되도록 설정 하였다.

#TTL index 생성
> db.item.createIndex({"regDts":1}, {expireAfterSeconds: 600})

 TTL index 는 지정된 컬럼의 값과 TTL 로 지정한 EXPIRE TIME 을 더한 값이 현재 시간에 도달 했는지 체크한다. 때문에 미래 시점으로 입력을 해두면 그만큼 삭제를 지연 시킬 수 있다. 

 TTL 은 데이터가 삭제되기 때문에 capped 컬렉션에서는 사용 할 수 없다.

 

System Collection

시스템 컬렉션은 MongoDB 가 내부에서 사용하는 컬렉션들로, 대표적으로 system.namespaces, system.indexes 가 있다.

 

 

 

Document - 도큐먼트

document 는 MongoDB 의 데이터 단위이다. 이번에는 세부적으로 데이터 타입에 대해 알아보자

MongoDB 는 BSON 형태로 데이터를 저장 하기때문에 기본적으로 BSON 의 모든 스펙 기준으로만 데이터 입/출력이 가능하다.

참고 : https://docs.mongodb.com/manual/reference/operator/query/type/

 

문자열

 문자열은 UTF-8 을 사용해야 한다. import 시에 캐릭터 셋을 항상 고려 해야한다.

 

숫자

 double, int, long, 이렇게 3가지 타입을 사용 할 수 있다. 

 

 숫자형을 지정하는 방법은 다음과 같다.

 

NumberInt(숫자) : int 형의 데이터를 지정한다. 32bit 의 integer 형으로 입력된다.

NumberLong(숫자) : long 형의 데이터를 지정한다. 64bit 의 integer 형으로 입력된다.

미지정 : double 형의 데이터를 지정한다. 소숫점까지 포함해 입력 가능하다.

 

예제와 같이 입력하면 다음 결과를 얻는다.

db.item.update({itemId: "1000000000001"}, 
    {$set:{
        typeTest :{
            intCnt:NumberInt(10),
            longCnt:NumberLong(10),
            doubleCnt:10
        }
     }
    }
);

보기 편하게 MongoDB IDE 툴인 Roto 3T 로 조회한 조회 결과 이다.

intCnt 는 int32형으로, longCnt는 int64형으로, 미지정 숫자는 Double 형으로 입력 되었다.

 

 

컬럼의 타입형으로 데이터 조회도 가능하다. 다음과 같이 $type 을 지정하고 find 를 하면 원하는 타입의 컬름을 가진 document 를 검색 할 수 있다.

# $type : 1 은 double 를 나타낸다. 해당 결과는 없다.
> db.item.find({"typeTest.longCnt" : {$type :1 }}).pretty(); 


# $type : 18 은 long 을 나타낸다.
> db.item.find({"typeTest.longCnt" : {$type :18 }}).pretty();
{
        "_id" : ObjectId("61642a98b177ff22016eb51d"),
        "itemId" : "1000000000001",
        "mltgItemNm" : {
                "kor" : "korean itemNm",
                "eng" : "english itemNm"
        },
        "tag" : [
                "child",
                "toy"
        ],
        "color" : [
                ObjectId("61643031c51b2db8e27dfe88"),
                ObjectId("61643035c51b2db8e27dfe89")
        ],
        "invCnt" : 10,
        "typeTest" : {
                "intCnt" : 10,
                "longCnt" : NumberLong(10),
                "doubleCnt" : 10
        }
}

$type 으로 조회 가능한 타입의 숫자 표현식은 다음 참고 url 의 Available Type 항목에서 참고 가능하다.

참고 : https://docs.mongodb.com/manual/reference/operator/query/type/

 

 MongoDB 3.4 이상 버젼부터 decimal 도 지원한다.

 

날짜

 MongoDB는 유닉스 에폭 (Unix Epoch) 이후 millisecond 단위의 경과 시간으로 표현된다. 유닉스 에폭은 1970년 1월 1일 00시를 기준으로 하며, 이전은 음수, 이후는 양수로 표현한다. 

 참고 : https://docs.mongodb.com/manual/core/shell-types/

 

new Date()

 현재 시간을 입력하려면 new Date() 를 이용해 입력한다. 날짜 유형은 ISODate 형으로 출력되며, insert 시간이 노출 되게 된다.

> db.item.update({itemId : "1000000000001"}, { $set: { "regDts" : new Date()} })
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

> db.item.find({itemId : "1000000000001"}).pretty();
{
        "_id" : ObjectId("61642a98b177ff22016eb51d"),
        "itemId" : "1000000000001",
        "regDts" : ISODate("2021-10-14T11:00:23.614Z")
}

 

 이번에는 날짜를 지정해서 이력해보자. 

new Date(2021, 10, 14) 로 입력했는데, 조회된 날짜는 2021년 11월 14일 이다. 이는, 자바스크립트에서는 월을 0부터 시작하기 때문이다. 그래서 10은 11월을 의미한다.

> db.item.update({itemId : "1000000000001"}, { $set: { "regDts" : new Date(2021, 10, 14)} })
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

> db.item.find({itemId : "1000000000001"}).pretty();
{
        "_id" : ObjectId("61642a98b177ff22016eb51d"),
        "itemId" : "1000000000001",
        "regDts" : ISODate("2021-11-14T00:00:00Z")
}
>

 

new ISODate(시간);

 이번에는 ISODate 를 이용해 시간을 입력 해보았다. 예제와 같이 10월로 입력해도, 정상적으로 입력된다.

> db.item.update({itemId : "1000000000001"}, { $set: { "regDts" : new ISODate("2021-10-14T12:12:12.121Z")} })
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

> db.item.find({itemId : "1000000000001"}).pretty();
{
        "_id" : ObjectId("61642a98b177ff22016eb51d"),
        "itemId" : "1000000000001",
        "regDts" : ISODate("2021-10-14T12:12:12.121Z")
}
>

 

가상타입  (생략)

 

Document 제약사항

 Document의 최대 크기는 16MB 로 제한된다. 개발자가 효율적이지 못한 데이터를 설계하는것을 방지하고, 데이터를 시리얼라이즈 할때 성능 저하를 방지 하기 위함이다. 그렇기 때문에 Document 설계시 데이터 단위를 분할 할 것인지 고민이 필요하다.

 또한 최대 중첩은 100개까지 가능하다. 

 

대량 삽입

 대량 삽입도 가능하다. 단, 버퍼 limit 인 16MB 안에서만 가능하니, 대량 삽입시 삽입되는 전체 Document 사이즈 합이 16MB 이하가 되도록 조정 해야한다.

 

 

 

참고 : https://docs.mongodb.com/manual/

반응형