...
Mongoose 모듈
몽구스(mongoose)는 시퀄라이즈와는 달리 릴레이션이 아닌 도큐멘트를 사용하므로 ORM이 아니라 ODM (Object Document Mapping) 이라고 불린다.
몽구스는 노드 프로젝트에서 몽고디비를 다루기 위해 탄생되었다.
그런데 몽고디비 자체가 이미 자바스크립트인데 왜 굳이 자바스크립트 객체와 매핑해서 사용할까?
이는 몽고디비에 없어서 불편한 점들을 몽구스가 보완해주어 서버단에서 NoSQL DB를 프로그래밍하는데 최적화 해주기 때문이다.
먼저 몽고디비에 없던 스키마라는 개념이 생겼다. (database의 스키마를 말하는 것이다)
원래 몽고디비에는 테이블 개념이 없어(collection) 자유롭게 데이터를 넣을 수 있지만, 이 자유분방함이 오히려 독이될 수도 있다. 실수로 잘못된 자료형의 데이터를 넣거나 다른 도큐멘트에는 없는 필드의 데이터를 넣을 수도 있기 때문이다.
그래서 몽구스(ODM)는 몽고디비에 데이터를 넣기 전 노드 서버 단에서 데이터를 한 번 필터링하는 역할을 해준다.
예를들어 몽고디비의 컬렉션을 update할때 $set 빼먹는 실수를 방지해주기도 한다. ($set을 빼먹으면 전체가 초기화된다)
또한 MySQL의 JOIN 역할 기능을 populate 메서드로 어느 정도 구현을 해줄 수 있다.
직접 테이블을 일일히 구현해서 연결해주어야 하는 생 SQL과는 달리 ORM에서는 자동으로 JOIN 매핑을 해주듯이, 몽고디비에도 관계 라는 개념이 있는데 이를 ODM에서도 자동으로 populate해서 유기적이고 편리하게 관계 기능을 사용할 수 있다고 이해하면 된다.
비록 쿼리 한 번에 데이터를 합쳐서 가져오지는 않지만 이 작업을 내가 직접 해줄 필요는 없으므로 편리하다.
또한, 몽고디비 쿼리를 프로미스 객체로 만들어주어 강력하고 가독성이 높은 쿼리 빌더를 지원하는 것도 장점이다
몽구스를 사용하기에 앞서 당연히 몽고DB의 종합적인 지식이 필요하다. 몽구스 ODM 문법 자체가 몽도디비 sql문이기 때문이다.
아직 몽고디비에 익숙치 않은 독자들은 아래의 카테고리를 참고하길 바란다.
'DBMS/MongoDB' 카테고리의 글 목록
안녕하세요. 웹 개발 기술블로그를 운영하고 있어요. 반갑습니다.
inpa.tistory.com
몽구스 프로젝트 구성
몽구스 프로젝트 구성은 다음과 같다.
먼저 MVC 디자인 패턴을 차용해, 클라이언트 화면단을 보여주는 views 폴더, 몽고디비 연결 및 스키마 설정하는 모델인 Schema 폴더, 서버단 동작을 하는 컨트롤러 routes 폴더로 구성되어 있다.
몽고디비의 컬렉션(테이블)에는 User(회원정보)와 Comment(댓글)이 있으며 이를 index.js에서 연결처리 해준다고 보면 된다.
화면은 위와 같이 User, Comment 스키마를 이용해 작성자와 댓글 테이블을 출력하는 간단한 프로젝트이다.
GitHub - inpa-dev/mongoose_-: mongoose_연습
mongoose_연습. Contribute to inpa-dev/mongoose_- development by creating an account on GitHub.
github.com
지금부터 노드 프로젝트에서 몽구스 스키마를 어떻게 정의하고 연결하며, 어떻게 관계를 맺고 쿼리를 날리는지 알아보자.
몽구스 설치
$ npm i mongoose
스키마 정의
몽고디비의 컬렉션(스키마)를 정의한다.
noSQL 몽고디비를 SQL스럽게 몽구스가 만들어주는 것이라고 이해하면 된다.
User 회원정보 스키마 (/schemas/users.js)
const mongoose = require('mongoose');
const { Schema } = mongoose;
const userSchema = new Schema({
// _id 부분은 기본적으로 생략. 알아서 Object.id를 넣어줌
name: {
type: String,
// notnull이나 유니크 인덱스 같은건 원래 몽고디비에는 해당 설정이 없음.
// 몽구스에서 sql처럼 표현하기 위해 추가된 것!
required: true, // null 여부
unique: true, // 유니크 여부
},
age: {
type: Number, // Int32가 아니다. 기본 자바스크립트에는 존재하지 않으니 넘버로 해줘야 한다.
required: true,
},
married: {
type: Boolean,
required: true,
},
comment: String, // 옵션에 type밖에 없을 경우 간단하게 표현 할 수 있다.
createdAt: {
type: Date,
default: Date.now, // 기본값
},
});
module.exports = mongoose.model('User', userSchema);
Comment 회원의 댓글 정보 스키마 (/schemas/comments.js)
여기서 몽구스의 특징이 나오는데 스키마(컬렉션)을 정의할때 미리 관계 설정을 해줄 수 있다는 것이다.
type으로는 데이터 타입이 아닌 몽고디비의 ID인 ObjectId를 기재해주며, ref 속성에 관계를 맺은 스키마를 명시해두면, 뒤에서 배울 populate 메소드를 통해 자동으로 스키마(컬렉션)끼리 데이터 관계를 맺어 불러올수 있다.
const mongoose = require('mongoose');
const { Schema } = mongoose;
const { Types: { ObjectId } } = Schema; // ObjectId 타입은 따로 꺼내주어야 한다.
const commentSchema = new Schema({
// _id 부분은 기본적으로 생략. 알아서 Object.id를 넣어줌
commenter: {
type: ObjectId, // 몽고디비에서 ObjectId타입으로 데이터를 다룸
required: true,
ref: 'User', // user.js스키마에 reference로 연결되어 있음. join같은 기능. 나중에 populate에 사용
},
comment: {
type: String,
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
});
module.exports = mongoose.model('Comment', commentSchema);
몽구스(스키마) 와 시퀄라이저(모델) 문법 표현 비교
몽고디비 연결
몽고디비 ↔ 몽구스 연결 설정 (/schemas/index.js)
const mongoose = require('mongoose');
// 기본 요청 url : mongodb://localhost:27017/admin
const dbUrl = 'mongodb://' +
'barzz:123123' + // 관리자아이디 : 비밀번호
'@' +
'localhost' + // host
':27017' + // port
'/admin'; // db : admin db는 로그인을 위한 db
// 몽구스 연결 함수
const connect = () => {
// 만일 배포용이 아니라면, 디버깅 on
if (process.env.NODE_ENV !== 'production') {
mongoose.set('debug', true); // 몽고 쿼리가 콘솔에서 뜨게 한다.
}
mongoose.connect(dbUrl, {
dbName: 'nodejs', // 실제로 데이터 저장할 db명
useNewUrlParser: true,
useCreateIndex: true,
}, (error) => {
if (error) {
console.log('몽고디비 연결 에러', error);
} else {
console.log('몽고디비 연결 성공');
}
});
};
// 몽구스 커넥션에 이벤트 리스너를 달게 해준다. 에러 발생 시 에러 내용을 기록하고, 연결 종료 시 재연결을 시도한다.
mongoose.connection.on('error', (error) => {
console.error('몽고디비 연결 에러', error);
});
mongoose.connection.on('disconnected', () => {
console.error('몽고디비 연결이 끊겼습니다. 연결을 재시도합니다.');
connect(); // 연결 재시도
});
module.exports = connect;
몽고디비 연결 및 서버 실행 (app.js)
const express = require('express');
// ...
// DB연결 모듈
const connect = require('./schemas');
// 몽고 디비 연결
connect();
// 라우터 모듈
const indexRouter = require('./routes');
const usersRouter = require('./routes/users');
const commentsRouter = require('./routes/comments');
// ...
// 서버 연결
const app = express();
app.listen(3002, () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
라우터 정의 & 몽구스 쿼리 질의
유저의 댓글 정보 (/routes/comments.js)
const express = require('express');
const Comment = require('../schemas/comment');
const router = express.Router();
// axios.post('/comments', { id, comment }); 로부터 요청 받음
router.post('/', async (req, res, next) => {
try {
// Comment 스키마(컬렉션)에 데이터를 insert한다.
const comment = await Comment.create({
commenter: req.body.id, // 유저 스키마의 아이디 (_id)
comment: req.body.comment, // 댓글 내용
});
console.log(comment);
// 위의 comment 쿼리결과에서 commenter 키에 populate를 설정해주면, objectid인 필드값을 실제 user 임베디드 다큐먼트로 매핑해주게 된다
const result = await Comment.populate(comment, { path: 'commenter' });
res.status(201).json(result);
} catch (err) {
console.error(err);
next(err);
}
});
router.route('/:id')
// axios.patch(`/comments/${comment._id}`, { comment: newComment }); 로부터 요청 받음
.patch(async (req, res, next) => {
try {
// Comment 스키마 업데이트
const result = await Comment.update({
_id: req.params.id, // 업데이트 대상 검색
}, {
comment: req.body.comment, // 업데이트 내용. 원래는 $set해줘야 되지만 몽구스는 알아서 보호가 된다.
});
res.json(result);
} catch (err) {
console.error(err);
next(err);
}
})
// axios.delete(`/comments/${comment._id}`);로부터 요청 받음
.delete(async (req, res, next) => {
try {
// Comment 스키마 삭제
const result = await Comment.deleteOne({ _id: req.params.id });
res.json(result);
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
[몽고디비/몽구스 다큐먼트에 데이터 넣는 메소드 차이]
몽고디비 :
db.컬렉션명.insert(데이터)
(컬렉션이 없다면 insert가 자동으로 생성해줌)
몽구스 :
스키마클래스.create(데이터)
(대신 몽고디비의 단축메소드 insertMany 같은거 지원함)
유저 정보 (/routes/users.js)
const express = require('express');
const User = require('../schemas/user');
const Comment = require('../schemas/comment');
const router = express.Router();
router.route('/')
// axios.get('/users') 로부터 요청 받음
.get(async (req, res, next) => {
try {
const users = await User.find({}); // User 컬렉션 모두 가져오기
res.json(users);
} catch (err) {
console.error(err);
next(err);
}
})
// axios.post('/users', { name, age, married }) 로부터 요청 받음
.post(async (req, res, next) => {
try {
// User 컬렉션에 document 생성 & 등록
const user = await User.create({
name: req.body.name,
age: req.body.age,
married: req.body.married,
});
console.log(user);
res.status(201).json(user);
} catch (err) {
console.error(err);
next(err);
}
});
// 해당 User의 Comment 가져오기 라우터
router.get('/:id/comments', async (req, res, next) => {
try {
// req.params.id(유저 _id)를 commenter키에 인자로 주게되면, 위에서 등록한 User 스키마에서 미리 정의된 ref로 연결된 user 임베디드 도큐먼트로 변환 해준다.
const comments = await Comment.find({ commenter: req.params.id }).populate('commenter');
console.log(comments); // 배열로 반환
/*
[
{
// comments 다큐먼트 정보
_id: 6192f13b4407f5881bbc3b74,
comment: '제로 댓글입니다',
created_at: 2020-08-30T14:07:00.000Z,
createdAt: 2021-11-16T02:07:33.768Z
// users 다큐먼트를 임베디드 한다.
commenter: {
_id: 618dd18614694987b7eccef1,
name: 'zero',
age: 24,
married: false,
comment: '안녕하세요. 간단히 몽고디비 사용 방법에 대해 알아봅시다.',
createdAt: 2021-11-12T02:29:26.128Z
},
}
]
*/
res.json(comments);
} catch (err) {
console.error(err);
next(err);
}
});
module.exports = router;
몽구스 Populate
populate 메소드는 몽구스 ODM의 핵심 요소로, 몽고디비 컬렉션 간에 동적 관계 맺어주는 역할을 한다.
RDMBS의 JOIN 맺어주는 역할이라고 봐도 된다.
위에서 소개한 코드를 예시로 하여 몽구스 관계(populate)가 어떻게 맺어지는지 살펴보자.
populate할 스키마 설정
위의 댓글 스키마에서 소개했듯이 type에는 ObjectId를 ref에는 관계를 맺은 스키마를 적는다.
popuate 관계 맺기
위의 스키마 설정에서 ref로 지정해주었다고 끝이 아니다.
직접 몽구스 api로 다음과 같이 commenter 필드를 지정하여 populate 설정해주어야 한다.
popuate한 관계 데이터 가져오기
단순히 Comment 스키마를 find하면 commenter필드에는 objectid값만이 들어있다.
만일 관계 데이터를 가져오고 싶다면, 뒤에 populate를 명시해서 도큐먼트를 임베디드 시켜야 한다.
몽고디비의 관계 원리에 대해서 자세히 알고 싶다면 이 포스팅을 참고하길 바란다.
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.