Nest에는 SQL과 NoSQL 데이터베이스를 자유롭게 연결할 수 있습니다. 일반적으로 Nest에 데이터베이스를 연결하는 과정은 Express나 Fastify에 데이터베이스를 연결하기 위해 적절한 Node.js 설정을 하는 것과 동일합니다.
Nest는 TypeORM(@nestjs/typeorm
)과 Sequelize(@nestjs/sequelize
), Mongoose(@nestjs/mongoose
)를 제공합니다. 이를 통해 모델/리포지토리 인젝션, 테스트, 비동기 설정을 보다 간편하게 진행할 수 있습니다.
TypeORM 사용하기
SQL 또는 NoSQL 데이터베이스 조합을 위해 Nest는 @nestjs/typeorm
패키지를 제공합니다. TypeORM은 타입스크립트를 위한 가장 완성도 높은 ORM으로 간주되고 있습니다.
이를 시작하려면 필수 디펜던시를 먼저 설치해야 합니다. 여기서는 MySQL을 사용할 것이지만 PostgreSQL, Oracle, SQLite, MongoDB 등을 사용할 수도 있습니다.
TypeORM 설치 명령어는 다음과 같습니다.
$ npm install --save @nestjs/typeorm typeorm mysql2
설치가 완료되면 루트 appModule
에 TypeOrmModule
을 임포트할 수 있습니다.
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
],
})
export class AppModule {}
이 때 프로덕션 환경에서는 synchronize: true
를 비활성해야 합니다. 그렇지 않으면 프로덕션 데이터가 유실될 수 있습니다.
저는 연결 과정에서 아래와 같은 오류가 발생했는데요.
ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
다음과 같이 해결했습니다. 먼저, mysql에 접속하여 다음을 입력하여 cashing_sha2_password 속성을 변경해줍니다.
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
모든 호스트에 대해 열어주려면 다음과 같이 입력합니다.
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'password';
비밀번호 조건을 만족하지 않는다면 강력한 비밀번호를 설정하거나 또는 개발 환경에서 비밀번호 보안 강도를 낮출 수 있습니다.
set global validate_password.policy=LOW;
그리고 나서 이렇게 쳐보면 변경 사항을 확인할 수 있습니다.
SELECT Host,User,plugin,authentication_string FROM mysql.user;
연동은 됐으나 외부 ip에서 접속이 안될 때는 다음과 같이 권한을 설정해줍니다(도커 기준)
$ docker exec -it <mysql_container_id> bash
$ mysql -u root -p
$ CREATE USER 'root'@'%' IDENTIFIED BY 'password';
$ GRANT ALL ON *.* TO 'root'@'%';
여기까지 설정이 완료됐다면 이제 TypeORM DataSource
와 EntityManager
객체를 프로젝트에서 사용할 수 있습니다.
// app.module.ts
import { DataSource } from 'typeorm';
@Module({
imports: [TypeOrmModule.forRoot(), UsersModule],
})
export class AppModule {
constructor(private dataSource: DataSource) {}
}
리포지토리 패턴
TypeORM은 리포지토리(repository) 디자인 패턴을 지원하며 각 엔티티(entity)는 고유한 리포지토리를 가질 수 있습니다. 이러한 리포지토리는 데이터베이스 데이터 소스로부터 얻을 수 있습니다.
시작하려면 엔티티가 필요합니다. User
엔티티를 정의해보도록 하겠습니다.
// user.entity.ts
import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
User
엔티티 파일은 users
디렉토리 내에 있습니다. 해당 디렉토리에는 UserModule
과 연관된 모든 파일이 있습니다. 모델 파일을 저장할 곳을 지정할 수 있지만 모듈 디렉토리 내 해당 도메인 근처에 두는 게 좋습니다.
User
엔티티를 시작하려면 모듈 forRoot()
메소드 옵션 내 entities
배열에 이를 명시해줘야 합니다.
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [User],
synchronize: true,
}),
],
})
export class AppModule {}
이제 UserModule
을 살펴보겠습니다.
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
이 모듈은 forFeature()
메소드를 사용하여 현재 스코프에 등록된 레포지토리를 정의하고 있습니다. 여기서 우리는 @InjectRepository()
데코레이터를 사용하여 UsersRepository
를 UsersService
에 삽입할 수 있습니다.
// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
findOne(id: string): Promise<User> {
return this.usersRepository.findOneBy({ id });
}
async remove(id: string): Promise<void> {
await this.usersRepository.delete(id);
}
}
모듈 외부에서 리포지토리를 사용하고자 한다면 TypeOrmModule.forFeature
를 해야 하므로, 다음과 같이 프로바이더를 엑스포트할 수 있습니다.
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
exports: [TypeOrmModule]
})
export class UsersModule {}
이제 UserHttpModule
에 UsersModule
을 임포트 하려면 @InjectRepository(User)
를 사용할 수 있습니다.
// users-http.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
imports: [UsersModule],
providers: [UsersService],
controllers: [UsersController]
})
export class UserHttpModule {}
MongoDB 연결하기
MongoDB를 연결하고자 한다면 @nestjs/mongoose
패키지를 사용할 수 있습니다.
$ npm i @nestjs/mongoose mongoose
이후 다음과 같이 AppModule
에 MongooseModule
을 삽입해줍니다.
// app.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [MongooseModule.forRoot('mongodb://localhost/nest')],
})
export class AppModule {}
몽구스의 경우에는 모든 것이 스키마(Schema)에서 파생됩니다. 각 스키마는 MongoDB 컬렉션에 매핑되며, 컬렉션 내 도큐먼트 구조를 정의합니다. 스미카는 모델을 정의하는 데 사용됩니다. 모델은 해당 MongoDB 데이터베이스에서 도큐먼트를 생성 및 읽어오는 역할을 합니다.
위와 같이 데이터베이스를 연결했지만 데이터베이스 위치는 환경 변수를 통해 관리하는 것이 좋습니다. .env
파일을 생성한 다음 해당 URI를 불러와서 사용합니다.
먼저 npm i --save @nestjs/config
명령어를 통해 필요한 패키지를 설치해줍니다. 그리고 모듈 부분을 다음과 같이 수정해줍니다.
// app.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [MongooseModule.forRoot(process.env.MONGODB_URI)],
})
export class AppModule {}
다음으로 Schema()
데코레이터를 사용하여 스키마를 생성할 수 있습니다.
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type CatDocument = Cat & Document;
@Schema()
export class Cat {
@Prop()
name: string;
@Prop()
age: number;
@Prop()
breed: string;
}
export const CatSchema = SchemaFactory.createForClass(Cat);
@Shcema()
데코레이터는 스미카를 정의하는 클래스 역할을 합니다. 이는 우리의 Cat
클래스를 MongoDB 컬렉션에 연결해줍니다. MongoDB 컬렉션을 열어보면 "s"가 추가되어 컬렉션이 생성된 것을 알 수 있습니다.
@Prop()
데코레이터는 도큐먼트의 속성을 정의합니다. 예를 들어, 위의 스키마에서 우리는 name
, age
, breed
를 정의하고 있습니다. 타입스크립트 메타데이터를 통해 각 속성들은 자동으로 해당 스키마 유형을 참조하고 있습니다. 그러나 배열과 같이 보다 복잡한 자료 구조의 경우에는 타입을 별도로 명시해줘야 합니다.
@Prop([String])
tags: string[];
또한 해당 속성이 필수적인지 아닌지를 설정할 수 있습니다.
@Prop({ required: true })
name: string;
이제 CatsModule
을 살펴보도록 하겠습니다.
//cats.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { Cat, CatSchema } from './schemas/cat.schema';
@Module({
imports: [MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }])],
controllers: [CatsController],
providers: [CatsService],
exports: [MongooseModule],
})
export class CatsModule {}
MongooseModule
을 forFeature()
메소드를 제공하며 이를 통해 모듈을 구성할 수 있습니다. 또한 현재 스코프 내에서 어떤 모델이 등록되어야 하는지 정의합니다. 이 모델을 다른 모델에서 사용하고자 한다면 exports
에 MongooseModule
을 추가하고 다른 모듈에서 CatsModule
을 불러오면 됩니다.
이렇게 스키마까지 등록을 완료했다면 Cat
모델을 CatsService
에 @InjectModel()
데코레이터를 사용하여 삽입할 수 있습니다.
// cats.service.ts
import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
import { CreateCatDto } from './dto/create-cat.dto';
@Injectable()
export class CatsService {
constructor(@InjectModel(Cat.name) private catModel: Model<CatDocument>) {}
async create(createCatDto: CreateCatDto): Promise<Cat> {
const createdCat = new this.catModel(createCatDto);
return createdCat.save();
}
async findAll(): Promise<Cat[]> {
return this.catModel.find().exec();
}
}
마지막으로 MongoDB 쿼리를 터미널에 띄워주려면 다음과 같이 작성할 수 있습니다.
// app.module.ts
import { LoggerMiddleware } from './common/logger.middleware';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule } from '@nestjs/config';
import * as mongoose from 'mongoose';
@Module({
imports: [
ConfigModule.forRoot(),
MongooseModule.forRoot(process.env.MONGODB_URI),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {
configure() {
mongoose.set('debug', true);
}
}
참고 자료
'개발 > NestJS' 카테고리의 다른 글
NestJS 기초 (5) 프로바이더 (0) | 2022.10.04 |
---|---|
NestJS 기초 (3) 컨트롤러 (0) | 2022.09.30 |
NestJS 기초 (2) 프로젝트 시작하기 (0) | 2022.09.29 |