본문 바로가기

Mongoose를 활용한 MongoDB 연동

MongoDB의 문서 모델과 Mongoose 스키마를 이해하고 모델 주입부터 기본 데이터 작업까지 NestJS에 연결합니다.

관계형 데이터베이스와 TypeORM에 이어, 이 글에서는 NoSQL 데이터베이스인 MongoDB를 NestJS에서 쓰는 방법을 다룹니다. 관계형 데이터베이스가 엄격한 스키마와 트랜잭션으로 정합성을 강조한다면, NoSQL 데이터베이스는 유연한 스키마, 수평 확장, 대량의 비정형 데이터 처리에 강점이 있습니다. MongoDB는 JSON과 비슷한 BSON(Binary JSON) 형식으로 데이터를 저장하는 문서(document) 기반 데이터베이스라서, 웹 애플리케이션에서 직관적으로 쓸 수 있습니다.

NestJS에서는 주로 Mongoose 라이브러리와 @nestjs/mongoose 패키지로 MongoDB를 연결하며, 이 교재는 Mongoose 9와 @nestjs/mongoose 12를 기준으로 합니다.

연결과 스키마가 모델을 만들고 서비스가 문서를 다룬다

연결/스키마에서 Model을 만든 뒤 HTTP 값이 service/model을 거쳐 문서로 이동하며 원문에 없는 사전 pipe를 추가하지 않는다.

Nest Mongoose · 모델과 문서

연결과 Schema로 Model을 등록하고, 서비스가 문서를 조회·저장합니다

forRoot는 연결 URI를, forFeature는 모델 이름과 Schema를 등록합니다. 본문의 inline body 타입은 HTTP 런타임 검증이 아니며, Mongoose의 unique 선언은 validator 대신 실제 MongoDB 인덱스의 조건입니다.

연결과 스키마가 모델을 만들고 서비스가 문서를 다룬다 MongoDB 연결과 스키마가 모델을 만들고 HTTP 입력이 서비스를 통해 모델에 전달된다. 원문에는 HTTP 바디·ObjectId 사전 검증 파이프가 없고, 스키마 검증과 데이터베이스 인덱스는 별개다. CONFIG MongoDB 연결 URI BOOTSTRAP Mongoose connection RUNTIME CONTRACT Schema validators index metadata 별도 INJECTED TOKEN Model<User> 쿼리 · 생성 API HTTP BOUNDARY 검증 단계 추가 필요 USE CASE Nest service INSTANCE HydratedDocument <User> save · change tracking MONGODB collection + unique index unique는 validator가 아님 Model 정의 · HydratedDocument 인스턴스 · 실제 unique index를 구분합니다
스키마와 모델 등록 뒤 MongoDB에 쓰는 과정Schema는 필드 규칙과 인덱스 설정을 선언한다. forFeature에서 name으로 등록한 Model을 같은 이름의 InjectModel 토큰으로 서비스에 주입한다. 서비스는 Model로 검증과 DB 쓰기를 실행한다. required 검증 통과와 유일 인덱스의 중복 쓰기 거부는 별도 단계다.
Schema 선언필드 · 검증 · 인덱스
연결 관계Schema 선언 · 필드 · 검증 · 인덱스 → Model 등록 · forFeature의 name · InjectModel 같은 토큰
Model 등록forFeature의 nameInjectModel 같은 토큰
연결 관계Model 등록 · forFeature의 name · InjectModel 같은 토큰 → UsersService · 주입된 Model 호출
UsersService주입된 Model 호출
연결 관계UsersService · 주입된 Model 호출 → Mongoose Model · 검증 뒤 쿼리 실행
Mongoose Model검증 뒤 쿼리 실행
연결 관계Mongoose Model · 검증 뒤 쿼리 실행 → MongoDB · 문서 · 유일 인덱스
MongoDB문서 · 유일 인덱스
  1. 연결

    원문은 예제 URI와 mongo:latest를 사용합니다. 운영에서는 버전과 자격 증명을 별도로 관리해야 합니다.

  2. 스키마와 모델

    forFeature가 모델 이름과 스키마를 등록하고 InjectModel이 모델을 주입합니다. 스키마의 validator와 인덱스 메타데이터가 연결과 합쳐져 Model<User> 토큰이 됩니다.

  3. HTTP 검증

    원문은 바디 필드와 id를 직접 넘깁니다. 런타임 바디 검증과 ObjectId 오류 처리를 별도로 추가해야 합니다.

  4. 문서 실행

    모델 쿼리는 HydratedDocument<User>를 반환할 수 있으며 모델 shape와 문서 타입을 구분합니다.

  5. 인덱스 집행

    unique 선언은 validator가 아니라 MongoDB 인덱스이므로 실제 인덱스 빌드와 중복 키 오류를 관찰합니다.

같은 값의 두 문서는 로컬 validate를 모두 통과할 수 있다. unique는 MongoDB 인덱스가 쓰기 시점에 집행하며, 중복 키 오류를 도메인 충돌로 매핑하려면 별도 처리가 필요하다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • MongoDB 연결과 스키마가 모델을 만들고 HTTP 입력이 서비스를 통해 모델에 전달된다. 원문에는 HTTP 바디·ObjectId 사전 검증 파이프가 없고, 스키마 검증과 데이터베이스 인덱스는 별개다.
  • Schema는 필드 규칙과 인덱스 설정을 선언한다. forFeature에서 name으로 등록한 Model을 같은 이름의 InjectModel 토큰으로 서비스에 주입한다. 서비스는 Model로 검증과 DB 쓰기를 실행한다. required 검증 통과와 유일 인덱스의 중복 쓰기 거부는 별도 단계다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
모델 조립

Mongo 연결과 스키마에서 Model을 만듭니다.

입력: 연결·schema · 산출물: Model문서 접근 역할이 준비됩니다.
service 입력

HTTP 값을 service를 통해 Model에 전달합니다.

사전 ObjectId pipe: 현재 없음 · 입력: HTTP 값없는 사전 검증을 추가하지 않고 경계를 넘습니다.
문서 처리

Model이 문서를 다룹니다.

검증: schema · index: DB 별도 제약모델 검증과 저장소 제약을 구분합니다.

연결과 스키마가 모델을 만들고 서비스가 문서를 다룬다

연결/스키마에서 Model을 만든 뒤 HTTP 값이 service/model을 거쳐 문서로 이동하며 원문에 없는 사전 pipe를 추가하지 않는다.

Nest Mongoose · 모델과 문서

연결과 Schema로 Model을 등록하고, 서비스가 문서를 조회·저장합니다

forRoot는 연결 URI를, forFeature는 모델 이름과 Schema를 등록합니다. 본문의 inline body 타입은 HTTP 런타임 검증이 아니며, Mongoose의 unique 선언은 validator 대신 실제 MongoDB 인덱스의 조건입니다.

연결과 스키마가 모델을 만들고 서비스가 문서를 다룬다 MongoDB 연결과 스키마가 모델을 만들고 HTTP 입력이 서비스를 통해 모델에 전달된다. 원문에는 HTTP 바디·ObjectId 사전 검증 파이프가 없고, 스키마 검증과 데이터베이스 인덱스는 별개다. CONFIG MongoDB 연결 URI BOOTSTRAP Mongoose connection RUNTIME CONTRACT Schema validators index metadata 별도 INJECTED TOKEN Model<User> 쿼리 · 생성 API HTTP BOUNDARY 검증 단계 추가 필요 USE CASE Nest service INSTANCE HydratedDocument <User> save · change tracking MONGODB collection + unique index unique는 validator가 아님 Model 정의 · HydratedDocument 인스턴스 · 실제 unique index를 구분합니다
스키마와 모델 등록 뒤 MongoDB에 쓰는 과정Schema는 필드 규칙과 인덱스 설정을 선언한다. forFeature에서 name으로 등록한 Model을 같은 이름의 InjectModel 토큰으로 서비스에 주입한다. 서비스는 Model로 검증과 DB 쓰기를 실행한다. required 검증 통과와 유일 인덱스의 중복 쓰기 거부는 별도 단계다.
Schema 선언필드 · 검증 · 인덱스
연결 관계Schema 선언 · 필드 · 검증 · 인덱스 → Model 등록 · forFeature의 name · InjectModel 같은 토큰
Model 등록forFeature의 nameInjectModel 같은 토큰
연결 관계Model 등록 · forFeature의 name · InjectModel 같은 토큰 → UsersService · 주입된 Model 호출
UsersService주입된 Model 호출
연결 관계UsersService · 주입된 Model 호출 → Mongoose Model · 검증 뒤 쿼리 실행
Mongoose Model검증 뒤 쿼리 실행
연결 관계Mongoose Model · 검증 뒤 쿼리 실행 → MongoDB · 문서 · 유일 인덱스
MongoDB문서 · 유일 인덱스
  1. 연결

    원문은 예제 URI와 mongo:latest를 사용합니다. 운영에서는 버전과 자격 증명을 별도로 관리해야 합니다.

  2. 스키마와 모델

    forFeature가 모델 이름과 스키마를 등록하고 InjectModel이 모델을 주입합니다. 스키마의 validator와 인덱스 메타데이터가 연결과 합쳐져 Model<User> 토큰이 됩니다.

  3. HTTP 검증

    원문은 바디 필드와 id를 직접 넘깁니다. 런타임 바디 검증과 ObjectId 오류 처리를 별도로 추가해야 합니다.

  4. 문서 실행

    모델 쿼리는 HydratedDocument<User>를 반환할 수 있으며 모델 shape와 문서 타입을 구분합니다.

  5. 인덱스 집행

    unique 선언은 validator가 아니라 MongoDB 인덱스이므로 실제 인덱스 빌드와 중복 키 오류를 관찰합니다.

같은 값의 두 문서는 로컬 validate를 모두 통과할 수 있다. unique는 MongoDB 인덱스가 쓰기 시점에 집행하며, 중복 키 오류를 도메인 충돌로 매핑하려면 별도 처리가 필요하다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • MongoDB 연결과 스키마가 모델을 만들고 HTTP 입력이 서비스를 통해 모델에 전달된다. 원문에는 HTTP 바디·ObjectId 사전 검증 파이프가 없고, 스키마 검증과 데이터베이스 인덱스는 별개다.
  • Schema는 필드 규칙과 인덱스 설정을 선언한다. forFeature에서 name으로 등록한 Model을 같은 이름의 InjectModel 토큰으로 서비스에 주입한다. 서비스는 Model로 검증과 DB 쓰기를 실행한다. required 검증 통과와 유일 인덱스의 중복 쓰기 거부는 별도 단계다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
모델 조립

Mongo 연결과 스키마에서 Model을 만듭니다.

입력: 연결·schema · 산출물: Model문서 접근 역할이 준비됩니다.
service 입력

HTTP 값을 service를 통해 Model에 전달합니다.

사전 ObjectId pipe: 현재 없음 · 입력: HTTP 값없는 사전 검증을 추가하지 않고 경계를 넘습니다.
문서 처리

Model이 문서를 다룹니다.

검증: schema · index: DB 별도 제약모델 검증과 저장소 제약을 구분합니다.

그림은 연결 URI로 Mongoose 연결을 맺고, 스키마로 모델을 등록한 뒤, 서비스가 주입받은 Model<User>로 문서를 조회하고 저장하는 흐름입니다. forRoot()는 연결 URI를, forFeature()는 모델 이름과 스키마를 등록하며, forFeature()의 name과 @InjectModel()의 토큰이 같아야 서비스에 모델이 주입됩니다. 그림은 이 글의 예제에 HTTP 본문과 ObjectId를 미리 검사하는 파이프가 아직 없다는 점, 스키마의 required 검증과 데이터베이스의 유일 인덱스(unique index)가 서로 다른 단계라는 점도 함께 보여 줍니다.


MongoDB와 Mongoose

MongoDB는 데이터를 컬렉션(collection) 안에 문서 형태로 저장합니다. 문서는 필드와 값의 쌍으로 이루어진 JSON 객체와 비슷한 구조이며, 관계형 데이터베이스와 비교하면 테이블은 컬렉션에, 행은 문서에, 열은 문서의 필드에 해당합니다.

Mongoose는 Node.js에서 MongoDB를 더 쉽게 쓰도록 돕는 객체 데이터 모델링(ODM, Object Data Modeling) 라이브러리입니다. TypeORM이 관계형 데이터베이스를 위한 ORM이라면, Mongoose는 MongoDB를 위한 ODM이라고 보면 됩니다. 스키마 정의, 데이터 유효성 검사, 쿼리 빌더, 미들웨어 기능을 제공해 MongoDB 작업을 더 구조적으로 할 수 있게 해 줍니다.

MongoDB를 쓴다고 원자성을 포기하는 것은 아닙니다. MongoDB는 한 문서에 대한 쓰기를 원자적으로 처리하며, 여러 문서의 변경을 한 단위로 묶는 트랜잭션은 레플리카 세트(replica set)나 샤드 클러스터처럼 지원되는 구성에서 쓸 수 있습니다. SQL과 NoSQL이라는 이름만으로 트랜잭션 유무를 판단하지 말고, 함께 바뀌어야 할 데이터와 실제 DB 구성을 먼저 확인합니다.

MongoDB와 Mongoose의 장점은 이렇습니다. 데이터 구조가 고정되어 있지 않아 바뀌는 요구에 빨리 대응할 수 있으면서도, 필요하면 Mongoose 스키마로 구조를 강제할 수 있습니다. 샤딩(sharding)으로 대량의 데이터를 나눠 저장하고 처리할 수 있고, JSON 형태의 문서는 프런트엔드와 백엔드 사이의 데이터 전달을 쉽게 하며 자바스크립트 개발자에게 익숙합니다. Mongoose는 유효성 검사, 저장 전후의 훅(pre/post hook), 쿼리 도우미 같은 편의 기능도 제공합니다.


스키마와 모델

스키마

Mongoose의 스키마(schema)는 컬렉션에 저장할 문서의 구조, 데이터 타입, 유효성 규칙, 기본값을 정의합니다. 관계형 데이터베이스의 테이블 스키마와 비슷하지만, NoSQL의 유연함을 유지하면서 데이터의 일관성을 확보하는 데 도움을 줍니다. NestJS에서는 @Schema() 데코레이터로 정의합니다.

src/users/schemas/user.schema.ts
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() 데코레이터로 문서의 필드를 정의하고, 타입, 필수 여부, 유일 인덱스, 기본값 등을 설정합니다. 여기서 required는 Mongoose의 검증 규칙이지만, unique: true는 검증기가 아니라 MongoDB의 유일 인덱스를 만드는 설정입니다. 인덱스가 실제로 만들어진 상태에서 같은 값을 저장하면 DB의 중복 키(duplicate key) 오류가 나며, 이것을 Mongoose의 ValidationError와 같은 오류로 다뤄서는 안 됩니다. 테스트에서 중복을 검사할 때도 인덱스가 준비됐는지부터 확인합니다. Mongoose 검증 문서는 이 차이를 따로 설명합니다.

모델

모델(model)은 특정 스키마로 정의한 컬렉션을 다루는 인터페이스입니다. 모델로 데이터베이스에 쿼리를 실행하거나 새 문서를 만들며, TypeORM의 리포지토리처럼 실제 데이터베이스 작업을 수행하는 주체라고 할 수 있습니다. NestJS에서는 @InjectModel() 데코레이터로 모델을 주입받습니다.

src/users/users.service.ts
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의 Mongoose 통합 모듈을 설치합니다. Mongoose 9는 Node.js 20.19 이상이 필요하며, 이 교재의 Node.js 24는 이 조건을 만족합니다.

npm install @nestjs/mongoose mongoose

로컬에 MongoDB가 없다면 Docker나 MongoDB Atlas 같은 클라우드 서비스를 쓰는 것이 편리합니다. 로컬에서 가장 간단한 방법은 Docker Compose이며, 현재의 Compose 명세에서는 최상위 version 필드가 더 이상 쓰이지 않으므로 생략합니다. 이미지는 주 버전을 고정해 두면 다시 만들 때 버전이 바뀌지 않습니다.

docker-compose.yml 예시
services:
  mongodb:
    image: mongo:8
    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 컨테이너를 실행합니다. 다음으로 루트 모듈의 imports에 MongooseModule.forRoot()로 연결 정보를 설정합니다.

src/app.module.ts
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 {}

연결 URI는 mongodb://사용자명:비밀번호@호스트:포트/데이터베이스?authSource=admin 형식입니다. authSource=admin은 인증에 쓸 데이터베이스를 지정하는 것으로, Docker에서 루트 사용자로 인증할 때 주로 씁니다. 운영 환경에서는 이런 인증 정보를 코드에 적지 말고, 환경 변수 관리와 비밀 정보 보호에서처럼 바깥에서 주입합니다.

마지막으로 각 도메인의 모듈에서 MongooseModule.forFeature()로 그 모듈이 쓸 스키마와 모델을 등록합니다.

src/users/users.module.ts
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 {}

스키마 정의, 모듈 등록, 모델 주입이 모두 같은 컬렉션과 스키마를 가리키는지 먼저 확인합니다.

더 깊이: 유일 인덱스는 언제 만들어질까

unique: true가 뜻하는 인덱스는 스키마를 선언할 때가 아니라 애플리케이션이 DB에 연결한 뒤에 만들어집니다. Mongoose 문서에 따르면 Mongoose는 애플리케이션이 시작할 때 스키마에 정의된 인덱스마다 createIndex를 호출하고, 생성이 끝나거나 실패하면 모델에서 index 이벤트를 냅니다. 인덱스가 아직 없는 동안에는 같은 username의 저장이 오류 없이 성공할 수 있습니다. 그래서 테스트에서 중복 저장을 확인할 때는 모델의 init()이 돌려주는 Promise를 기다려 인덱스가 준비된 뒤에 시작하며, Mongoose FAQ는 init()을 여러 번 호출해도 안전하다고 설명합니다.

운영 환경에서는 이 자동 생성을 끄라는 것이 Mongoose의 권장입니다. 인덱스 생성이 성능에 큰 영향을 줄 수 있기 때문이며, 스키마나 연결의 autoIndex 옵션을 false로 두고 인덱스는 배포 절차에서 MongoDB 셸 등으로 따로 만듭니다. 이때 컬렉션에 이미 제약을 어기는 문서가 있으면 MongoDB는 유일 인덱스를 만들지 못합니다. 인덱스 필드가 빠진 문서도 인덱스에는 null로 기록되므로, 그 필드가 없는 문서가 둘 이상이어도 인덱스 생성이 중복 키 오류로 실패합니다.


기본 데이터 작업

이제 MongoDB와 연결된 서비스를 컨트롤러에서 써 봅니다. 아래 네 핸들러는 각각 서비스의 create, findAll, findOne, delete를 그대로 호출하고, 그 결과를 응답으로 돌려줍니다. 경로의 :id에는 MongoDB가 문서마다 붙이는 _id 값을 문자열로 넣습니다.

src/users/users.controller.ts
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);
  }
}

DB 연결과 위 모듈 등록이 끝난 뒤 POST /users에 {"username":"Alice","email":"[email protected]"}를 보내고, 응답의 _id를 기록합니다. GET /users/<기록한 ID>의 이름과 이메일이 같은지 확인한 뒤 삭제 요청을 보냅니다. _id와 생성 시각은 실행할 때마다 달라지므로, 교재의 고정값과 비교하지 않습니다.

이 컨트롤러의 본문 타입 표기는 런타임 DTO 검증을 실행하지 않습니다. 또 findOne()이 없는 문서에 null을 돌려주는 것과 HTTP 404로 응답하는 것은 다른 일입니다. ObjectId 형식이 잘못된 경우의 변환 오류, ID는 유효하지만 문서가 없는 경우, 삭제 결과의 deletedCount를 각각 확인해 API 정책을 정해야 하며, 지금의 발췌는 그 오류 변환까지 구현하지 않았습니다.

MongoDB는 비정형 데이터가 많거나 스키마가 자주 바뀌는 서비스에서 검토할 만한 선택지입니다. NestJS와 함께 쓸 때는 스키마 정의, 인덱스, 검증의 위치를 분명히 정해 둡니다.


정리

  • MongoDB는 컬렉션에 문서를 저장하고, Mongoose는 스키마와 모델로 그 문서를 구조적으로 다루게 해 주는 ODM입니다.
  • forRoot()는 연결을, 피처 모듈의 forFeature()는 모델을 등록하며, @InjectModel()의 이름이 등록한 이름과 같아야 합니다.
  • required는 Mongoose의 검증이고 unique는 MongoDB의 인덱스이므로, 중복 저장은 검증이 아니라 중복 키 오류로 실패합니다.
  • 없는 문서의 null, 잘못된 ObjectId의 변환 오류, 삭제 건수는 각각 HTTP 응답 정책을 따로 정해야 합니다.

연습 문제

1. 검증과 인덱스

username 유일 인덱스가 준비된 DB에서, 이름과 이메일이 모두 있는 문서 두 개를 같은 username: 'Alice'로 차례로 저장합니다. required 검증을 둘 다 통과하면 두 문서가 모두 저장되나요? 실패를 HTTP 409로 전달하려면 무엇을 더 구현해야 하나요?

해설 보기

필수 값이 있다는 검증과 값이 유일하다는 조건은 다릅니다. 첫 문서가 저장된 뒤 두 번째 쓰기는 유일 인덱스에 걸려 중복 키 오류로 실패합니다.

서비스나 예외 필터가 그 DB 오류를 식별해 공개할 HTTP 오류로 바꿔야 하며, unique: true나 TypeScript의 타입 표기만으로 409 응답이 만들어지지는 않습니다.

2. 세 가지 ID로 조회하기

위 컨트롤러와 서비스를 그대로 두고 GET /users/:id를 세 번 보냅니다. 첫째는 저장된 문서의 ID, 둘째는 형식은 맞지만 없는 문서의 ObjectId, 셋째는 abc입니다. 별도의 예외 필터는 없습니다. 각 요청의 응답 상태와 본문은 어떻게 되나요?

해설 보기

첫째는 해당 문서를 JSON으로 돌려주며 200입니다. 둘째는 findById()가 null을 돌려주고, Express 어댑터는 null인 본문을 빈 응답으로 보내므로 200에 빈 본문이 됩니다. 클라이언트 입장에서는 없는 문서인데도 성공처럼 보입니다.

셋째는 abc를 ObjectId로 바꿀 수 없어 Mongoose가 CastError를 던지고, 이것은 HttpException이 아니므로 기본 예외 처리가 500으로 응답합니다. 실제 API라면 파이프로 ID 형식을 먼저 검사해 400을, 서비스에서 null을 확인해 404를 돌려주도록 정해야 합니다.

3. 저장 전 훅의 작성 방식

Mongoose 8에서 쓰던 schema.pre('save', function (next) { /* 작업 */ next(); })를 Mongoose 9 프로젝트에 그대로 옮겼습니다. 무엇이 문제이며, 어떻게 고쳐야 하나요?

해설 보기

Mongoose 9 이전 안내에 따르면, Mongoose 9의 pre 미들웨어는 더 이상 next() 매개변수를 받지 않습니다. 그래서 next를 호출하는 이 방식은 동작하지 않습니다.

비동기 작업이 있다면 schema.pre('save', async function () { /* 작업 */ })처럼 async 함수로 쓰거나 Promise를 돌려주게 고칩니다. 버전을 올릴 때는 이런 훅의 작성 방식이 바뀌었는지 이전 안내를 먼저 확인합니다.

다음 글 Prisma를 이용한 데이터베이스 작업에서는 스키마 파일에서 타입이 있는 클라이언트를 생성하는 Prisma로 같은 사용자 데이터를 다뤄 봅니다.