Node.js와 Mongoose를 이용한 MongoDB 관리: 실용적인 가이드와 코드 예제

들어가기 전에

MongoDB는 문서 지향적 NoSQL 데이터베이스로, JSON과 유사한 형태의 문서를 사용하여 데이터를 저장합니다. 이러한 특성은 스키마 설계의 유연성을 제공하며, 대용량 데이터 처리에도 적합합니다.

그런데 MongoDB와 함께 사용되는 라이브러리 중 하나인 Mongoose는 어떤 역할을 하는 걸까요? Mongoose는 MongoDB를 위한 객체 모델링 도구로, 개발자가 데이터를 쉽고 안전하게 다룰 수 있게 도와줍니다. 스키마 정의, 데이터 검증, 쿼리 구성 등 복잡한 작업을 단순화시켜 주는 Mongoose는 MongoDB와 함께 사용하면 개발의 효율성을 크게 높일 수 있습니다.

이 포스팅에서는 MongoDB의 특징과 Mongoose의 역할, 그리고 둘을 함께 사용하는 방법에 대해 자세히 살펴보겠습니다.


설치 및 설정

Node.js와 MongoDB 설치

Node.js 설치: 먼저 Node.js를 설치해야 합니다. Node.js 공식 웹사이트에서 자신의 운영체제에 맞는 설치파일을 다운로드하고 실행하면 됩니다.

본 블로그의 Express편을 참고해주세요

MongoDB 설치: MongoDB도 설치가 필요합니다. MongoDB 공식 웹사이트에서 설치 가이드를 참조하세요. 이부분은 다루지 않겠습니다.

Mongoose 설치

Mongoose 설치는 간단합니다. 프로젝트의 루트 디렉토리에서 다음 명령어를 실행하면 됩니다.

npm install mongoose

2.3 데이터베이스 연결 설정

Mongoose를 사용하려면 MongoDB와의 연결을 설정해야 합니다. 이 연결은 다음 코드를 통해 수행될 수 있습니다.

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost/your_database_name', {
  useNewUrlParser: true,
  useUnifiedTopology: true
})
.then(() => console.log('Database connected'))
.catch(err => console.log(err));

이 코드는 MongoDB가 로컬에서 실행되고 있는 경우의 예시입니다. your_database_name 부분은 실제 데이터베이스 이름으로 대체해야 합니다.


스키마와 모델 정의

스키마 정의와 의미

Mongoose에서 스키마는 데이터베이스의 특정 컬렉션 내의 문서 구조를 설명합니다. 스키마는 문서 내의 각 필드, 데이터 유형, 검증, 기본값 등을 정의합니다.

예를 들어, ‘User’ 컬렉션에 대한 스키마를 정의해보겠습니다:

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true
  },
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

모델 생성

스키마를 정의한 후에는 이를 사용하여 모델을 생성해야 합니다. 모델은 스키마를 데이터베이스와 연결하는 생성자입니다.

const User = mongoose.model('User', userSchema);

이제 User 모델을 사용하여 데이터베이스에서 CRUD 작업을 수행할 수 있습니다.

필드 검증, 기본 값, 메소드 추가

스키마에서는 필드에 대한 검증도 가능합니다. 예를 들어, email 필드에 대해 검증을 추가하고 싶다면 다음과 같이 작성할 수 있습니다.

email: {
  type: String,
  required: true,
  unique: true,
  validate: {
    validator: function(v) {
      // 이메일 형식 검증 로직
      return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v);
    },
    message: props => `${props.value} is not a valid email!`
  }
}

기본값 설정은 default 키워드를 사용하여 수행할 수 있으며, 스키마에 메소드를 추가하는 것도 가능합니다.

다른 예제를 보겠습니다.

Book 스키마와 모델

도서 관리 애플리케이션에서 사용될 ‘Book’ 스키마와 모델을 정의해보겠습니다.

스키마 정의
const bookSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true
  },
  author: {
    type: String,
    required: true
  },
  publishedDate: {
    type: Date,
    required: true
  },
  isbn: {
    type: String,
    validate: {
      validator: function(v) {
        // ISBN 형식 검증 로직
        return /^(97(8|9))?\d{9}(\d|X)$/.test(v);
      },
      message: props => `${props.value} is not a valid ISBN!`
    }
  },
  categories: [String]
});

여기서 ISBN 필드는 특별한 형식을 가지므로, 커스텀 검증 로직을 추가하였습니다. categories 필드는 문자열 배열로 정의되어 있어, 한 권의 책이 여러 카테고리에 속할 수 있습니다.

모델 생성

스키마를 정의한 후, 이를 사용하여 ‘Book’ 모델을 생성합니다.

const Book = mongoose.model('Book', bookSchema);

인스턴스 메소드 추가

또한, 인스턴스 메소드를 스키마에 추가하여 모델의 인스턴스에서 사용할 수 있습니다.

bookSchema.methods.getInfo = function() {
  return `Title: ${this.title}, Author: ${this.author}, ISBN: ${this.isbn}`;
};

const myBook = new Book({/* ... */});
console.log(myBook.getInfo()); // 예: Title: Moby Dick, Author: Herman Melville, ISBN: 1234567890


CRUD 작업 수행

Mongoose 주요 함수 및 메서드

스키마(Schema) 생성

문서의 구조와 필드, 타입, 검증을 지정합니다.

const bookSchema = new mongoose.Schema({
  title: { type: String, required: true },
  author: { type: String, required: true },
  publishedDate: { type: Date, default: Date.now },
  categories: [String]
});

모델(Model) 생성

스키마를 기반으로 모델을 생성합니다.

const Book = mongoose.model('Book', bookSchema);

문서(Document) 생성 및 저장

const book = new Book({
  title: 'Moby Dick',
  author: 'Herman Melville'
});

book.save(function(err) {
  if (err) console.error(err);
  // 저장 완료
});

문서 조회

  • find(query, callback): 조건에 맞는 모든 문서 조회
  • findOne(query, callback): 조건에 맞는 첫 번째 문서 조회
  • findById(id, callback): ID로 문서 조회
Book.find({ author: 'Herman Melville' }, function(err, books) { /* ... */ });
Book.findOne({ title: 'Moby Dick' }, function(err, book) { /* ... */ });
Book.findById('12345', function(err, book) { /* ... */ });

문서 수정

  • updateOne(query, update, options, callback): 조건에 맞는 하나의 문서 수정
  • findOneAndUpdate(query, update, options, callback): 조건에 맞는 첫 번째 문서 찾고 수정
Book.updateOne({ title: 'Moby Dick' }, { author: 'New Author' }, function(err, res) { /* ... */ });
Book.findOneAndUpdate({ title: 'Moby Dick' }, { author: 'New Author' }, function(err, book) { /* ... */ });

문서 삭제

  • deleteOne(query, callback): 조건에 맞는 하나의 문서 삭제
  • findOneAndDelete(query, options, callback): 조건에 맞는 첫 번째 문서 찾고 삭제
Book.deleteOne({ title: 'Moby Dick' }, function(err) { /* ... */ });
Book.findOneAndDelete({ title: 'Moby Dick' }, function(err, book) { /* ... */ });

CRUD 작업 수행

이제 CRUD 작업을 수행해보겠습니다.

Create: 문서 생성 및 저장

‘Book’ 모델의 인스턴스를 생성하고 저장하는 과정입니다.

const book = new Book({
  title: 'Moby Dick',
  author: 'Herman Melville',
  publishedDate: new Date('1851-10-18'),
  isbn: '1234567890',
  categories: ['Fiction', 'Adventure']
});

book.save((err) => {
  if (err) console.error(err);
  else console.log('Book saved successfully!');
});

Read: 문서 조회

특정 조건을 충족하는 문서를 조회하는 과정입니다. 예를 들어, ISBN으로 특정 책을 찾을 수 있습니다.

Book.find({ isbn: '1234567890' }, (err, books) => {
  if (err) console.error(err);
  else console.log(books);
});

Update: 문서 수정

특정 문서의 필드를 수정하고 저장하는 과정입니다. 아래는 ISBN이 일치하는 문서의 제목을 업데이트하는 예입니다.

Book.findOneAndUpdate(
  { isbn: '1234567890' },
  { title: 'Moby Dick: Updated Edition' },
  (err, book) => {
    if (err) console.error(err);
    else console.log('Book updated successfully!');
  }
);

Delete: 문서 삭제

특정 문서를 삭제하는 과정입니다. 아래는 ISBN이 일치하는 문서를 삭제하는 예입니다.

Book.findOneAndDelete({ isbn: '1234567890' }, (err) => {
  if (err) console.error(err);
  else console.log('Book deleted successfully!');
});

각 CRUD 작업은 Mongoose를 통해 MongoDB와의 상호작용을 추상화하고 간소화합니다. 이러한 과정은 데이터 관리를 훨씬 더 직관적이고 유연하게 만들어 주며, 복잡한 데이터베이스 작업을 간단한 자바스크립트 코드로 수행할 수 있게 합니다.

사용자 DB를 예시로 새로운 예시를 볼게요.

Create: 생성 및 저장

새로운 사용자를 생성하고 저장하는 예제입니다.

const userSchema = new mongoose.Schema({
  username: String,
  email: String
});

const User = mongoose.model('User', userSchema);

const newUser = new User({ username: 'johndoe', email: 'johndoe@example.com' });
newUser.save(function(err) {
  if (err) return console.error(err);
  console.log('User saved successfully!');
});
  • newUser 객체는 User 모델을 기반으로 생성되었습니다.
  • save 메서드는 문서를 데이터베이스에 저장합니다.

Read: 조회

특정 조건에 맞는 사용자를 조회하는 예제입니다.

User.find({ username: 'johndoe' }, function(err, users) {
  if (err) return console.error(err);
  console.log(users);
});
  • find 메서드는 조건에 맞는 모든 문서를 배열로 반환합니다.

Update: 수정

특정 조건에 맞는 사용자를 수정하는 예제입니다.

User.updateOne({ username: 'johndoe' }, { email: 'john.doe@newdomain.com' }, function(err, res) {
  if (err) return console.error(err);
  console.log('User updated successfully:', res);
});
  • updateOne 메서드는 조건에 맞는 첫 번째 문서를 수정합니다.

Delete: 삭제

특정 조건에 맞는 사용자를 삭제하는 예제입니다.

User.deleteOne({ username: 'johndoe' }, function(err) {
  if (err) return console.error(err);
  console.log('User deleted successfully!');
});
  • deleteOne 메서드는 조건에 맞는 첫 번째 문서를 삭제합니다.

CRUD 작업은 데이터베이스 작업의 핵심이며, Mongoose를 통해 이러한 작업을 직관적이고 간단하게 수행할 수 있습니다.


고급 쿼리 기능

Mongoose에서 제공하는 고급 쿼리 기능을 소개하겠습니다.

Aggregation

Aggregation은 데이터의 여러 문서를 그룹화하고, 특정 연산을 통해 요약된 결과를 반환하는 작업입니다.

const UserSchema = new mongoose.Schema({
  age: Number,
  city: String
});

const User = mongoose.model('User', UserSchema);

User.aggregate([
  { $group: { _id: '$city', avgAge: { $avg: '$age' } } }
]).then(result => console.log(result));
  • $group을 사용하여 동일한 도시의 사용자들을 그룹화하고, 평균 나이를 계산합니다.

Population

Population은 참조된 다른 문서의 필드를 현재 문서에 채워 넣는 작업입니다.

const authorSchema = new mongoose.Schema({
  name: String,
  stories: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Story' }]
});

const storySchema = new mongoose.Schema({
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' },
  title: String
});

const Author = mongoose.model('Author', authorSchema);
const Story = mongoose.model('Story', storySchema);

Story.find().populate('author').exec((err, stories) => {
  if (err) return console.error(err);
  console.log('Stories with authors:', stories);
});
  • populate 메서드는 author 필드를 해당 문서의 데이터로 채워 넣습니다.

Indexing

Indexing은 데이터베이스의 검색 성능을 향상시키는 방법입니다. 인덱스를 생성하면 특정 필드를 기준으로 데이터를 빠르게 찾을 수 있습니다.

const userSchema = new mongoose.Schema({
  username: { type: String, index: true },
  email: String
});

const User = mongoose.model('User', userSchema);

User.find({ username: 'johndoe' }).exec((err, user) => {
  if (err) return console.error(err);
  console.log('User found:', user);
});
  • username 필드에 인덱스를 생성하면, 해당 필드로 검색할 때 성능이 향상됩니다.

이렇게 Mongoose는 데이터베이스 작업을 더 풍부하고 강력하게 만들어주는 다양한 고급 기능을 제공합니다. 이러한 기능을 활용하면 복잡한 데이터 처리 작업도 효과적으로 수행할 수 있습니다.


Mongoose의 미들웨어 활용

Mongoose 미들웨어는 데이터베이스 작업의 특정 단계에서 실행되는 함수로, 데이터 검증, 변환, 로깅 등과 같은 추가 작업을 수행할 수 있게 해줍니다.

미들웨어 개념

미들웨어는 문서의 저장, 검색, 업데이트, 삭제 등과 같은 데이터베이스 연산이 발생하기 전이나 후에 특정 로직을 실행할 수 있게 해줍니다.

Pre 미들웨어 활용 예제

Pre 미들웨어는 특정 작업이 수행되기 전에 실행되는 함수입니다.

const userSchema = new mongoose.Schema({
  password: String
});

userSchema.pre('save', function (next) {
  // 패스워드 암호화
  this.password = bcrypt.hashSync(this.password, 10);
  next();
});

const User = mongoose.model('User', userSchema);

const user = new User({ password: 'plaintext' });
user.save(); // 패스워드가 암호화된 상태로 저장됩니다.
  • save 메서드가 호출되기 전에 패스워드 암호화를 수행합니다.

Post 미들웨어 활용 예제

Post 미들웨어는 특정 작업이 수행된 후에 실행되는 함수입니다.

userSchema.post('save', function (doc) {
  console.log(`${doc._id} has been saved`);
});

const user = new User({ password: 'plaintext' });
user.save(); // 콘솔에 "문서ID has been saved" 메시지가 출력됩니다.
  • save 메서드가 호출된 후에 문서가 저장되었음을 로깅합니다.

Mongoose 미들웨어를 활용하면, 문서 생명주기의 여러 단계에서 추가 작업을 수행할 수 있어, 데이터 관리를 더욱 효율적이고 안전하게 만들 수 있습니다.


실전 예제

Mini Project

실제 프로젝트에서는 여러가지 요구사항이 결합되며, 데이터베이스 스키마 설계부터 CRUD 작업, 고급 쿼리 기능, 미들웨어 등의 사용이 필요합니다. 다음은 Mongoose를 이용한 간단한 미니 프로젝트를 예로 들어 설명합니다.

요구사항 분석

  • 목적: 간단한 블로그 시스템 구축
  • 기능:
  • 사용자 등록 및 로그인
  • 게시글 작성, 수정, 삭제, 조회
  • 댓글 작성, 수정, 삭제

설계 및 구현

사용자 스키마
const userSchema = new mongoose.Schema({
  username: String,
  password: String,
  email: String
});

userSchema.pre('save', function (next) {
  this.password = bcrypt.hashSync(this.password, 10);
  next();
});

const User = mongoose.model('User', userSchema);

게시글 스키마
const postSchema = new mongoose.Schema({
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  title: String,
  content: String,
  comments: [{ body: String, date: Date }]
});

const Post = mongoose.model('Post', postSchema);

CRUD 작업
  • 게시글 생성:
const post = new Post({
  author: user._id,
  title: 'My first post',
  content: 'This is my first post'
});
post.save();
  • 게시글 조회:
Post.find().populate('author').exec((err, posts) => {
  // 게시글 목록 조회
});

테스트

테스트는 각 기능별로 수행되며, 사용자 등록, 로그인, 게시글 작성 등의 주요 기능을 검증합니다.

  • 사용자 등록 테스트: 새 사용자를 생성하고, 암호화된 패스워드가 올바르게 저장되는지 확인합니다.
  • 게시글 작성 테스트: 새 게시글을 작성하고, 데이터베이스에 올바르게 저장되는지 확인합니다.


끝으로

Mongoose는 MongoDB와 함께 Node.js 환경에서 백엔드 개발을 효율적으로 수행할 수 있도록 도와주는 라이브러리입니다. 이 글을 통해 Mongoose의 주요 기능과 활용 방법을 살펴보았으며, 이러한 경험을 바탕으로 Mongoose를 활용한 백엔드 개발의 주요 장점을 다음과 같이 정리할 수 있습니다.

1. 개발 생산성 향상

  • 스키마 정의: 데이터 구조와 검증 로직을 명확하게 정의함으로써, 개발자는 데이터 무결성을 보장하며 개발할 수 있습니다.
  • CRUD 작업 간소화: Mongoose의 메소드들을 활용하면 복잡한 쿼리 작성 없이도 데이터베이스의 CRUD 작업을 수행할 수 있습니다.

2. 코드의 재사용성과 유지보수성

  • 미들웨어 활용: 공통 로직을 미들웨어로 분리하여 코드의 재사용성을 높이고 유지보수를 용이하게 합니다.
  • 모델 중심의 설계: 데이터와 관련된 로직을 모델 내부에 캡슐화함으로써, 코드의 응집력을 높이고 변화에 대응하기 쉽게 만듭니다.

Mongoose를 활용하면, 데이터베이스 작업을 효과적으로 수행하면서 개발 생산성을 향상시킬 수 있으며, 코드의 재사용성과 유지보수성 또한 높일 수 있습니다.

Leave a Comment