Medium - An Overview of MongoDB & Mongoose
in Trend
Trend 파악을 Medium 기고문 요약 포스팅 - MongoDB와 몽구스 개요, 몽고 산타마리아!
A Little Historical Context
데이터베이스는 여러개의 구분으로 나뉘게 되었습니다. 1980년대 중반까지 초기 데이터 베이스는 CODASYL이라고 불렸습니다. 이런 레코드 속의 정리된 데이터와 다른 타입의 레코드들은 해쉬를 사용하여 네트워크를 구성했습니다. 대개 Cullinet Software의 IDMS나 IBM의 IMS처럼 CODASYL 스타일의 데이터베이스가 효율적이지만 context와 navigation은 응용프로그램이 레코드와 물리적으로 연결되어 있어야 했습니다.
데이터를 새로운 방식으로 응용프로램과 조합하기 위한 새로운 요구사항이 드러나면 데이터베이스 관리자는 응용프로그램 개발이 시작되기도 전에 새로운 물리적 관계를 만들어서 요구사항을 준수하도록 요구받았습니다. 이것은 단지 새로운 관계를 정의하는 쉬운 것으로 보일 수도 있지만 항상 섬세한 계획과 방대한 데이터베이스 재구축이라는 결과를 낳았습니다.
이러한 수준의 물리 데이터베이스 구조를 응용프로그램에 노출하는 것은 복잡도를 증가시키는 영향을 끼쳤습니다. 추가적으로 응용프로그램에 내장된 물리 데이터베이스의 의존도는 데이터베이스가 변하면 방대한 응용프로그램 수정과 테스트를 요구했습니다.
예를 들어 일반적인 부모자식 관계는 부모 레코드 하나가 있고 하나 이상의 자식 코드를 가집니다. 이러한 종류의 관계는 개별 저축 레코드와 인출 레코드를 연결하여 유지하는데 사용됩니다.
다음의 pseudo 코드는 CA-IDMS와 같은 CODASYL 데이터베이스에서 이런 종류의 관계에 있는 레코드에 응용프로그램이 어떻게 접근하는 지 보여줍니다.
INITIALIZE ACCOUNT
Move account number ‘111111’ to the Account field in record
OBTAIN CALC ACCOUNT
Do-Forever
OBTAIN NEXT Transaction WITHIN Account-Transaction
If no more Transaction records then BREAK
If transaction-type = ‘deposit’ then
<<do something with the data>>
End-If
End-Do
1980년대 초 SQL을 기반으로 하는 새로운 종류의 데이터베이스가 정착하기 시작했습니다. CODASYL 데이터 베이스에 비해 SQL 데이터베이스의 주요 장점은 SQL 데이터베이스의 데이터가 물리적보다 관계적으로 정리되어 데이터 조작이 더욱 간단하다는 것이었습니다. 이러한 개념에 기반한 응용프로그램 로직은 행과 열로 구성된 표 형태의 데이터를 리턴했고 물리적인 관계가 아니라 테이블에 공유된 데이터를 기반으로 다른 테이블의 데이터가 합쳐질 수도 있었습니다. 이 말은 응용프로그램 작성과 유지보수가 훨씬 간단해졌다는 말이죠.
SELECT account-no, transaction-date, transaction-type,
transaction-amount
FROM ACCOUNT, TRANSACTION
WHERE account-no = ‘111111’
AND transaction-account-no = account-no
AND transaction-type = ‘deposit’
END-SELECT
위의 예제는 SQL의 힘을 보여줍니다. 데이터베이스를 가로지르는 길찾는 로직을 응용프로그램에서 요구하는 것이 아니라 계좌와 거래 사이의 공유된 키를 기반으로 순회하는 것입니다. 즉 계좌번호를 이용하는 것입니다. DBMS의 SELECT 명령의 결과는 프로그램이 요구한 정확한 데이터 집합입니다.
1990년대 후반 인터넷붐으로 응용프로그램은 계좌와 거래같은 엄격하고 구조화된 데이터 처리를 포함해 텍스트와 같은 비구조화 데이터도 처리하게 되었습니다. 중요한 사실은 응용프로그램이 빠르게 새로운 요구사항을 준수해야 했다는 것과 오래된 메인컴퓨터에서 볼 수 있었던 거래량 초과를 확장하는 것을 지원해야 했고 분산 컴퓨팅 환경을 지원해야 했습니다.
NoSQL데이터 베이스는 기초 데이터모델의 급격한 수정없이 변경을 할 수 있고 높은 데이터 볼륨과 확장하기 쉬운 구조를 지원할 수 있는 환경을 제공하기 위해 개발되었습니다.
What is MongoDB?
MongoDB는 오픈소스로 다음과 같은 특징을 가진 NoSQL 데이터베이스 관리 시스템입니다.
- Ad hoc queries
- Indexing
- Load Balancing
- Replicated file storage over multiple servers
- Data aggregation
- Server-side Javascript execution
- Capped collections
MongoDB는 문서 기반의 DBMS로 높은 대역폭을 달성하기 위해 2진JSON(BSON)과 같은 JSON 스타일 저장 포맷을 활용합니다. BSON은 응용프로그램이 데이터를 쉽게 조작하거나 추출할 수 있게하며 효과적으로 색인되고 매핑되며 복잡한 쿼리 연산과 표현을 지원하는 중첩이 됩니다.
예를 들어 위의 예제에서 ‘111111’계좌에 대한 모든 인출 거래는 다음과 같이 가져올 수 있습니다.
db.transaction.find({
transaction-account-no:/'111111',
transaction-type: /'deposit'/
})
MongoDB Application Structure
MongoDB 응용프로그램은 다음과 같은 세가지 기본 컴포넌트로 구성됩니다.
- MongoDB 인스턴스 연결 설정
- 데이터베이스 데이터 조작과 접근을 위한 로직
- MongoDB 인스턴스 연결을 종료
Establish a connection to a MongoDB instance
MongoDB에 요청을 하기 위해서는 먼저 연결이 생성되어야 합니다. 연결을 구축하는 것은 전화를 거는 것과 비슷하며 응용프로그램과 상호작용할 MongoDB 인스턴스와 데이터베이스의 전화번호가 필요합니다. 이 전화번호는 다음과 같은 요소로 구성됩니다.
- 몽고디비 인스턴스의 호스트 이름
- 데이터베이스 이름
추가적으로 전화번호에는 다음과 같은 연결 파라미터가 설정될 수 있습니다.
- 사용자 id및 패스워드
- 옵션
호스트이름과 데이터베이스 이름은 필수항목이고 나머지 연결을 위한 매개변수들은 선택사항입니다. 예제를 단순화 하기 위해서 호스트와 데이터베이스 이름만 사용할 것입니다. 그러나 production 환경에서는 보안을 위해 아이디와 패스워드로 프로그램과 사용자를 인증할 것입니다.
일반적으로 데이터베이스 접근은 응용프로그램 내에 하나 이상의 파일로 분산되어 있습니다. 가장 올바른 사용예는 호스트와 데이터베이스 이름을 모든 응용프로그램 컴포넌트에게 공유된 파일인 config.js에 독립시켜 놓는 것입니다. 이 방법의 장점은 데이터베이스나 응용프로그램이 수정되어도 config.js 파일만 업데이트하면 된다는 것입니다. config.js파일의 내용은 다음과 같습니다.
let config = {};
config.db = {};
// Create properties on the config.db object for the host and
// database names
config.db.host = ‘localhost:27017’;
config.db.name = ‘account’;
module.exports = config;
MongoDB 연결을 구축하기 위해 config.js 파일의 프로퍼티를 사용하는 require은 반드시 커넥션을 생성하는 소스파일 안에서 선언되어야 합니다. 그래야 config.js 파일의 프로퍼티를 조회할 수 있습니다. 이 프로퍼티들은 URI 커넥션 문자열과 MongoClient 레퍼런스를 만드는데 사용됩니다.
계속해서 데이터베이스 요청이 선언될 것입니다. 첫 요청은 mongoCLient.connet(mongoUri)로 커넥션은 만들고 클라이언트 인스턴스에게 레퍼런스를 리턴합니다.
const config = require(‘../config’);
const mongodb = require(‘mongodb’);
...
// Establish a mongo connection using settings from the config.js // // file
const mongoUri = `mongodb://${config.db.host}/${config.db.name}`;
const mongoClient = mongodb.MongoClient;
...
mongoClient.connect(mongoUri)
.then((db) => {
...
})
.catch((err) => {
...
});
Logic to access and manipulate database data
한번 연결이 구축되면 응용프로그램은 데이터베이스 안에 문서들을 조작하거나 접근할 준비가 된 것입니다. 이 프로세스의 첫 단계는 데이터베이스 내에 문서 컬렉션을 정의하는 것입니다. 이것은 개념적으로 관계형 데이터베이스의 테이블과 유사합니다. 모든 행의 형태가 같은 RDBMS의 테이블과 달리 컬렉션은 다른 형태를 가진 다른 종류의 문서들을 가질 수 있습니다.
우리의 예제에서 계좌 컬렉션을 조회하기 위한 컬렉션은 함수 호출에 의해 만들어집니다.
collection = accountsDb.collection(‘accounts’);
한번 컬렉션이 만들어지면 데이터베이스 안에 문서를 조회하기 위해 MongoDB 함수 호출하는데 사용될 수 있습니다. 예를들어 collection.count는 컬렉션 안에 문서의 수를 리턴합니다.
...
mongoClient.connect(mongoUri)
.then((db) => {
accountsDb = db;
collection = accountsDb.collection(‘accounts’);
log.addEntry(‘Successfully connected to MongoDB’);
return collection.count();
})
.then((count) => {
log.addEntry(`Existing record count: ${count}`);
return collection.find();
})
.then((cursor) => {
cursor.each((error, anAccount) => {
if (anAccount === null) {
log.writeLog(‘normal’, response, ‘Findall test successfully completed’);
return;
}
log.addEntry(`Account: account_no:${anAccount.account_no}
owner_fname:${anAccount.owner_fname}
owner_mi:${anAccount.owner_mi}
owner_lname:${anAccount.owner_lname}`);
});
...
})
.catch((err) => {
...
});
중요한 점은 우리의 몽고디비 함수 호출에서 콜백을 사용하지 않고 자바스크립트 프로미스를 사용했다는 것입니다. 이것은 중첩된 콜백의 결과인 callback Hell에 빠지는 것을 막기 위한 것입니다. 콜백 지옥의 주요 특성은 소스코드가 피라미드 형태가 되고 매우 이해하기 어렵고 유지보수의 난이도가 증가한다는 것입니다.
프로미스는 중첩을 제거하는 데 도움이 됩니다. 코드를 더욱 읽기 쉽고 유지보수하기 용이하게 만들죠. 이 작업의 로직은 .then()이 연관된 프로미스가 resolve되었을 때만 실행되는 것입니다. 반대로 .catch()는 프로미스가 reject되었을 때만 실행됩니다. 그러니까 에러가 감지되었단 말이죠.
Close the connection to the MongoDB instance
응용프로그램이 목적을 달성하고 나면 MongoDB의 데이터는 더이상 조회 되지 않을 것이고 우아하게 몽고디비 인스턴스와의 연결을 종료하는 것이 좋습니다. 이것은 연결을 닫는 것에 의해 달성됩니다. 위의 예제에서는 accountDb.close()를 호출하여 연결을 중단하고 사용된 자원을 회수합니다.
...
mongoClient.connect(mongoUri)
.then((db) => {
accountsDb = db;
...
})
.then((count) => {
...
})
.then((cursor) => {
...
accountsDb.close();
})
.catch((err) => {
...
});
여러분은 아마 처음 .then(db) 함수 내에 db의 값이 accountDb의 값을 저장하는 것을 눈치채셨을 겁니다. 이것은 나중에 응용프로그램이 연결을 종료하기 위해 필요한 것입니다. 이것은 db의 스코프가 처음 .then(db)함수로 제한되기 때문에 꼭 필요합니다.
추가적인 고려사항으로 여러분이 커넥션이 끝나길 원치않는 경우가 있습니다. 커넥션을 열고 닫는 것이 상대적으로 비싼 높은 볼륨의 응용프로그램은 계속해서 새로운 연결을 열고 닫는 것 보다 connection pooling을 사용하여 커넥션을 재사용 하는 것을 원할 것입니다.
What is Mongoose?
MongooseJS는 Object Document Mapper(ODM)으로 몽고디비를 더욱 쉽게 사용할 수 있도록 해주는 것으로 몽고디비 데이터베이스 안에 문서를 프로그램의 객체로 쉽게 변환해줍니다. MongooseJS 외에도 몽고디비를 위한 다른 ODM들이 개발되고 있습니다. Doctrine, MongoLink, Mandango가 있습니다.
순수히 MongoDB를 사용하는 것보다 Mongoose를 사용하면 세가지 주요 장점이 있습니다.
- 몽구스JS는 MongoDB의 상위 추상화 레이어를 제공하여 명명된 컬렉션을 사용할 필요성을 제거해줍니다.
- 몽구스의 모델은 문서 프로퍼티의 기본값을 구축하는 작업과 데이터 유효성을 수행합니다.
- 몽구스JS의 모델에 함수들이 부착될 수 있습니다. 이것은 원활한 새로운 기능 통합을 가능케합니다.
- 쿼리가 내장된 기억저장소가 아닌 함수 체이닝을 사용함으로서 코드가 더욱 유연하고 가독성이 좋아지기 때문에 유지보수가 쉬워집니다.
이러한 장점의 최종 결과는 응용프로그램의 데이터베이스 접근 단순화 입니다. 몽구스의 주요 단점으로는 추상화를 쓰기 때문에 순수 몽고디비보다 성능이 떨어진다는 것입니다.
Mongoose Concepts
몽구스는 응용프로그램이 몽고 디비에 저장하고 조작하고 싶은 데이터를 모델링 하기 위해 스키마를 사용합니다. 이것은 타입 변환, 유효성, 쿼리 빌딩과 같은 피처들을 포함합니다.
스키마는 응용프로그램이 조작할 프로퍼티(필드)의 특성을 기술합니다. 특성은 다음과 같은 것을 포함합니다.
- Data type (String, Number)
- 필수 인지 선택인지
- 문서에 해당 프로퍼티의 값이 오직 하나만 가지고 있도록 의미하는 유니크 값인지 여부
모델은 스키마로부터 생성되어 응용프로그램이 사용할 문서를 정의합니다. 더욱 정확하게는 모델은우리가 스키마에 정의한 프로퍼티와 행동으으로 문서를 정의하는 클래스입니다. 몽구스를 사용하는 문서에 대한 모든 데이터베이스의 작업은 반드시 모델을 조회해야합니다.
How Does Mongoose Code Differ from MongoDB?
몽구스와 순수 몽고디비 응용프로그램의 첫번째 차이는 스키마와 모델을 가지는 모듈이 models 디렉토리에 생성된다는 것입니다. 스키마 정의는 꽤 유용합니다. 왜냐하면 데이터 타입을 포함하여 각 프로퍼티의 특성이 필수인지 아닌지, 업데이트와 삽입에 선택적인지,값이 유니크인지 지정할 수 있기 때문입니다.
이 파일의 가장 좋은 예는 모델과 같은 이름을 가지는 것입니다. 스카마에서 만들어지는 모델이 클래스이기 때문에 문자의 첫글자를 대문자로 시작합니다. 마치 모든 함수의 첫글자가 대문자로 시작해야 하는 것처럼 말이죠.
다음의 예제에서 Account.js 파일은 몽구스 스키마와 모델 정의를 가지고 있습니다.
// Mongoose schema and model definitions
const mongoose = require(‘mongoose’);
const Schema = mongoose.Schema;
// Create the schema for the Account database
const accountSchema = new Schema({
account_no: { type: Number, required: true, unique: true },
owner_fname: { type: String, required: true, unique: false },
owner_mi: { type: String, required: true, unique: false },
owner_lname: { type: String, required: true, unique: false },
created_on: { type: Date, required: false, unique: false },
updated_on: { type: Date, required: false, unique: false },
});
// Create a model for the schema
const Account = mongoose.model(‘Account’, accountSchema);
module.exports = Account;
두번째 큰 차이점은 각 개발자 마다 좀 다를 수 있지만 순수 몽고디비에 비해 몽구스가 더 쿼리가 쉽게 구성할 수 있고 가독성이 높다는 것입니다. 몽고디비 쿼리는 문서의 프로퍼티 이름과 연산자 그리고 어떻게 문서를 필터할 지 지정하는 값들을 지정하는 BSON 구조로 이뤄져 있습니다.
몽고디비는 다음과 같은 상위 카테고리의 풍부한 쿼리 연산자들을 제공합니다.
- 비교 연산자
- 논리 연산자
- Element 연산자
- 평가 연산자
- Geospacial 연산자
- 배열 연산자
- 비트 연산자
- Projection 연산자
onwer_fname이 “Roger”와 같은(“$eq”) 문서를 선택하는 몽고디비 쿼리의 예제는 다음과 같습니다.
collection.find({ owner_fname: { $eq: ‘Roger’ } });
문서를 필터하기 위한 연산자를 쓰는 위의 방식과 함수 조합과 함수 체이닝을 쓰는 몽구스를 비교해봅시다. 몽고 디비와 비교하면 몽구스 버전의 쿼리가 더욱 길지만 훨씬 깔끔하고 읽기 쉬우며 쿼리가 더욱 복잡해지고 BSON이 요구하는 프로퍼티가 더욱 많아 질수록 결국엔 더 짧아질 것입니다.
const query = Account.find({})
.where(‘owner_fname’).equals(‘Roger’);
query.exec();
순수 몽고디비 프로그램과 동일한 몽구스 프로그램의 코드를 한줄한줄 비교한 것은 다음과 같습니다.
Conclusion
이 기사에서 사용된 예제 코드는 GitHub에서 확인할 수 잇습니다. 이 정보가 도움이 되셨으면 좋겠네요 감사합니다!
Summary
- 데이터베이스의 발전 과정과 NoSQL인 몽고디비의 등장 배경, 관계가 없기 때문에 수정이 쉽고 확장이 빠르며 대용량에서 높은 처리량을 기대할 수 있다.
- 인터넷의 발달로 계좌와 같이 엄격히 구조화된 데이터외에 텍스트와 같이 비구조적 데이터를 저장할 필요성이 생겼다.
- NoSQL은 모두 같은 형태의 행을 가지는 테이블 대신에 다른 형태의 document를 가지는 collection이 있다.
- 몽구스는 몽고디비를 더욱 쉽게 사용할 수 있게해주는 ODM(obejct document mapper)이다.
- 성능은 조금 떨어지지만 스키마를 통해 모델링을 할 수 있고 쿼리 작성 등 응용프로그램이 몽고디비에 쉽게 접근할 수 있게 해준다.