NestJS 데이터베이스 연결 설정하기

NestJS 데이터베이스 연동

NestJS는 다양한 데이터베이스와 ORM을 지원합니다. 가장 일반적으로 TypeORM, Prisma, Mongoose를 사용합니다.

TypeORM 설정

TypeORM은 TypeScript와 JavaScript를 위한 ORM으로, NestJS와 완벽하게 통합됩니다.

패키지 설치

# TypeORM과 MySQL
npm install @nestjs/typeorm typeorm mysql2

# PostgreSQL
npm install @nestjs/typeorm typeorm pg

# SQLite
npm install @nestjs/typeorm typeorm sqlite3

# MariaDB
npm install @nestjs/typeorm typeorm mariadb

# MSSQL
npm install @nestjs/typeorm typeorm mssql

기본 설정

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: 'password',
database: 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true, // 개발 환경에서만 사용
}),
],
})
export class AppModule {}

환경 변수 사용

.env

DB_TYPE=mysql
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=password
DB_DATABASE=test

app.module.ts (ConfigModule 사용)

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: configService.get('DB_TYPE') as any,
host: configService.get('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: configService.get('NODE_ENV') !== 'production',
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}

Entity 정의

user.entity.ts

import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';

@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column({ length: 100 })
name: string;

@Column({ unique: true })
email: string;

@Column()
password: string;

@Column({ nullable: true })
age: number;

@Column({ default: true })
isActive: boolean;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}

컬럼 옵션

@Entity()
export class Product {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'varchar', length: 200 })
name: string;

@Column({ type: 'decimal', precision: 10, scale: 2 })
price: number;

@Column({ type: 'text', nullable: true })
description: string;

@Column({ type: 'int', default: 0 })
stock: number;

@Column({ type: 'simple-array' })
tags: string[];

@Column({ type: 'json', nullable: true })
metadata: object;

@Column({ type: 'enum', enum: ['draft', 'published', 'archived'], default: 'draft' })
status: string;
}

관계(Relations) 설정

One-to-Many / Many-to-One

// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Post } from '../posts/post.entity';

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@OneToMany(() => Post, post => post.user)
posts: Post[];
}

// post.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { User } from '../users/user.entity';

@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;

@Column()
title: string;

@Column('text')
content: string;

@ManyToOne(() => User, user => user.posts)
user: User;
}

Many-to-Many

// post.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Tag } from '../tags/tag.entity';

@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;

@Column()
title: string;

@ManyToMany(() => Tag, tag => tag.posts)
@JoinTable()
tags: Tag[];
}

// tag.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
import { Post } from '../posts/post.entity';

@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@ManyToMany(() => Post, post => post.tags)
posts: Post[];
}

One-to-One

// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { Profile } from '../profiles/profile.entity';

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column()
email: string;

@OneToOne(() => Profile, profile => profile.user)
@JoinColumn()
profile: Profile;
}

// profile.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm';
import { User } from '../users/user.entity';

@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;

@Column()
bio: string;

@Column({ nullable: true })
avatar: string;

@OneToOne(() => User, user => user.profile)
user: User;
}

Repository 사용

모듈에 Entity 등록

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './user.entity';

@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}

Service에서 Repository 사용

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}

async findAll(): Promise<User[]> {
return this.usersRepository.find();
}

async findOne(id: number): Promise<User> {
const user = await this.usersRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}

async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}

async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
await this.usersRepository.update(id, updateUserDto);
return this.findOne(id);
}

async remove(id: number): Promise<void> {
const result = await this.usersRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`User with ID ${id} not found`);
}
}
}

고급 쿼리

조건 검색

// 단일 조건
const users = await this.usersRepository.find({
where: { isActive: true },
});

// 여러 조건 (AND)
const users = await this.usersRepository.find({
where: { isActive: true, age: 25 },
});

// 여러 조건 (OR)
const users = await this.usersRepository.find({
where: [{ age: 25 }, { age: 30 }],
});

// 특정 필드만 선택
const users = await this.usersRepository.find({
select: ['id', 'name', 'email'],
});

// 정렬
const users = await this.usersRepository.find({
order: { createdAt: 'DESC' },
});

// 페이지네이션
const users = await this.usersRepository.find({
skip: 0,
take: 10,
});

관계 로딩

// Eager Loading
const users = await this.usersRepository.find({
relations: ['posts', 'profile'],
});

// 중첩 관계
const users = await this.usersRepository.find({
relations: ['posts', 'posts.tags'],
});

// 특정 관계만
const user = await this.usersRepository.findOne({
where: { id: 1 },
relations: ['posts'],
});

QueryBuilder 사용

// 기본 사용
const users = await this.usersRepository
.createQueryBuilder('user')
.where('user.age > :age', { age: 18 })
.getMany();

// 복잡한 조건
const users = await this.usersRepository
.createQueryBuilder('user')
.where('user.age BETWEEN :min AND :max', { min: 18, max: 65 })
.andWhere('user.isActive = :isActive', { isActive: true })
.orderBy('user.createdAt', 'DESC')
.take(10)
.getMany();

// JOIN
const users = await this.usersRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.where('post.status = :status', { status: 'published' })
.getMany();

// 집계
const count = await this.usersRepository
.createQueryBuilder('user')
.where('user.age > :age', { age: 18 })
.getCount();

// Raw 쿼리
const users = await this.usersRepository.query(
'SELECT * FROM users WHERE age > ?',
[18],
);

트랜잭션

QueryRunner 사용

import { DataSource } from 'typeorm';

@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private dataSource: DataSource,
) {}

async createUserWithProfile(userData: any, profileData: any) {
const queryRunner = this.dataSource.createQueryRunner();

await queryRunner.connect();
await queryRunner.startTransaction();

try {
const user = queryRunner.manager.create(User, userData);
await queryRunner.manager.save(user);

const profile = queryRunner.manager.create(Profile, {
...profileData,
user,
});
await queryRunner.manager.save(profile);

await queryRunner.commitTransaction();
return user;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
}

Transaction 데코레이터 사용

import { Transaction, TransactionRepository } from 'typeorm';

@Injectable()
export class UsersService {
@Transaction()
async createUserWithProfile(
@TransactionRepository(User) userRepository: Repository<User>,
@TransactionRepository(Profile) profileRepository: Repository<Profile>,
userData: any,
profileData: any,
) {
const user = await userRepository.save(userData);
const profile = await profileRepository.save({ ...profileData, user });
return { user, profile };
}
}

MongoDB (Mongoose) 설정

패키지 설치

npm install @nestjs/mongoose mongoose

모듈 설정

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/nest'),
],
})
export class AppModule {}

Schema 정의

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type UserDocument = User & Document;

@Schema()
export class User {
@Prop({ required: true })
name: string;

@Prop({ required: true, unique: true })
email: string;

@Prop()
age: number;

@Prop({ default: Date.now })
createdAt: Date;
}

export const UserSchema = SchemaFactory.createForClass(User);

Service에서 사용

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from './user.schema';

@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
) {}

async create(createUserDto: any): Promise<User> {
const createdUser = new this.userModel(createUserDto);
return createdUser.save();
}

async findAll(): Promise<User[]> {
return this.userModel.find().exec();
}

async findOne(id: string): Promise<User> {
return this.userModel.findById(id).exec();
}

async update(id: string, updateUserDto: any): Promise<User> {
return this.userModel.findByIdAndUpdate(id, updateUserDto, { new: true }).exec();
}

async remove(id: string): Promise<User> {
return this.userModel.findByIdAndRemove(id).exec();
}
}

Prisma 설정

패키지 설치

npm install @prisma/client
npm install -D prisma

Prisma 초기화

npx prisma init

Schema 정의 (prisma/schema.prisma)

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

generator client {
provider = "prisma-client-js"
}

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

Prisma Service

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}

사용 예제

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}

async findAll() {
return this.prisma.user.findMany();
}

async findOne(id: number) {
return this.prisma.user.findUnique({ where: { id } });
}

async create(data: any) {
return this.prisma.user.create({ data });
}

async update(id: number, data: any) {
return this.prisma.user.update({ where: { id }, data });
}

async remove(id: number) {
return this.prisma.user.delete({ where: { id } });
}
}

Migration

TypeORM Migration

# Migration 생성
npm run typeorm migration:create -- -n CreateUsersTable

# Migration 실행
npm run typeorm migration:run

# Migration 되돌리기
npm run typeorm migration:revert

Prisma Migration

# Migration 생성 및 적용
npx prisma migrate dev --name init

# Production에 적용
npx prisma migrate deploy

# Schema 변경 후 재생성
npx prisma generate

정리

  • TypeORM: SQL 데이터베이스를 위한 강력한 ORM
  • Mongoose: MongoDB를 위한 ODM
  • Prisma: 현대적인 타입 안전 ORM
  • Entity/Schema로 데이터 모델 정의
  • Repository 패턴으로 데이터 액세스
  • 관계(Relations)로 테이블 간 연결
  • QueryBuilder로 복잡한 쿼리 작성
  • Transaction으로 데이터 일관성 보장

다음 단계

데이터베이스 연결을 마스터했다면:

  • 검증 파이프로 데이터 유효성 검사하기
  • 미들웨어, 가드, 인터셉터 활용하기
  • JWT 인증 구현하기
  • API 문서화 (Swagger) 설정하기
Share