[MongoDB in Action] CH4. 도큐먼트 지향 데이터
이 내용은 몽고디비 인 액션 2nd Edition 책을 읽고 개인적으로 정리한 내용입니다.
이 장에서는 이커머스 데이터 모델을 예시로 스키마를 설계하는 방법을 알아보며, 몽고DB가 데이터베이스, 컬렉션, 도큐먼트 단위로 어떻게 구성되는지 살펴본다.
스키마 설계 원리
RDBMS 에서는 정규화된 데이터 모델을 권장한다. 일반적인 질의성을 보장하고 데이터 무결성을 지키기 때문이다. 하지만 스키마 설계는 관계 데이터베이스에서조차 FIXED 된 규칙은 아니다. 애플리케이션 기능과 성능에 따라서 예외가 발생할 수도 있다.
몽고DB는 “이론에 따라 스키마를 설계해야 하지만, 실제에서는 이론을 융통성 있게 적용해야 한다”라는 전제를 바탕으로 한다.
- 애플리케이션의 요구사항이 무엇인지 정확히 파악해서 스키마 설계를 하자
- 여러가지 질문을 해야하는데, 읽기/쓰기 비율은 어떻게 되는가? 쿼리는 키를 찾는 정도로 쉬운지 복잡한지, 집계는 필요한지? 데이터는 얼마나 저장이되는지?
- 데이터베이스에 어떤 기능이 있는가?
- 간단한 키-값 저장시스템은 하나의 키에 대해서만 값을 가져올 수 있는 반면에 RDBMS는 조인이라는 특징이 있다. 또한, 트랜잭션에 대한 원자성, 롤백을 고려해서 데이터베이스를 선택해야 한다. (MongoDB는 트랜잭션을 지원하지는 않는다.)
- Unique Id 와 PK를 무엇으로 만드는가?
- PK나 고유식별자를 고르는 것은 데이터를 접근하는 방식과 저장하는 방식에서 큰 차이를 만들어 낼 수 있다.
_id
를 어떤 값으로 할지 고민해야된다. 추후 샤딩을 이용할 때, 특정 행이 저장될 곳을 결정하기 때문이다.
스키마 변경하는 것에 대해 두려움을 갖지 않았으면 한다. 여러 경험으로부터 좋은설계가 나오기 때문이다.
이커머스 데이터 모델 설계
상품, 카테고리, 상품평, 주문 데이터를 몽고 컬렉션으로 어떻게 구성하는지 예시를 들면서 알아보고 그 과정에서 스키마와 도큐먼드 관계를 알아본다.
상품 도큐먼트
doc =
{ _id: new ObjectId("4c4b1476238d3b4dd5003981"),
slug: "wheel-barrow-9092",
sku: "9092",
name: "Extra Large Wheel Barrow",
description: "Heavy duty wheel barrow...",
details: {
weight: 47,
weight_units: "lbs",
model_num: 4039283402,
manufacturer: "Acme",
color: "Green"
},
total_reviews: 4,
average_review: 4.5,
pricing: {
retail: 589700,
sale: 489700,
},
price_history: [
{retail: 529700,
sale: 429700,
start: new Date(2010, 4, 1),
end: new Date(2010, 4, 8)
},
{retail: 529700,
sale: 529700,
start: new Date(2010, 4, 9),
end: new Date(2010, 4, 16)
},
],
category_ids: [new ObjectId("6a5b1476238d3b4dd5000048"),
new ObjectId("6a5b1476238d3b4dd5000049")],
main_cat_id: new ObjectId("6a5b1476238d3b4dd5000048"),
tags: ["tools", "gardening", "soil"],
}
- _id (고유 객체 ID)
- slug (고유 슬러그)
- details (중첩 도큐먼트)
- price_history(일대다 관계) : 배열로 저장 > 배열의 키에 대해 인덱스를 만들 수 있음
- category_ids (다대다 관계) : 서브도큐먼트를 가리킨다.
주문 도큐먼트
{ _id: ObjectId("6a5b1476238d3b4dd5000048"),
user_id: ObjectId("4c4b1476238d3b4dd5000001"),
purchase_data: new Date(),
state: "CART",
line_items: [
{ _id: ObjectId("4c4b1476238d3b4dd5003981"),
sku: "9092",
name: "Extra Large Wheel Barrow",
quantity: 1,
pricing: {
retail: 5897,
sale: 4897,
}
},
{ _id: ObjectId("4c4b1476238d3b4dd5003981"),
sku: "10027",
name: "Rubberized Work Glove, Black",
quantity: 2,
pricing: {
retail: 1499,
sale: 1299,
}
}
],
shipping_address: {
street: "588 5th Street",
city: "Brooklyn",
state: "NY",
zip: 11215
},
sub_total: 6196
}
- line_items : 비정규화된 상품정보
- sub_total : 비정규화된 세일 가격의 합
user_id는 user 컬렉션 _id 값을 가지고 있으며 이걸 통해 user 컬렉션과의 조인이 가능하다.
또 주의할 점은 MongoDB는 도큐먼트 내의 배열의 크기를 질의하는 것을 허용하지 않기 때문에, 집계된 값을 필드로 저장하는 것을 고려해야 한다.
데이터베이스, 컬렉션, 도큐먼트에 대한 세부사항
데이터베이스
컬렉션과 인덱스의 물리적인 모음이며, 동시에 네임스페이스 이다. 우리가 아래와 같이 데이터베이스를 사용한다고 명령을 하면, 그 때 데이터베이스는 아직 생성되지 않은 상태다.
use garden
데이터베이스에 컬렉션을 쓴 후에야 데이터 파일이 만들어진다. 그 때, garden
이라는 데이터베이스가 디스크에 할당 된다.
db를 없애기 위해서는 db.dropDatabase()
명령어를 이용하는데, 이 때 관련된 파일들을 디스크로부터 함께 지우게 되므로 취소할 수 없다.
데이터 파일과 할당
데이터베이스가 생성될 때 MongoDB는 몇 가지 데이터 파일을 디스크에 할당한다. 모든 컬렉션, 인덱스, 데이터베이스에 대한 메타데이터가 저장된다.
보통 /data/db 에 저장되는데, 나 같은 경우는 config 파일을 찾아서 경로를 봐줬어야 했다. 경로에 보면 책에서 설명한거랑 조금 다른 파일들이 생성되었는데, 컬렉션 이름이 아닌 뭔가 id 같이 보이는 이름으로 파일이 저장되어있다. 이러한 파일들은 환경에 따라 달라질 수 있다고 한다.
- mongod.lock : 서버의 프로세스ID를 저장한다. 절대 지우면 안되는 파일이다. ❌
- mongod를 시작한 후 lock 파일에 대한 에러 메시지를 본다면 비정상적으로 셧다운된 후에 다시 부팅되었을 가능성이 높으므로 복구 프로세스를 시작해야 한다. (자세한건 11장에서)
- garden.ns : 나같은 경우엔 ns 파일이 생기진 않았지만, namespace를 뜻한다. 데이터베이스 내의 각각의 컬렉션과 인덱스에 대한 메타데이터는 자신만의 네임스페이스 파일을 가지며, 해시 테이블로 구성되어 있다.
- 16MB로 고정되어 있어, 약 26,000개의 컬렉션을 저장할 수 있다.
- 이보다 더 많이 필요할 경우에는 mongod를 시작할 때
--nsize
옵션을 사용하면 된다. - ✅ v3.0 이후 부터는 WiredTiger 엔진을 사용해서 위와 같은 제한을 받지 않는다고 한다
쉘에서 stats
명령어를 사용해서 사용하는 공간과 할당된 공간을 확인할 수 있다.
{ db: 'admin',
collections: 1,
views: 0,
objects: 2,
avgObjSize: 382,
dataSize: 764,
storageSize: 36864,
numExtents: 0,
indexes: 1,
indexSize: 32768,
fsUsedSize: 186570412032,
fsTotalSize: 202949259264,
ok: 1 }
- dataSize : 데이터베이스에서 BSON 객체의 실제 크기
- storageSize : 컬렉션이 증가할 것을 대비한 여분의 공간과 삭제되었지만 아직 할당되지 않은 공간
- indexSize : 데이터베이스 인덱스의 전체 크기. 모든 인덱스가 램에 있을 때 데이터베이스가 성능이 최적화되므로 이 값을 주의해서 봐야 한다. (7장 10장)
위의 정보로 MongoDB가 어느정도의 디스크 공간과 RAM이 필요한지 가늠해볼 수 있다.
컬렉션
일반 컬렉션
db.createCollection("users")
db.createCollection("users", {size:20000}) // 크기를 미리할당한 컬렉션
db의 이름 + 컬렉션의 이름은 128자 이내여야 한다.
Capped 컬렉션
더 이상의 공간이 없게 되면 도큐먼트를 insert 할 때, 컬렉션에 추가된지 가장 오래된 도큐먼트를 덮어쓰게 된다. 따라서 로깅 기능을 위해 설계된 기능이다.
캡드 컬렉션에 대해 최대 크기나 최대 개수를 지정할 수 있다. 대신 크기설정이 우선순위가 있다.
db.createCollection("user.actions", {capped: true, size: 16384, max 100})
다만, 캡드 컬렉션은 일반적인 보통 컬렉션의 연산이 불가하다. 삭제할 수 없고 업데이트도 안된다.
TTL 컬렉션
특정 필드에 index를 추가할 시, TTL을 설정하면 해당 도큐먼트가 삭제된다.
시스템 컬렉션
MongoDB 내부에서 시스템 컬렉션을 사용하는데, 이 중 특별한게 system.namespaces
와 system.indexes
를 들 수 있다. 모두 표준 컬렉션이다. oplog.rs
라는 특별한 캡드 컬렉션을 시스템 컬렉션으로 사용한다. replicaset이 있을 때 복제를 이 컬렉션에 모든 쓰기에 대한 기록을 남기고 세컨더리 노드가 이 컬렉션에서 순차적으로 읽어 자기 자신에 대해 적용한다.
도큐먼트와 인서트
모든 도큐먼트는 저장하기 전에 BSON으로 serialize되고, 나중에 deserialize 된다. 드라이버는 이러한 과정을 처리하고, 그 프로그래밍 언어를 통해 적절한 데이터 타입으로 변환하게 된다.
Serialize 와 deserialize를 위해서는 여러 제약이 있는데, Key 이름이 유효해야 하고, Key의 값이 BSON 타입으로 변환할 수 있어야 한다. 또한, 이 Key 이름은 최대로 허용되는 길이가 255 바이트인 문자열이어야 한다. ASCII면 다 되지만 세 가지 예외가 있다.
- $
- .
- 마지막 위치 외에 null 허용하지 않음
이 Key를 도큐먼트에 매번 저장이 되므로, 키의 이름으로 date_of_birth 대신 dob를 쓰면 도큐먼트당 10바이트를 절약하게 된다.
BSON 타입에서의 문자열
모든 문자열은 UTF-8이어야 한다.
BSON 타입에서의 숫자
double, int, long 의 세 가지 수 타입을 규정한다. int를 serialize 할 때, 드라이버는int나 long 타입을 자동적으로 결정한다. 자바스크립트는 Number라는 수 타입을 하나만 가지고 있는데, 이것은 Double에 해당한다. 따라서 셸을 통해 정숫값으로 저장하고 싶다면 NumberLong()
이나 NumberInt()
를 써서 정수라는 것을 명시해주어야 한다.
db.numbers.save({n: NumberLong(5)});
컬렉션의 필드가 어떤 타입인지 확인하려면 쿼리 파라미터로 $type
을 주면 된다.
BSON 날짜와 시간
datetime 타입은 날짜와 시간을 저장한다. 자바스크립트에서 날짜를 생성하는 경우 월을 표현할 때 0부터 시작한다. new Date(2011, 5, 11)
은 2011년 6월 11을 나타낸다.
도큐먼트 크기에 대한 제약
MongoDB v2.0과 그 이후 버전에서 도큐먼트의 최대 크기는 16MB이다. 이 제한은 다음과 같은 2가지 이유 때문이다.
- 개발자가 효율적이지 못한 데이터 모델을 생성할 수도 있음
- 성능
- 코어 서버에서 크기가 큰 도큐먼트에 질의하면 질의 결과를 클라이언트로 보내기 전에 버퍼에 복사되어야 한다.
- 또한, Seriazlie, Desrialize를 하므로 크기가 너무 크면 부하가 생긴다.
도큐먼트의 최대 중첩 깊이는 100으로 제한되어 있다.
만약 위의 제약사항에 걸린다면 다시 모델링을 하는게 옳다.
대량 삽입 연산
insertMany
로 대량으로 데이터 삽입이 가능한데, 건 by 건이 아니라 서버로 단 한번의 왕복 통신을 한다.
단, 어떠한 연산도 16MB이상은 허용되지 않는다.
댓글남기기