Mongoose를 활용한 MongoDB 연동
관계형 데이터베이스와 TypeORM 연동에 대해 알아보았다면, 이번에는 또 다른 강력한 데이터베이스 패러다임인 NoSQL 데이터베이스, 그중에서도 MongoDB를 NestJS와 함께 사용하는 방법을 살펴보겠습니다.
관계형 데이터베이스가 엄격한 스키마와 ACID(원자성, 일관성, 고립성, 지속성) 속성을 통해 데이터의 정합성을 강조한다면, NoSQL 데이터베이스는 유연한 스키마, 수평 확장성, 대량의 비정형 데이터 처리에 강점을 가집니다. 특히 MongoDB는 문서(Document) 기반의 NoSQL 데이터베이스로, JSON과 유사한 BSON(Binary JSON) 형식으로 데이터를 저장하여 웹 애플리케이션 개발에 매우 직관적이고 효율적입니다.
NestJS는 TypeORM을 통한 관계형 데이터베이스 연동만큼이나 MongoDB와의 연동도 매끄럽게 지원합니다. 주로 Mongoose 라이브러리와 NestJS의 @nestjs/mongoose
패키지를 사용하여 MongoDB를 연동합니다.
MongoDB와 Mongoose란?
MongoDB는 데이터를 컬렉션(Collection)이라는 개념 안에 문서(Document) 형태로 저장하는 NoSQL 데이터베이스입니다. 각 문서는 필드-값 쌍으로 구성되며, JSON 객체와 유사한 구조를 가집니다. 관계형 데이터베이스의 테이블이 컬렉션에, 행(Row)이 문서에, 그리고 컬럼(Column)이 문서의 필드에 해당한다고 볼 수 있습니다.
Mongoose는 Node.js 환경에서 MongoDB를 더 쉽게 사용할 수 있도록 도와주는 객체 데이터 모델링(ODM: Object Data Modeling) 라이브러리입니다. TypeORM이 관계형 데이터베이스를 위한 ORM이라면, Mongoose는 MongoDB를 위한 ODM이라고 생각하시면 됩니다. Mongoose는 스키마 정의, 데이터 유효성 검사, 쿼리 빌더, 미들웨어 기능 등을 제공하여 개발자가 MongoDB 작업을 더욱 구조적이고 효율적으로 수행할 수 있도록 돕습니다.
MongoDB와 Mongoose 사용의 주요 장점
- 유연한 스키마: 데이터 구조가 고정적이지 않아 변화하는 요구사항에 빠르게 대응할 수 있습니다. (물론 Mongoose를 통해 스키마를 정의하여 구조를 강제할 수도 있습니다.)
- 수평 확장성: 샤딩(Sharding)을 통해 대량의 데이터를 효율적으로 분산 저장하고 처리할 수 있습니다.
- 개발 생산성: JSON 형태의 문서 사용은 프론트엔드와 백엔드 간 데이터 전달을 용이하게 하고, JavaScript 개발자에게 익숙한 데이터 형식을 제공합니다.
- 풍부한 기능: Mongoose는 강력한 유효성 검사, 미들웨어(Pre/Post 훅), 쿼리 헬퍼 등 다양한 편의 기능을 제공합니다.
NestJS에서 MongoDB 연동하기: 핵심 개념
NestJS에서 MongoDB를 연동할 때는 주로 다음과 같은 개념들을 사용합니다.
스키마 (Schema):
Mongoose에서 스키마는 컬렉션에 저장될 문서의 구조와 데이터 타입, 유효성 규칙, 기본값 등을 정의합니다. 관계형 데이터베이스의 테이블 스키마와 유사한 역할을 하지만, NoSQL의 유연성을 유지하면서 데이터의 일관성을 확보하는 데 도움을 줍니다. @Schema()
데코레이터를 사용하여 정의합니다.
예시
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
export type UserDocument = HydratedDocument<User>; // Mongoose 문서 타입 정의
@Schema() // 이 클래스가 Mongoose 스키마임을 나타냅니다.
export class User {
@Prop({ required: true, unique: true }) // 필수 필드, 유니크 제약 조건
username: string;
@Prop({ required: true })
email: string;
@Prop({ default: Date.now }) // 기본값 설정
createdAt: Date;
}
export const UserSchema = SchemaFactory.createForClass(User); // 스키마 객체 생성
@Prop()
데코레이터를 사용하여 문서의 필드를 정의하고, 각 필드의 타입, 필수 여부, 유니크 제약 조건, 기본값 등을 설정할 수 있습니다.
모델 (Model):
모델은 특정 스키마에 정의된 컬렉션에 대한 인터페이스 역할을 합니다. 모델을 통해 데이터베이스에 쿼리를 실행하거나 새 문서를 생성할 수 있습니다. TypeORM의 리포지토리와 유사하게, 실제 데이터베이스 작업을 수행하는 주체라고 할 수 있습니다. NestJS에서는 @InjectModel()
데코레이터를 사용하여 모델을 주입받습니다.
예시
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from './schemas/user.schema'; // 스키마 및 문서 타입 임포트
@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>, // User 모델 주입
) {}
async create(username: string, email: string): Promise<User> {
const createdUser = new this.userModel({ username, email });
return createdUser.save(); // 새 문서 생성 및 저장
}
async findAll(): Promise<User[]> {
return this.userModel.find().exec(); // 모든 사용자 조회
}
async findOne(id: string): Promise<User | null> {
return this.userModel.findById(id).exec(); // ID로 사용자 한 명 조회
}
async delete(id: string): Promise<any> {
return this.userModel.deleteOne({ _id: id }).exec(); // ID로 사용자 삭제
}
}
InjectModel(User.name)
에서 User.name
을 사용하는 이유는 Mongoose가 모델을 등록할 때 스키마 클래스의 이름을 사용하기 때문입니다.
NestJS와 MongoDB 연동 설정
NestJS 애플리케이션에 MongoDB를 통합하는 기본적인 단계는 다음과 같습니다.
필요한 패키지 설치: MongoDB 드라이버와 NestJS의 Mongoose 통합 모듈을 설치합니다.
npm install @nestjs/mongoose mongoose
MongoDB 서비스 실행: 로컬에 MongoDB가 설치되어 있지 않다면 Docker를 사용하거나 MongoDB Atlas와 같은 클라우드 서비스를 이용하는 것이 편리합니다. 로컬에서 MongoDB를 실행하는 가장 간단한 방법 중 하나는 Docker Compose를 사용하는 것입니다.
version: '3.8'
services:
mongodb:
image: mongo:latest
container_name: nestjs-mongo
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db # 데이터 영속성을 위해 볼륨 마운트
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
volumes:
mongo-data:
docker-compose up -d
명령어로 MongoDB 컨테이너를 실행할 수 있습니다.
MongooseModule 설정:
루트 모듈(AppModule
)의 imports
배열에 MongooseModule.forRoot()
를 사용하여 MongoDB 연결 정보를 설정합니다. 그리고 특정 모듈(UsersModule
)에서 사용할 스키마를 MongooseModule.forFeature()
를 통해 등록합니다.
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; // Mongoose 모듈 임포트
import { UsersModule } from './users/users.module'; // 사용자 모듈 임포트
@Module({
imports: [
MongooseModule.forRoot('mongodb://root:example@localhost:27017/nest_mongo_db?authSource=admin'), // MongoDB 연결 URI
UsersModule, // 사용자 모듈
],
controllers: [],
providers: [],
})
export class AppModule {}
주의: mongodb://username:password@host:port/database_name?authSource=admin
형식으로 연결 URI를 작성합니다. authSource=admin
은 인증 데이터베이스를 지정하는 것으로, Docker 환경에서 루트 사용자 인증을 위해 주로 사용됩니다. 실제 운영 환경에서는 보안에 유의하여 자격 증명을 관리해야 합니다.
피처(Feature) 모듈에 스키마 등록:
각 도메인(예: users
)에 해당하는 모듈에서는 MongooseModule.forFeature()
를 사용하여 해당 모듈에서 사용할 스키마와 모델을 등록합니다.
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User, UserSchema } from './schemas/user.schema'; // 스키마 임포트
@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), // User 모델 등록
],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // 다른 모듈에서 UsersService를 사용하려면 export
})
export class UsersModule {}
forFeature()
는 현재 모듈에서 사용할 컬렉션과 그에 해당하는 스키마를 정의합니다.
실제 데이터 작업 예시 (간략)
이제 MongoDB와 연동된 서비스를 사용하여 컨트롤러에서 데이터를 처리하는 간단한 예를 보겠습니다.
import { Controller, Get, Post, Body, Param, Delete } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
async create(@Body() createUserDto: { username: string; email: string }) {
return this.usersService.create(createUserDto.username, createUserDto.email);
}
@Get()
async findAll() {
return this.usersService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
@Delete(':id')
async delete(@Param('id') id: string) {
return this.usersService.delete(id);
}
}
이처럼 NestJS는 @nestjs/mongoose
패키지를 통해 Mongoose를 완벽하게 통합하여, TypeScript의 타입 안정성과 MongoDB의 유연성을 동시에 활용하는 강력한 애플리케이션을 구축할 수 있도록 돕습니다.
MongoDB는 특히 대용량의 비정형 데이터를 다루거나 빠른 개발 속도가 요구될 때 매우 효과적인 선택이 될 수 있습니다. NestJS와의 결합은 이러한 장점들을 극대화하여 여러분의 프로젝트에 큰 시너지를 가져다줄 것입니다.