TypeORM을 사용한 데이터베이스 연동
NestJS와 TypeORM을 함께 사용하면 타입 안전성과 강력한 ORM 기능을 결합하여 효율적인 데이터베이스 연동을 구현할 수 있습니다.
TypeORM 설정
- 필요한 패키지 설치
npm install @nestjs/typeorm typeorm mysql2
app.module.ts
파일에 TypeORM 모듈 추가
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 {}
주의 : synchronize: true
는 개발 환경에서만 사용하세요.
엔티티 정의
엔티티는 데이터베이스 테이블을 표현하는 클래스입니다.
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column({ default: true })
isActive: boolean;
}
관계 설정
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Post } from './post.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(() => Post, post => post.user)
posts: Post[];
}
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToOne(() => User, user => user.posts)
user: User;
}
Repository 패턴 활용
NestJS 서비스에 Repository를 주입하여 사용합니다.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.userRepository.find();
}
}
모듈에 Repository 등록
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UserService } from './user.service';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserService],
})
export class UserModule {}
CRUD 작업 구현
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
create(createUserDto: CreateUserDto): Promise<User> {
const user = this.userRepository.create(createUserDto);
return this.userRepository.save(user);
}
findAll(): Promise<User[]> {
return this.userRepository.find();
}
findOne(id: number): Promise<User> {
return this.userRepository.findOne(id);
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
await this.userRepository.update(id, updateUserDto);
return this.userRepository.findOne(id);
}
async remove(id: number): Promise<void> {
await this.userRepository.delete(id);
}
}
고급 쿼리 기능
쿼리 빌더 사용
findActiveUsers(): Promise<User[]> {
return this.userRepository.createQueryBuilder('user')
.where('user.isActive = :isActive', { isActive: true })
.getMany();
}
조인 사용
findUsersWithPosts(): Promise<User[]> {
return this.userRepository.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.getMany();
}
서브쿼리
findUsersWithManyPosts(): Promise<User[]> {
return this.userRepository.createQueryBuilder('user')
.where(qb => {
const subQuery = qb.subQuery()
.select('user.id')
.from(User, 'user')
.leftJoin('user.posts', 'post')
.groupBy('user.id')
.having('COUNT(post.id) > :count', { count: 5 })
.getQuery();
return 'user.id IN ' + subQuery;
})
.getMany();
}
마이그레이션 관리
- TypeORM CLI 설정 (
package.json
)
"scripts": {
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
}
- 마이그레이션 생성
npm run typeorm migration:create -- -n UserTable
- 마이그레이션 실행
npm run typeorm migration:run
성능 최적화
N+1 문제 해결
관계가 있는 엔티티를 조회할 때 leftJoinAndSelect
를 사용하여 한 번의 쿼리로 데이터를 가져옵니다.
findAllWithRelations(): Promise<User[]> {
return this.userRepository.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.getMany();
}
인덱스 사용
자주 조회하는 컬럼에 인덱스를 추가합니다.
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Index()
@Column()
email: string;
}
Best Practices와 주의사항
- 엔티티 설계 : 데이터베이스 스키마를 정확히 반영하도록 엔티티를 설계하세요.
- 리포지토리 패턴 : 직접
Connection
을 사용하기보다는 리포지토리 패턴을 활용하세요. - 트랜잭션 사용 : 여러 연산이 하나의 단위로 처리되어야 할 때는 트랜잭션을 사용하세요.
async createUserWithPosts(userData: CreateUserDto, postsData: CreatePostDto[]) {
return this.connection.transaction(async manager => {
const user = await manager.save(User, userData);
const posts = postsData.map(postData => ({
...postData,
user: user
}));
await manager.save(Post, posts);
return user;
});
}
- Eager Loading 주의 :
eager: true
옵션은 성능 문제를 일으킬 수 있으므로 신중히 사용하세요. - Cascading 사용 : 관계 엔티티의 삭제나 업데이트를 자동화하기 위해 cascading을 활용하세요.
- 벌크 연산 활용 : 대량의 데이터를 처리할 때는 벌크 연산을 사용하세요.
async deactivateOldUsers() {
await this.userRepository.createQueryBuilder()
.update(User)
.set({ isActive: false })
.where('lastLoginDate < :date', { date: new Date('2023-01-01') })
.execute();
}
- 쿼리 최적화 : 복잡한 쿼리는 실행 계획을 분석하고 최적화하세요.
- 비동기 처리 : TypeORM의 모든 데이터베이스 연산은 비동기이므로,
async/await
를 적절히 사용하세요. - 환경별 설정 : 개발, 테스트, 프로덕션 환경에 따라 다른 데이터베이스 설정을 사용하세요.
- 마이그레이션 관리 : 스키마 변경사항을 마이그레이션으로 관리하고, 버전 관리 시스템에 포함시키세요.
TypeORM과 NestJS의 조합은 강력하고 타입 안전한 데이터베이스 연동을 가능케 합니다.
엔티티 정의부터 복잡한 쿼리 실행, 성능 최적화까지 다양한 기능을 제공하여 효율적인 백엔드 개발을 지원합니다.
그러나 ORM의 편의성에 지나치게 의존하여 데이터베이스의 특성을 간과하지 않도록 주의해야 합니다.