NoSQL (1)

On a regular basis·2021년 12월 1일
0

NoSQL은 Not Only SQL, SQL 뿐만 아니다라는 의미를 지니고있다. 즉, SQL을 사용하는 관계형 데이터베이스가 아닌 데이터베이스를 의미. 대표적인 관계형 데이터베이스로는 MySQL, Oracle, PostgreSQL이 있고, NoSQL 진영에는 이 포스트에서 다루는 MongoDB와 Redis, HBase 등이 있다.

그래서 NoSQL은 왜 탄생하게 된걸까? 사실 RDBMS만으로 충분하지 않을까? 하지만 RDBMS은 분명한 한계점이 있다. NoSQL은 다음과 같이 RDBMS에선 하기 힘든 일을 쉽게 지원한다.

  • 수평 확장 가능한 분산 시스템
  • Schema-less
  • 완화된 ACID

RDBMS vs NoSQL

마치 RDBMS에 수평 확장이 불가능한 것 처럼 써놨지만 MySQL Replication이나 MySQL Cluster가 존재하여 수평 확장이 불가능한 것은 아니다. 그리고 NoSQL에서도 ACID가 불가능하지 않다. MongoDB의 경우 분산 트랜젝션까지도 지원하고 있다. 단, NoSQL 데이터베이스는 대게 분산 아키텍처를 염두하고 출시된 제품이 많아 더 편리하다는 장점이 있고 BASE 기반이기 때문에 완전한 ACID가 아니다. 점점 서로의 장점을 흡수하고 있기 때문에 위 표는 참고 정도로만 보면 될 것 같음!

그래서 MongoDB가 무엇?

MongoDB

  • Data를 table이 아닌 문서처럼 저장하는 document 형식의 대표적인 database!!
  • MongoDB는 앞서 설명한 것 처럼 NoSQL 데이터베이스고 다음 세 가지 특징을 가지고있다.
    1) Document
    2) BASE
    3) Open Source

데이터는 Document 기반으로 구성되어있고, ACID 대신 BASE를 택하여 성능과 가용성을 우선시한다. 그리고 오픈 소스라는 점 덕분에 무료로 이용이 가능하다.

Document

MongoDB는 Document 기반 데이터베이스다. Database > Collection > Document > Field 계층으로 이루어져 있으며 Document는 RDBMS의 Row에 해당한다. 계층은 RDBMS와 유사하다.

흥미로운 점은 Document 기반 데이터베이스은 RDBMS와 다르게 자유로이 데이터 구조를 잡을 수 있다는 점이다. MongoDB는 BSON으로 데이터가 쌓이기 때문에 Array 데이터나 Nested한 데이터를 쉽게 넣을 수 있다.

위 데이터 구조에서 ObjectId라는 생소한 타입을 볼 수 있다. ObjectId는 RDBMS의 Primary Key와 같이 고유한 키를 의미하는데 차이점은 Primary Key는 DBMS가 직접 부여한다면 ObjectId는 클라이언트에서 생성한다는 점이다. 이는 MongoDB 클러스터에서 Sharding된 데이터를 빠르게 가져오기 위함인데 Router(mongos)는 ObjectId를 보고 데이터가 존재하는 Shard에서 데이터를 요청할 수 있다. 의아하게도 MongoDB 서버에서 알아서 ObjectId를 부여해서 저장해도 될 것 같은데 딱히 지원해주지 않는다. 참고로 ObjectId를 넣지않고 저장한다면 데이터가 그대로 저장된다.

ObjectId는 세 영역으로 나눠져있다. 각각 첫 4 byte는 UNIX Timestamp 정보를 담고있고 다음 5 byte는 랜덤한 값으로 이루어져 있는데 3 byte와 2 byte로 나뉜다. 첫 3 byte는 클라이언트의 머신별로 고유한 키(mac 주소나 ip 주소)를 이용하여 랜덤 값을 만들어 사용한다. 다음 2 byte는 process id를 이용한다. 5 byte를 채운 후 마지막 2 byte는 Auto Increment되는 값으로 구성된다.

이쯤되면 ObjectId가 충돌날 가능성이 어느 정도일지 궁금할 수 있다. 충돌이 발생하려면 같은 시간, 기기에서 만들어낸 해시 값이 일치하고 우연히 같은 process id를 가지고 있으며 정말 우연히 increase된 count가 일치해야 한다. 확률은 계산해보지 않았지만 거의 충돌날 일은 없을 것 같다.

다음으로 MongoDB 데이터 조작에 대해서 알아보자. MongoDB와 같은 NoSQL은 이름처럼 SQL을 사용하지 않고 별도로 제공하는 API를 통해 데이터를 건들 수 있다. MongoDB의 경우 자바스크립트 엔진 SpiderMonkey를 사용하여 API를 제공한다. 따라서 자바스크립트를 조금은 알아야한다.

데이터를 삽입하는 쿼리를 보면 SQL과는 모습이 많이 다른 것을 알 수 있다. 마치 클래스에서 메서드를 통해 실행하는 모습인데, 이처럼 MongoDB는 객체 조작을 통해 데이터를 관리할 수 있다.

BASE

BASE는 ACID와 대립되는 개념으로 다음 세 가지로 이루어져있다.

Basically Avaliable
기본적으로 언제든지 사용할 수 있다는 의미를 가지고 있다.
즉, 가용성이 필요하다는 뜻을 가진다.
Soft state
외부의 개입이 없어도 정보가 변경될 수 있다는 의미를 가지고 있다.
네트워크 파티션 등 문제가 발생되어 일관성(Consistency)이 유지되지 않는 경우 일관성을 위해 데이터를 자동으로 수정한다.
Eventually consistent
일시적으로 일관적이지 않은 상태가 되어도 일정 시간 후 일관적인 상태가 되어야한다는 의미를 가지고 있다.
장애 발생시 일관성을 유지하기 위한 이벤트를 발생시킨다.
이처럼 BASE는 ACID와는 다르게 일관성을 어느정도 포기하고 가용성을 우선시한다. 즉, 데이터가 조금 맞지 않더라도 일단 내려준다는 뜻이다.

참고로 굳이 왜 Basically Avaliable이나 Eventually consistent처럼 어렵게 표현했는지 의아했는데 Acid(산)과 대립되는 느낌을 주기 위해 억지로 Base(염기)로 맞췄다는 소리를 들었다. 물론 진짜인진 모르겠지만 꽤 재밌는 이야기라고 생각한다.

ACID?
마치 MongoDB는 전혀 ACID하지 않다는 식으로 글을 썼지만 사실 MongoDB는 트랜젝션을 제공한다. 아직 ACID하지 않을 때도 Single-Document Transaction을 제공하고 MongoDB 4.0부터는 Multi-Document Transaction을 제공함으로서 ACID를 충족했다. 이후 MongoDB 4.2에서 Shard Cluster Transacion을 제공하면서 분산 트랜젝션까지 가능해졌다.

MongoDB는 분산 시스템이 핵심!! ✔️

웹 서비스가 발전하면서 데이터 무결성을 버리면서까지 더 많은 데이터, 빠른 성능, 수평 확장이 필요한 데이터베이스가 필요해졌다. 그런 요구 사항으로 인해 MongoDB가 탄생했다.

MongoDB 패턴

Model Tree Structure
같은 Collection에서 데이터가 서로를 참조하는 Tree 구조를 가지고 있을 때 사용할 수 있는 패턴은 다섯가지가 있다. 소개하는 모든 패턴은 아래 트리 구조를 참고하여 구성했다.

Parent References
Parent References는 다음과 같은 구조를 가진다.

[
  { _id: "MongoDB", parent: "Databases" },
  { _id: "dbm", parent: "Databases" },
  { _id: "Databases", parent: "Programming" },
  { _id: "Languages", parent: "Programming" },
  { _id: "Programming", parent: "Books" },
  { _id: "Books", parent: null }
]

부모 Document를 바로 찾아야 하는 경우 적합하다. 만약 하위 트리를 모두 찾아야하는 경우엔 적합하지 않다.

Child References
Child References는 다음과 같은 구조를 가진다.

[
  { _id: "MongoDB", children: [] },
  { _id: "dbm", children: [] },
  { _id: "Databases", children: [ "MongoDB", "dbm" ] },
  { _id: "Languages", children: [] },
  { _id: "Programming", children: [ "Databases", "Languages" ] },
  { _id: "Books", children: [ "Programming" ] }
]

자식 Document를 바로 찾아야하는 경우 적합하다. 부모 Document도 찾을 수 있지만 Parent References보다 탐색 성능이 느리다.

Array of Ancestors
Array of Ancestors는 다음과 같은 구조를 가진다.

[
  { _id: "MongoDB", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" },
  { _id: "dbm", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" },
  { _id: "Databases", ancestors: [ "Books", "Programming" ], parent: "Programming" },
  { _id: "Languages", ancestors: [ "Books", "Programming" ], parent: "Programming" },
  { _id: "Programming", ancestors: [ "Books" ], parent: "Books" },
  { _id: "Books", ancestors: [ ], parent: null }
]

조상 Document를 바로 알 수 있어야하는 경우와 자식 Document를 모두 찾아야 하는 경우 적합하다. Breadcrumb 등에 쓸 수 있다. 만약 여러 부모 Document를 가진 경우 적합하지 않다.

Materialized Paths
Materialized Paths는 다음과 같은 구조를 가진다.

[
  { _id: "Books", path: null },
  { _id: "Programming", path: ",Books," },
  { _id: "Databases", path: ",Books,Programming," },
  { _id: "Languages", path: ",Books,Programming," },
  { _id: "MongoDB", path: ",Books,Programming,Databases," },
  { _id: "dbm", path: ",Books,Programming,Databases," }
]

Array of Ancestors와 유사하다. Array 타입이 아닌 String 타입을 이용하는데 정규식을 이용하여 하위 항목을 찾을 수 있다. 이때 하위 트리를 찾는데에 Array of Ancestors보다 빠르다. 단, 공통 부모를 찾아야 하는 경우엔 더 느려질 수 있다.

Nested Sets
Nested Sets은 조금 특이한 구조를 가진다. 아래 그림의 번호를 참고하여 구조를 살펴보자.

[
  { _id: "Books", parent: 0, left: 1, right: 12 },
  { _id: "Programming", parent: "Books", left: 2, right: 11 },
  { _id: "Languages", parent: "Programming", left: 3, right: 4 },
  { _id: "Databases", parent: "Programming", left: 5, right: 10 },
  { _id: "MongoDB", parent: "Databases", left: 6, right: 7 },
  { _id: "dbm", parent: "Databases", left: 8, right: 9 }
]

하위 트리를 찾는데 가장 빠르고 효율적이다. 하지만 구조가 변경되는 경우 다시 데이터 번호를 매기는데 비용이 크기 때문에 데이터가 추가, 삭제, 변경되지 않는 정적인 구조에 적합하다.

CRUD in MongoDB

Create

db.collection_name.insert({data1},{data2}...)

** 여러 개의 데이터를 삽입할 경우 객체를 요소로 갖는 배열을 넣어주자. index 순으로 데이터가 삽입되며, ordered 옵션을 추가하면 삽입 순서를 변경할 수 있음.

** {"ordered": true} 일 경우, 배열 안의 index 순으로 작업이 수행됨. _id 필드 값이 같을 경우, duplicate error 발생.

Read

db.collection_name.find({query1, query2...})
// 데이터 중 20개만 무작위로 뽑아서 렌더링 
db.collection_name.findOne({"_id":ObjectId("")}) // 특정 데이터만 조회.

db.collection_name.find() // 전체 데이터 탐색

.pretty() // 보기 편하게 정렬
.count() // 데이터 개수 반환 

Update

db.collection_name.updateMany({query1}, {"$inc":{"<field>":<increment value1>,
	<field>":<increment value2>, ...}})
// $inc 연산자를 통해 다수의 필드 값을 동시에 업데이트

db.zips.updateOne({"zip":"12534"},{"$set":{"pop":6235}}) 
// $set 연산자를 통해 단일 필드 값만 업데이트

db.grades.updateOne({"student_id":250, "class_id":339}, {"$push":{"type":"extra credit", "score":100}}}) 
// $push 연산자를 통해 배열로 이뤄진 필드 값에 요소를 추가 

** field 잘못 입력할 경우, MongoDB는 새로운 데이터가 추가되는 줄 알고 업데이트하므로 주의할 것. 

Delete

db.inspections.deleteMany({query})
db.inspections.deleteOne({query})

db.collection_name.drop() // collection 삭제

MongoDB와 RDBMS는 적합한 사용처가 다르다. 내 개인적인 생각으론 MongoDB를 비롯한 NoSQL은 최대한 단순하게 사용하는 것이 옳은 방향이라고 생각한다. NoSQL은 최대한 단순하면서 많은 데이터, RDBMS는 복잡하면서 무결성이 중요한 데이터에 적합하다고 생각한다.

오늘은 요기까지!
mongoDB...^^
SQL과 NoSQL 차이를 개념적으로만 알고있었는데 이제는 정말 실제로 사용해볼 때가 온 것 같다. SQL 쿼리랑 비슷하면서도 비슷하지않아서 어려우면서도 어렵지 않은 것 같다? 🥰 아직은 조금 낯설지만 오늘 이렇게 정리했으니 주말 활용해서 실제로 mongoDB를 다뤄봐야겠땅...💟 그럼 mongoDB랑 쫌 더 친해져있겠지~~~

참조1: https://kciter.so/posts/about-mongodb
참조2: https://velog.io/@96hxx_/MongoDB-%EA%B8%B0%EB%B3%B8

참고: https://velopert.com/436

profile
개발 기록

0개의 댓글