Search
▪️

Working with NoSQL & Using MongoDB

What is MongoDB?

MongoDB에서 쓰이는 Document의 모습은 JSON 데이터와 유사한 모습을 보인다. 이는 실제로 MongoDB에서 데이터를 저장할 때 JSON으로 Collection을 저장한다.
조금 더 정확하게 MongoDB는 Binary JavaScript Object Notion이라는 BSON이라는 타입을 이용하여 데이터를 저장한다. 파일로 저장하기 전에 Scene 뒤에서 해당 타입으로 변환하여 저장한다.
Document에는 Array를 사용할 수 있고, 해당 Array는 또 다른 Document나 Object를 가질 수 있다.
Nested된 구조를 갖고 있기 때문에 Relation에 대해서 관리하는 방식이 SQL과는 조금 다르다.

Relations in NoSQL

SQL에서 Relation에 대해서 정의 했던 것과 달리, NoSQL에서는 Collection에 대해서 Multiple Combine을 하지 않아도 되지만 강제로 Multiple Combine 시킬 수 있다.
NoSQL에서 강제로 Multiple Combine을 하는 작업이 대체로 안 좋지만, 중복된 데이터가 너무 많이 나타나고 이런 것들이 반복되는 구조라면 어쩔 수 없이 해야 하는 경우가 분명 있기도 하다.

Setting Up MongoDB & Installing the MongoDB Driver

Free Plan인 M0로 Cluster를 생성한 후, Database Access 가능한 유저를 최소 한 명은 두어야 한다. 해당 유저의 비밀번호 설정 및 readWriteAnyDatabase로 설정이 필요하다.
데이터베이스 접근 권한 유저에 대해서 설정했다면, Network Access를 통해 접근하고자 하는 IP Address를 White List에 등록해야 한다. (Node.js 서버가 돌아가는 서버 주소를 등록해야 해당 서버에서 MongoDB로의 접근이 가능하다.)
설정이 끝났다면 MongoDB를 Node.js의 Application과 Connect가 필요하다. (Connect to Application 시 Database Access에서 설정했던 User와 Password가 모두 필요하다.)
Node.js에서도 Third Party Package 설치가 필요하다. 아래 명령어로 설치가 가능하다.
npm install —save mongodb
mongodb에 MongoDB Third Party Package를 Import한 뒤, Client Instance인 mongodb.MongoClient를 변수에 할당한다. 설정했던 MongoDB Server에 접근하기 위해서, Client를 Connect 해주어야 하므로 MongoClient.connect() Method로 Connect한다.
connect() Method의 인자로 설정 때 받은 Connect to Application의 URL을 준다. (URL의 Username과 Password를 올바르게 설정한다. mongodb.net 바로 뒤에 오는 Path의 경우 데이터베이스 이름을 의미한다. connect() Method의 결과로 받은 client의 .db() Method 이용 시 Path에 기록된 데이터베이스를 반환하고, .db() Method의 인자를 따로 주면 Path에 기록했던 데이터베이스와 무관하게 인자로 주어진 이름의 데이터베이스를 반환하게 된다.)
connect() Method를 Anonymous Callback Function에 담아, 해당 함수를 변수에 담아서 app.js에서 활용한다.
Anonymous Callback Function의 인자로는 또 다른 Callback Function을 받는다. connect() Method를 통해 client값을 받으면 해당 값을 then내에서 처리할 때, 인자로 받은 Callback Function의 인자로 넘겨준다.
MongoDB의 경우 값을 쓰려고 데이터베이스를 봤는데, 데이터베이스가 존재하지 않으면 자동으로 해당 데이터베이스를 생성하여 값을 쓰게 된다.

Using the Database Connection

db.collection() Method를 통해, 데이터를 삽입할 때와 작업할 때 데이터베이스의 어떤 Collection을 사용할 것인지 정할 수 있다.
collection()을 통해서 불러왔을 때, 어떤 Method들을 추가로 수행할 수 있는지 궁금하다면 Docs를 방문하여 직접 확인하면 된다. (MongoDB Server CRUD Operations)
데이터 하나를 삽입 해야할 때는 insertOne() Method를, 데이터 여러 개를 삽입 해야할 때는 insertMany() Method를 이용한다. insertMany() Method 이용 시에 인자는 Array Type으로 받게 된다.
데이터를 삽입 했을 때 흥미로운 점은 삽입된 데이터의 ID는 MongoDB에 의해 자동으로 생성되고 자동으로 관리 된다는 것이다.
** MySQL에서 Workbench가 있었다면, MongoDB에는 Compass가 있다.

Fetching All Products

db.collection() Method를 통해서 Collection을 받아서 왔다면, 해당 Collection내에서 데이터를 불러오는 것은 find() Method를 통해서 할 수 있다. find() Method 안에 인자로 주는 JSON 데이터가 Filter처럼 작용하게 된다.
Node.js에서 데이터를 활용하기 위해서 Promise Type의 데이터로 받아야 하지만, find() Method를 통해서 받은 데이터는 Promise Type이 아닌 Cursor이다.
Cursor는 MongoDB에서 제공하는 Object이다. 해당 타입은 Collection내에 존재하는 Document의 각 Element에 대해 Step by Step으로 접근할 수 있게 해준다.
Cursor를 두는 이유는 다음과 같다. 사실 find()라고 하는 Method는 수백만의 데이터에 대해서도 접근이 가능하다. 하지만 단순히 Wire를 통해서 이렇게 많은 데이터를 한 번에 반환하여 보내게 되면 문제가 발생할 수도 있다. 따라서 이를 처리하기 위해 find()는 MongoDB에게 하나의 데이터를 받으면, 그 다음 데이터를 받고, 또 그 다음 데이터를 받을 수 있도록 Cursor을 조절해준다.
이렇게 find() Method의 Cursor라는 Object를 이용하여 순차적으로 Document를 받게 되면 두 가지 선택지가 있다. 모든 Document를 한 번에 가져올 것인지, Pagination을 할 것인지 말이다. 만약 데이터의 개수가 100개가 넘어가게 된다면 별도의 Pagination을 처리하여 JavaScript Array로 가져오는 것이 좋고 그렇지 않다면 toArray() Method를 통해서 한 번에 JavaScript Array로 가져오는 것이 좋다.
** 그렇다면 MySQL과 같은 SQL에서는 Cursor와 같은 것이 필요 없는가?... 싶다.

Fetching a Single Product

Single Item을 가져오려고 할 때, 일반적으로 ID 값을 통해서 찾아오게 된다. 따라서 아무런 처리 없이 ID를 인자로 받아 find() Method를 통해 ID에 매칭되는 항목을 찾으려고 하면 아무런 항목도 찾아올 수 없다.
이유는 다음과 같다. ID값은 MongoDB에서 자동으로 생성 해주는 값이었다. 이 ID의 Type은 자세히 살펴보면, ObjectId Type의 값이다. 즉, ID 값을 MongoDB의 ID 값과 비교하려고 하면 비교 자체가 불가능하여 항목을 찾을 수 없는 것이다. (ObjectId Type은 일종의 BSON이다. JSON과 비교했을 때, BSON을 사용하려고 하면 BSON이 조금 더 빠르기 때문에 그대로 이용하지 않는다. 하지만 MongoDB에서는 BSON의 사용을 허용하고 있고, 이에 대해서 별도의 처리가 필요하다.)
별도의 처리를 위해 mongodb Third Party Package를 Import하면, 해당 Package를 ObjectId Type을 접근하는데 이용할 수 있다. new mongodb.ObjectId($param)과 같이 이용하면 ObjectId Type에 접근 가능하다.
단일 값을 find() Method로 찾으려고 하면 find().next()와 같이 사용하는 방법도 있지만 이는 Cursor를 Return하므로 next() Method가 필요하기 때문에 findOne() Method 처럼 Element를 Return하는 Method를 사용하는 방법도 있다.

Working on the Product Model to Edit our Product

데이터베이스에 존재하는 데이터를 Update하기 위한 Method로는 updateOne(), updateMany() 두 가지가 있다. 한 개에 대해서 Update 수행하기 위해선 전자, 여러 개에 대해서 Update 수행하기 위해선 후자를 이용한다.
updateOne() Method의 경우 최소 두 개의 인자를 받는다. 첫 번째 인자는 여러 데이터들 중 Filter하여 Update할 Document를 찾아낼 인자이다. 해당 인자는 JSON 형태로 Filter를 주면 된다. 두 번째 인자는 어떻게 Update할지 정하는 인자이다. 해당 인자 역시 this를 JSON 형태로 넘기게 된다. (JSON 형태로 넘기지 않으면 Update 되지 않는다.)
두 번째 인자는 다음과 같이 두 방법으로 넘겨줄 수 있다. (Filter된 항목에 대해서, 두 번째 인자를 통해 자동으로 Field를 매칭하여 업데이트 하게 된다.)
{$set: this}
{$set: {$var1 : this.$var1, $var2 : this.$var2}}

Deleting Products

삭제를 위한 Method 역시 Insert, Update와 유사하다. Filter 인자를 JSON 형태로 줄 수 있다.

Storing Multiple Products in the Cart

JavaScript에서 이해할 수 있는 데이터 타입에 대한 비교가 아니라면 ==으로 비교를 하든지, 아니면 해당 타입을 toString()으로 변환하여 ===으로 비교를 하든지 하면 된다.

Displaying the Cart Items

find() Method로 Filter할 때, 여러 값들에 대해서 비교가 필요한 경우 {}와 같이 Object로 묶어서 '$in'이라는 Key 값으로 []와 같은 Array Value를 묶어서 보내면 된다.
Collection에 대한 find() Method와 Array에 대한 find() Method에 대해서 헷갈리지 않아야 한다. Collection에 대한 find() Method는 Cursor로 동작하여 여러 값들에 대해서 동작하지만, Array의 find() Method는 Iterator로 돌면서 한 개의 값을 찾으면 바로 Return 하게 된다.

Deleting Cart Items

삭제의 경우 파일로 하든 SQL로 하든 그냥 filter() Method를 통해서, 기존에 존재하던 항목들을 거른 뒤 Update하는 것이 편하다. (filter() Method는 Vanilla JavaScript에 존재하는 Method이다.)

Adding Relational Order Data

Cart의 경우 Product의 가격이나 정보가 바뀌면 수정이 되어야 한다. 반대로 Order의 경우, 현재의 정보가 바뀐다고 해서 과거의 정보가 바뀌어선 안 된다.
즉, Cart의 경우 Product의 ID값을 이용하여 불러오는 식으로, 현재 Product의 정보가 반영될 수 있도록 해야 한다. 반대로 Order의 경우 Product ID 값을 저장하여 갖고 있는 Cart를 그대로 쓰게 되면, 참조 방식을 그대로 쓰는 것이기 때문에 Product 정보가 바뀜에 따라 Cart내의 정보도 바뀔 여지가 있다. 따라서 Cart처럼 참조 값으로 항목들을 접근하는 방식이 아니라 참조 값으로 항목들의 값을 따와서 해당 항목들의 값을 저장하는 Snapshot으로 저장해야 한다.
비록 중복된 값을 저장하기는 하지만 바뀌어서는 안 되는 정보이므로 이렇게 활용한다.

Getting Orders

find() Method의 특이한 성질이 하나 더 있다. 특정 필드에 대해서 Filter를 할 때는 Quotation Mark 없이 필드명을 기입했지만, 만일 특정 필드가 Nested 되어서 또 다른 필드명을 갖고 있다면 Nested된 필드에 대해서도 Filter를 할 수 있다. 이렇게 할 때는, 이전과 다르게 Quotation Mark를 반드시 붙여서 이용한다.
예를 들면, 이전에 _id값에 대해서 Filter를 해야 했다면, find({_id : $id})와 같이 수행했었다. 하지만 user라는 필드에 대해서 user가 갖는 _id로 Filter를 하고 싶다면, find({'user._id' : $id})와 같이 수행할 수 있다.

Removing Deleted Items From the Cart

Product가 사라졌을 때, Cart에서도 해당 상품을 삭제해줘야 한다. 물론 Product를 삭제 했을 때, Cart는 Product의 ID 값을 통해서 View에 정보를 뿌려주므로 Product가 없다면 Cart에 보이는 것은 문제가 없다. 하지만 데이터베이스에는 여전히 Product에 대한 ID 값을 Cart가 가지고 있다. (Cart가 비어 있는데도 말이다.)
이에 대한 해결책으로는 두 가지가 있다. 24시간에 1회 정도, 특정 시간을 설정 해두고 Cart와 Product를 비교 했을 때 존재하지 않는 상품이 Cart에 있다면 Cart를 비워버리는 것이다. 또 다른 방법으로는, Cart 페이지를 Load할 때, Cart의 항목들을 Fetch하게 되고 해당 항목들을 토대로 Product를 Fetch하므로 두 항목에 대해서 Mis-Match가 일어날 때 Cart를 비우는 방법도 있다.