2026년 NestJS와 MongoDB 완벽 가이드: Mongoose, 집계 파이프라인, 면접 대비
NestJS 12와 Mongoose 9를 활용한 MongoDB 애플리케이션 개발 완벽 가이드. 스키마 설계, 집계 파이프라인, 트랜잭션 처리, 그리고 기술 면접에서 자주 나오는 질문과 답변을 상세히 다룹니다.

NestJS 12와 Mongoose 9의 조합은 확장 가능한 Node.js 백엔드를 구축하기 위한 프로덕션 준비 스택을 제공합니다. 이 가이드에서는 스키마 설계 패턴, 집계 파이프라인, 그리고 시니어 개발자와 주니어 개발자를 구분하는 면접 질문에 대해 다룹니다.
Mongoose 9.9.4는 Node.js 18 이상이 필요하며 MongoDB 6.0부터 8.0까지 지원합니다. NestJS 12는 ESM 호환 패키지를 제공하지만 CommonJS 프로젝트와의 하위 호환성도 유지됩니다.
NestJS 12 애플리케이션에서 Mongoose 설정하기
@nestjs/mongoose 패키지는 Mongoose를 NestJS의 의존성 주입 시스템과 통합합니다. 필요한 의존성을 설치합니다.
# Mongoose 통합 패키지 설치
npm install @nestjs/mongoose mongoose루트 모듈에서 연결을 등록합니다.
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRoot(process.env.MONGODB_URI, {
// 프로덕션 워크로드를 위한 연결 풀 크기
maxPoolSize: 10,
// 연결 실패 시 10초 후 타임아웃
serverSelectionTimeoutMS: 10000,
}),
],
})
export class AppModule {}forRoot 메서드는 모든 Mongoose 연결 옵션을 받습니다. maxPoolSize를 설정하면 부하 상황에서 연결 고갈을 방지할 수 있습니다. 이는 프로덕션 환경에서 흔히 발생하는 문제입니다.
TypeScript 데코레이터를 사용한 스키마 설계
NestJS의 Mongoose 스키마는 @nestjs/mongoose의 데코레이터를 사용합니다. 각 스키마는 MongoDB 컬렉션에 매핑됩니다.
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';
// TypeScript 자동완성을 위한 도큐먼트 타입
export type UserDocument = HydratedDocument<User>;
@Schema({
timestamps: true, // createdAt과 updatedAt 추가
collection: 'users', // 명시적 컬렉션 이름
})
export class User {
// MongoDB ObjectId, 자동 생성
_id: Types.ObjectId;
@Prop({ required: true, unique: true, index: true })
email: string;
@Prop({ required: true })
passwordHash: string;
@Prop({ type: String, enum: ['admin', 'user', 'guest'], default: 'user' })
role: string;
@Prop({ type: [String], default: [] })
permissions: string[];
}
export const UserSchema = SchemaFactory.createForClass(User);@Prop 데코레이터는 필드 제약 조건을 정의합니다. 자주 쿼리되는 필드에 index: true를 설정하면 쓰기 성능이 저하되는 대신 읽기 성능이 향상됩니다.
면접관은 임베디드 도큐먼트와 참조의 사용 구분에 대해 질문하는 경우가 있습니다. 임베디드 도큐먼트는 함께 접근되는 데이터(사용자 프로필 + 설정)에 적합합니다. 참조는 무제한으로 증가하는 데이터나 독립적인 쿼리가 필요한 데이터(사용자 + 주문)에 적합합니다.
Injectable 서비스를 활용한 리포지토리 패턴
NestJS에서는 데이터베이스 로직을 서비스로 분리하는 것을 권장합니다. @InjectModel 데코레이터는 Mongoose 모델에 대한 접근을 제공합니다.
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { User, UserDocument } from './user.schema';
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
) {}
async findById(id: string): Promise<UserDocument> {
// 쿼리 전에 ObjectId 형식 검증
if (!Types.ObjectId.isValid(id)) {
throw new NotFoundException('잘못된 사용자 ID 형식입니다');
}
const user = await this.userModel.findById(id).exec();
if (!user) {
throw new NotFoundException(`사용자 ${id}를 찾을 수 없습니다`);
}
return user;
}
async findByEmail(email: string): Promise<UserDocument | null> {
// 대소문자를 구분하지 않는 이메일 검색
return this.userModel.findOne({
email: { $regex: new RegExp(`^${email}$`, 'i') }
}).exec();
}
async create(data: Partial<User>): Promise<UserDocument> {
const user = new this.userModel(data);
return user.save();
}
}.exec()를 호출하면 Mongoose Query 객체 대신 적절한 Promise가 반환됩니다. 이는 올바른 async/await 동작과 에러 스택 트레이스에 중요합니다.
복잡한 쿼리를 위한 집계 파이프라인
MongoDB 집계는 SQL 데이터베이스가 JOIN과 GROUP BY로 해결하는 보고서, 분석, 데이터 변환을 처리합니다.
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, PipelineStage } from 'mongoose';
import { Order, OrderDocument } from './order.schema';
interface MonthlyRevenue {
month: number;
revenue: number;
orderCount: number;
}
@Injectable()
export class AnalyticsService {
constructor(
@InjectModel(Order.name) private orderModel: Model<OrderDocument>,
) {}
async getRevenueByMonth(year: number): Promise<MonthlyRevenue[]> {
const pipeline: PipelineStage[] = [
// 스테이지 1: 연도로 필터링
{
$match: {
createdAt: {
$gte: new Date(`${year}-01-01`),
$lt: new Date(`${year + 1}-01-01`),
},
status: 'completed',
},
},
// 스테이지 2: 월별로 그룹화
{
$group: {
_id: { $month: '$createdAt' },
revenue: { $sum: '$totalAmount' },
orderCount: { $sum: 1 },
},
},
// 스테이지 3: 월 순서로 정렬
{ $sort: { _id: 1 } },
// 스테이지 4: 출력 형식 정리
{
$project: {
_id: 0,
month: '$_id',
revenue: 1,
orderCount: 1,
},
},
];
return this.orderModel.aggregate(pipeline).exec();
}
}집계 파이프라인은 순서대로 실행됩니다. 각 스테이지는 다음 스테이지로 도큐먼트를 전달합니다. 성능을 최적화하려면 $match 스테이지를 가능한 한 초기에 배치해야 합니다.
$lookup을 사용한 컬렉션 간 조인
MongoDB는 도큐먼트 데이터베이스이지만, $lookup을 사용하여 다른 컬렉션 간에 데이터를 조인할 수 있습니다.
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, PipelineStage } from 'mongoose';
import { Order, OrderDocument } from './order.schema';
@Injectable()
export class OrderDetailsService {
constructor(
@InjectModel(Order.name) private orderModel: Model<OrderDocument>,
) {}
async getOrdersWithUserDetails(): Promise<any[]> {
const pipeline: PipelineStage[] = [
// users 컬렉션과 조인
{
$lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'user',
},
},
// 배열에서 단일 객체로 변환
{ $unwind: '$user' },
// 필요한 필드만 선택
{
$project: {
orderNumber: 1,
totalAmount: 1,
'user.email': 1,
'user.role': 1,
},
},
];
return this.orderModel.aggregate(pipeline).exec();
}
}$lookup은 왼쪽 외부 조인을 수행합니다. 대규모 컬렉션에서 사용할 경우 조인 필드에 인덱스가 있는지 확인해야 합니다.
$lookup은 조인되는 컬렉션이 클 경우 성능 문제를 일으킬 수 있습니다. 읽기 빈도가 높은 데이터의 경우 데이터 비정규화를 고려해야 합니다.
MongoDB 트랜잭션 구현
Mongoose 9는 멀티 도큐먼트 트랜잭션을 지원하여 여러 도큐먼트에 걸친 원자적 작업을 보장합니다.
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectModel, InjectConnection } from '@nestjs/mongoose';
import { Model, Connection } from 'mongoose';
import { Account, AccountDocument } from './account.schema';
@Injectable()
export class TransferService {
constructor(
@InjectModel(Account.name) private accountModel: Model<AccountDocument>,
@InjectConnection() private connection: Connection,
) {}
async transfer(
fromAccountId: string,
toAccountId: string,
amount: number,
): Promise<void> {
// 세션 시작
const session = await this.connection.startSession();
try {
await session.withTransaction(async () => {
// 출금 계좌에서 차감
const fromAccount = await this.accountModel.findByIdAndUpdate(
fromAccountId,
{ $inc: { balance: -amount } },
{ session, new: true },
);
if (!fromAccount || fromAccount.balance < 0) {
throw new BadRequestException('잔액이 부족합니다');
}
// 입금 계좌에 추가
await this.accountModel.findByIdAndUpdate(
toAccountId,
{ $inc: { balance: amount } },
{ session },
);
});
} finally {
await session.endSession();
}
}
}트랜잭션은 레플리카 셋 환경이 필요합니다. 로컬 개발에서는 mongod --replSet rs0으로 MongoDB를 시작해야 합니다.
인덱스 전략과 성능 최적화
적절한 인덱스 설계는 MongoDB 애플리케이션의 성능에 큰 영향을 미칩니다.
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
export type ProductDocument = HydratedDocument<Product>;
@Schema({
timestamps: true,
// 스키마 레벨에서 인덱스 정의
autoIndex: process.env.NODE_ENV !== 'production',
})
export class Product {
@Prop({ required: true, index: true })
name: string;
@Prop({ required: true })
price: number;
@Prop({ index: true })
category: string;
@Prop({ type: [String], index: true })
tags: string[];
}
export const ProductSchema = SchemaFactory.createForClass(Product);
// 복합 인덱스 정의
ProductSchema.index({ category: 1, price: -1 });
ProductSchema.index({ name: 'text', tags: 'text' });복합 인덱스는 여러 필드로 필터링하는 쿼리의 성능을 향상시킵니다. 텍스트 인덱스는 전문 검색을 가능하게 합니다.
기술 면접에서 자주 나오는 질문
NestJS와 MongoDB 관련 기술 면접에서 자주 출제되는 질문과 그 답변을 소개합니다.
Q1: MongoDB와 SQL 데이터베이스의 차이점은 무엇입니까?
MongoDB는 도큐먼트 지향 NoSQL 데이터베이스로 유연한 스키마를 가집니다. SQL 데이터베이스는 고정된 스키마와 ACID 트랜잭션을 가집니다. MongoDB는 수평 확장에 뛰어나며 비정형 데이터나 빠르게 변화하는 데이터 모델에 적합합니다.
Q2: 임베디드 도큐먼트와 참조 중 어떤 것을 사용해야 합니까?
임베디드 도큐먼트는 데이터가 함께 읽히고 부모 도큐먼트의 크기가 16MB를 초과하지 않을 때 사용합니다. 참조는 데이터가 독립적으로 업데이트되거나 다대다 관계가 있을 때 사용합니다.
Q3: Mongoose의 lean() 메서드는 무엇을 합니까?
lean()은 일반 JavaScript 객체를 반환하여 Mongoose 도큐먼트의 오버헤드를 피합니다. 읽기 전용 작업에서 성능이 크게 향상됩니다.
// lean()을 사용한 빠른 쿼리
const users = await this.userModel.find().lean().exec();Q4: 인덱스 선택 기준은 무엇입니까?
자주 쿼리되는 필드, 정렬에 사용되는 필드, 고유성 제약이 필요한 필드에 인덱스를 생성합니다. 단, 인덱스는 쓰기 성능을 저하시키므로 필요 최소한으로 유지해야 합니다.
Q5: 집계 파이프라인의 성능을 최적화하는 방법은?
$match와 $project를 파이프라인 초기에 사용하여 데이터 양을 줄입니다. allowDiskUse: true 옵션을 사용하면 100MB를 초과하는 집계 결과를 처리할 수 있습니다.
Node.js / NestJS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
프로덕션 환경 베스트 프랙티스
NestJS와 MongoDB를 프로덕션 환경에서 운영할 때의 베스트 프랙티스를 소개합니다.
// 프로덕션 환경용 연결 설정
MongooseModule.forRoot(process.env.MONGODB_URI, {
maxPoolSize: 50,
minPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
retryWrites: true,
w: 'majority',
});연결 풀 크기, 타임아웃, 쓰기 확인 설정을 적절히 조정하면 프로덕션 환경에서의 안정성과 성능이 향상됩니다.
결론
NestJS 12와 Mongoose 9의 조합은 현대적인 Node.js 백엔드 개발에서 강력한 선택입니다. 스키마 설계, 집계 파이프라인, 트랜잭션 처리에 대한 이해는 프로덕션 환경에서 신뢰성 있는 애플리케이션을 구축하는 데 필수적입니다. 기술 면접에서는 이러한 주제와 함께 성능 최적화 및 확장 전략에 대해서도 자주 질문받습니다. 실무 경험과 이론적 이해를 모두 갖추면 면접에서의 성공 확률이 높아집니다.
Node.js / NestJS 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 8월 28일 업데이트
태그
공유
관련 기사

NestJS + Prisma: Node.js를 위한 모던 백엔드 스택
NestJS와 Prisma로 모던한 백엔드 API를 구축하기 위한 완전한 가이드입니다. 설정, 모델, 서비스, 트랜잭션 및 모범 사례를 설명합니다.

NestJS: 완전한 REST API 구축 가이드
NestJS로 전문적인 REST API를 구축하는 완벽 가이드입니다. 컨트롤러, 서비스, 모듈 구성, class-validator를 활용한 유효성 검사, 에러 핸들링을 실전 코드로 설명합니다.

2026년 NestJS 마이크로서비스: 아키텍처, gRPC, 면접 질문 완벽 가이드
NestJS 마이크로서비스 아키텍처의 핵심 개념, gRPC 트랜스포트 구성, 스트리밍 패턴, 안정성 패턴, 면접 빈출 질문을 실무 중심으로 다룹니다.