Lsiron
TypeORM 이란? (feat. Nest.js, MySQL, Layered Architecture) 본문
금번 NUDDUCK 웹 프로젝트를 하면서 TypeORM을 적용 해 봤는데 사용법만 잘 알면 여러 방면으로 활용을 할 수 있을 거 같아 블로그로 남겨본다!
Nest.js에서 MySQL과 TypeORM을 통합하여, layered architecture의 repository 패턴에 어떻게 사용했는지 구체적인 코드 예시를 들어 설명을 해 보겠다.
1. TypeORM 이란?
먼저 TypeORM은 MySQL, PostgreSQL, SQLite 등 다양한 데이터베이스와 함께 사용할 수 있는 Object-Relational Mapper이다.
TypeORM은 객체지향 프로그래밍 패러다임을 데이터베이스와 연결해 주는 역할을 한다. 즉, 데이터베이스의 테이블을 클래스 형태로 표현하고, SQL 쿼리 없이 객체 지향 방식으로 데이터베이스 조작이 가능하다.
그러니까 데이터베이스는 보통 우리가 SQL이라는 언어로 대화해야 이해하는데, TypeORM은 이 SQL 언어를 우리가 잘 아는 클래스나 객체 같은 프로그래밍 언어로 바꿔준다.
즉, 우리가 데이터베이스에 직접 SQL을 쓰지 않고도, TypeORM이라는 통역사를 통해 객체를 사용해서 데이터를 저장하거나 불러올 수 있게 해 주는 것이다.
정말 쉽게 말하자면 우리가 여러 나라의 언어를 모르더라도, 통역사가 대신해서 그 언어로 소통할 수 있게 해주는 것과 비슷한 역할이다. 우리는 그냥 익숙한 언어(객체나 클래스)를 사용하고, 번역가(TypeORM)가 그걸 데이터베이스가 알아듣게끔 SQL로 바꿔주는 것.
2. TypeORM 설정과 MySQL 연결
그럼 간단하게 Nest.js 프로젝트에서 TypeORM과 MySQL을 설정하는 방법을 알아보자.
먼저 아래 명령어를 입력하여, @nestjs/typeorm 패키지를 설치 해 준다.
$ npm install @nestjs/typeorm typeorm mysql2
다음으로, app.module.ts 에서 TypeOrmModule을 구성하여 데이터베이스와 연결 해 준다.
( 프로젝트 발췌. TypeOrmModule 부분만 확인하자. )
import { AuthModule } from '@_modules/auth/auth.module';
import { ChatModule } from '@_modules/chat/chat.module';
import { CommunityModule } from '@_modules/community/community.module';
import { ExpertModule } from '@_modules/expert/expert.module';
import { FileUploadModule } from '@_modules/file-upload/file-upload.module';
import { LifeGraphModule } from '@_modules/life-graph/life-graph.module';
import { ProfileModule } from '@_modules/profile/profile.module';
import { ScheduleModule } from '@_modules/quote/schedule.module';
import { SimulationModule } from '@_modules/simulation/simulation.module';
import { UserModule } from '@_modules/user/user.module';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ScheduleModule as NestScheduleModule } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 글로벌로 환경 변수 사용
envFilePath: '.env',
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule], // ConfigModule에서 환경 변수 가져오기
inject: [ConfigService], // ConfigService 의존성 주입
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get<string>('DB_HOST'), // localhost로 설정
port: configService.get<number>('DB_PORT'), // 3307로 설정
username: configService.get<string>('DB_USER'), // RDS 사용자명
password: configService.get<string>('DB_PASSWORD'), // RDS 비밀번호
database: configService.get<string>('DB_NAME'), // DB 이름
entities: [__dirname + '/**/*.entity{.ts,.js}'], // 엔티티 경로 설정
synchronize: true, // 개발 환경에서만 true, 배포 환경에서는 false로 설정
connectTimeout: 60000, // 타임아웃 설정
extra: {}, // SSL 제외, 추가 설정 없음
logging: true, // 모든 쿼리 로그 활성화
}),
}),
AuthModule,
FileUploadModule,
LifeGraphModule,
ProfileModule,
UserModule,
SimulationModule,
ExpertModule,
CommunityModule,
ChatModule,
ScheduleModule,
NestScheduleModule.forRoot(),
],
})
export class AppModule {}
이 코드는 TypeOrmModule을 forRootAsync 메서드로 설정하고 환경 변수를 통해 MySQL 설정을 로드하는 예시이다.
보통 entities 배열에는 entities: [User] 와 같은 방식으로, 데이터베이스 테이블로 매핑될 엔티티 클래스를 입력하지만,
금번 프로젝트에서는 [__dirname + '/**/*.entity{.ts,.js}'] 를 사용하여, 현재 디렉터리에서 시작해서 모든 하위 디렉터리에서 .entity.ts나 .entity.js로 끝나는 파일을 자동으로 찾아 엔티티로 인식하도록 설정 해 주었다.
3. 엔티티(Entity) 정의
다음으로 엔티티를 정의해주자. 엔티티는 데이터베이스 테이블의 구조를 정의하는 클래스이다.
@Entity() 데코레이터를 사용하여 테이블로 매핑되며, 각 컬럼은 @Column() 데코레이터로 정의한다.
( 프로젝트 발췌. User 엔티티 이지만 기본 설정만 확인하자. )
import { Column, CreateDateColumn, DeleteDateColumn, Entity, OneToMany, ManyToOne, PrimaryGeneratedColumn, Unique, UpdateDateColumn, JoinColumn } from 'typeorm';
import { UserHashtag } from '@_modules/user/entity/hashtag.entity';
import { LifeGraph } from '@_modules/life-graph/entity/life-graph.entity';
import { Community } from '@_modules/community/entities/community.entity';
import { Comment } from '@_modules/community/entities/comment.entity';
import { ChatRoom } from '@_modules/chat/entities/room.entity';
import { Message } from '@_modules/chat/entities/message.entity';
@Entity()
@Unique(['provider', 'providerId'])
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 255 })
provider: string;
@Column({ type: 'varchar', length: 255 })
providerId: string;
@Column({ type: 'varchar', length: 255 })
name: string;
@Column({ type: 'varchar', length: 255 })
email: string;
@Column({ type: 'varchar', length: 255 })
nickname: string;
@Column({ type: 'varchar', length: 255, nullable: true })
refreshToken: string;
@Column({ type: 'varchar', length: 255, nullable: true })
imageUrl: string;
@OneToMany(() => UserHashtag, (userHashtag) => userHashtag.user)
hashtags: UserHashtag[];
@ManyToOne(() => LifeGraph)
@JoinColumn({ name: 'favoriteLifeGraphId' })
favoriteLifeGraph: LifeGraph;
@OneToMany(() => LifeGraph, (lifeGraph) => lifeGraph.user)
lifeGraphs: LifeGraph[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt: Date | null;
@OneToMany(() => Comment, (comment) => comment.user)
comments: Comment[];
@OneToMany(() => Community, (community) => community.user)
communities: Community[];
@OneToMany(() => ChatRoom, (chatRoom) => chatRoom.createdBy)
chatRooms: ChatRoom[];
@OneToMany(() => Message, (message) => message.user)
messages: Message[];
}
여기서 User 엔티티는 MySQL의 user 테이블에 대응한다.
@Entity()
- 설명: 이 데코레이터는 해당 클래스(User)가 데이터베이스 테이블에 매핑된다는 것을 TypeORM에 알려준다. @Entity()가 선언된 클래스는 자동으로 테이블로 변환된다.
- 사용 목적: 이 클래스는 데이터베이스의 테이블을 나타내며, 클래스의 필드가 테이블의 컬럼으로 변환된다.
@PrimaryGeneratedColumn()
- 설명: 이 데코레이터는 해당 필드(id)가 기본 키 이면서 자동으로 증가하는 값(Generated)을 가진다는 것을 나타낸다.
- 사용 목적: 이 필드는 데이터베이스에서 자동으로 값이 생성되며, 테이블의 각 레코드를 고유하게 식별한다.
@Column()
- 설명: 이 데코레이터는 해당 필드가 데이터베이스 컬럼으로 매핑된다는 것을 나타낸다.
- 옵션을 통해 컬럼의 데이터 타입, 길이, NULL 허용 여부 등을 지정할 수 있다.
@Unique()
- 설명: 이 데코레이터는 테이블 수준에서 고유한 값을 설정한다. ['provider', 'providerId']로 지정되어 있으므로 provider와 providerId의 조합이 유일해야 함을 나타낸다.
- 사용 목적: 두 개의 필드(provider, providerId)의 값이 중복되지 않도록 설정하여, 동일한 소셜 제공자와 소셜 ID로 중복 가입하는 것을 방지한다.
@OneToMany()
- 설명: 이 데코레이터는 일대다 관계를 나타낸다. 한 명의 User가 여러 개의 연관된 엔티티(예: UserHashtag, LifeGraph, Comment, Community 등)를 가질 수 있음을 의미한다.
- 사용 예시:여기서, User는 여러 개의 UserHashtag를 가질 수 있고, 반대로 UserHashtag는 하나의 User와만 연결된다.
@ManyToOne()
- 설명: 다대일 관계를 나타낸다. 여러 개의 User가 하나의 LifeGraph와 연관될 수 있음을 의미한다.
- 사용 목적: 한 명의 사용자가 favoriteLifeGraph라는 필드를 통해 특정 LifeGraph를 선호할 수 있다.
@JoinColumn()
- 설명: 관계의 외래 키 컬럼을 정의하는 데 사용된다. @ManyToOne() 관계에서 이 데코레이터를 사용하여 외래 키가 저장될 컬럼의 이름을 명시한다.
@CreateDateColumn()
- 설명: 이 데코레이터는 데이터베이스에서 레코드가 생성된 시간을 자동으로 기록한다.
- 사용 목적: 사용자가 생성된 시점을 자동으로 기록할 때 사용된다. 값을 명시적으로 설정하지 않아도, TypeORM이 자동으로 현재 시간을 기록한다.
@UpdateDateColumn()
- 설명: 이 데코레이터는 레코드가 수정된 시간을 자동으로 기록한다.
- 사용 목적: 레코드가 수정될 때마다 이 컬럼에 마지막 수정 시간이 자동으로 기록된다. createdAt과 마찬가지로 명시적으로 값을 설정하지 않아도 TypeORM이 관리한다.
@DeleteDateColumn()
- 설명: 이 데코레이터는 소프트 삭제를 위한 필드이다. 레코드를 완전히 삭제하는 대신, deletedAt 필드에 삭제 시간을 기록하여 레코드가 삭제되었음을 표시한다.
- 사용 목적: 레코드의 실제 데이터는 유지하면서도 삭제된 것으로 표시하고 싶을 때 사용한다. 데이터베이스에서 영구적으로 삭제하지 않고 복구할 수 있다.
4. Repository 패턴과 서비스 계층
Repository 패턴은 데이터 접근 로직을 추상화하는 방식으로, 서비스 계층에서 데이터베이스와 상호작용할 때 사용된다.
TypeOrmModule.forFeature()로 특정 엔티티의 리포지토리를 서비스에 주입할 수 있다.
( 프로젝트 발췌. 기본 설정만 확인하자. )
import { UserDto } from '@_modules/auth/dto/user.dto';
import { Community } from '@_modules/community/entities/community.entity';
import { LifeGraph } from '@_modules/life-graph/entity/life-graph.entity';
import { UserHashtag } from '@_modules/user/entity/hashtag.entity';
import { User } from '@_modules/user/entity/user.entity';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class UserRepository {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(UserHashtag)
private readonly userHashtagRepository: Repository<UserHashtag>,
@InjectRepository(Community)
private readonly communityRepository: Repository<Community>,
@InjectRepository(LifeGraph)
private readonly lifeGraphRepository: Repository<LifeGraph>,
) {}
// Provider로 사용자 찾기 (소셜 로그인용)
async findUserByProvider(provider: string, providerId: string): Promise<User | null> {
return this.userRepository.findOne({
where: { provider, providerId },
withDeleted: true,
});
}
// 게시글 찾기
async findMyPosts(userId: number, page: number, limit: number): Promise<Community[]> {
return this.communityRepository.find({
where: { user: { id: userId } },
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
});
}
// 게시글 카운트
async countMyPosts(userId: number): Promise<number> {
return this.communityRepository.count({
where: { user: { id: userId } },
});
}
// 즐겨찾기 인생 그래프 찾기
async findFavoriteLifeGraph(userId: number, graphId: number, relations?: string[]): Promise<LifeGraph | null> {
return this.lifeGraphRepository.findOne({
where: { id: graphId, user: { id: userId } },
relations: relations || [],
});
}
// User ID로 사용자 찾기 (relations 옵션 추가)
async findUserById(id: number, relations: string[] = []): Promise<User | null> {
return this.userRepository.findOne({
where: { id, deletedAt: null },
relations, // relations 옵션 추가
});
}
// 닉네임으로 사용자 찾기
async findUserByNickname(nickname: string): Promise<User | null> {
return this.userRepository.findOne({ where: { nickname } });
}
// 사용자 생성
async createUser(userDto: UserDto): Promise<User> {
const newUser = this.userRepository.create({
provider: userDto.provider,
providerId: userDto.providerId,
name: userDto.name,
email: userDto.email,
nickname: userDto.nickname,
imageUrl: userDto.imageUrl,
});
return this.userRepository.save(newUser);
}
// Refresh Token 업데이트
async updateRefreshToken(userId: number, refreshToken: string): Promise<void> {
await this.userRepository.update(userId, { refreshToken });
}
// 사용자 정보 업데이트
async updateUser(user: User): Promise<void> {
await this.userRepository.save(user);
}
// 해시태그 전체 추가
async createHashtags(userHashtags: UserHashtag[]): Promise<void> {
await this.userHashtagRepository.save(userHashtags);
}
// 해시태그 찾기
async findHashtagsByUserId(userId: number): Promise<string[]> {
const hashtags = await this.userHashtagRepository.find({
where: { user: { id: userId } },
});
return hashtags.map((hashtag) => hashtag.name);
}
// 해시태그 삭제
async deleteHashtags(userId: number, hashtags: string[]): Promise<void> {
await Promise.all(
hashtags.map((tag) =>
this.userHashtagRepository.delete({ user: { id: userId }, name: tag })
),
);
}
// 소프트 삭제 (사용자 계정 비활성화)
async deleteUser(userId: number): Promise<void> {
await this.userRepository.softDelete(userId);
}
}
UserRepository 는 서비스 계층에서 호출되며, 사용자의 행위와 관련된 데이터 처리를 담당한다.
예를 들어, 사용자 로그인이 이루어지면, 해당 사용자가 DB에 있는지 확인하는 로직을 담당하며, 소셜 로그인 시에도 제공자와 제공자 ID로 사용자를 찾는 작업을 수행한다.
이를 통해 서비스 계층에서는 복잡한 데이터베이스 로직을 신경 쓸 필요 없이 비즈니스 로직에만 집중할 수 있게 된다.
한번 repository를 분석 해 보자.
- UserRepository 클래스는 @Injectable() 데코레이터로 인해 NestJS에서 서비스로 사용된다.
- constructor는 TypeORM의 Repository를 주입받아 데이터베이스에 접근할 수 있도록 설정한다.
- userRepository: User 엔티티와 연결된 사용자 데이터베이스 테이블에 대한 접근을 담당.
- userHashtagRepository: UserHashtag 엔티티와 연결된 사용자 해시태그 데이터베이스 테이블에 대한 접근을 담당.
- communityRepository: Community 엔티티와 연결된 커뮤니티 게시글 데이터베이스 테이블에 대한 접근을 담당.
- lifeGraphRepository: LifeGraph 엔티티와 연결된 인생 그래프 데이터베이스 테이블에 대한 접근을 담당.
TypeORM의 Repository 클래스는 데이터베이스 테이블과 상호작용하기 위해 다양한 메서드를 제공한다.
save, update, find, delete, softDelete, findOne, where 같은 메서드는 모두 Repository에 내장되어 있으며, 데이터를 처리하는 기본적인 동작을 담당한다. 각 메서드를 하나씩 파헤쳐 보자.
1. 기본 CRUD 메서드
save(entity: Entity | Entity[])
- 엔티티 또는 엔티티 배열을 저장하거나 업데이트한다.
- 엔티티가 이미 존재하면 업데이트, 존재하지 않으면 삽입한다.
insert(entity: Entity | Entity[])
- 엔티티를 데이터베이스에 삽입한다. 삽입 전의 엔티티는 새로운 것으로 간주되며, ID가 없는 경우에 사용한다.
- 이미 존재하는 데이터가 있으면 오류가 발생한다.
update(criteria: any, partialEntity: QueryDeepPartialEntity<Entity>)
- 조건을 만족하는 데이터를 업데이트한다. 업데이트할 필드만 선택적으로 제공할 수 있다.
remove(entity: Entity | Entity[])
- 하나 이상의 엔티티를 삭제한다. 이 메서드는 엔티티 객체를 통해 삭제가 이루어진다.
delete(criteria: any)
- 조건에 맞는 데이터를 삭제한다. 특정 필드 값을 기준으로 데이터를 삭제할 수 있다.
softDelete(criteria: any)
- 데이터를 소프트 삭제(논리 삭제)한다. 실제로 데이터를 삭제하지 않고, 삭제된 것처럼 플래그를 설정한다.
restore(criteria: any)
- 소프트 삭제된 데이터를 복구한다.
findOne(options: FindOneOptions<Entity>)
- 조건에 맞는 단일 엔티티를 조회한다. 조건에 맞는 데이터가 없으면 null을 반환한다.
findOneBy(criteria: any)
- 특정 필드나 조건에 맞는 단일 엔티티를 조회한다.
find(options?: FindManyOptions<Entity>)
- 조건에 맞는 여러 엔티티를 배열로 조회한다.
findBy(criteria: any)
- 여러 개의 조건에 맞는 엔티티를 조회한다.
count(options?: FindManyOptions<Entity>)
- 조건에 맞는 데이터의 개수를 계산한다.
countBy(criteria: any)
- 조건에 맞는 데이터의 개수를 센다.
exist(options?: FindManyOptions<Entity>)
- 조건에 맞는 데이터가 존재하는지 여부를 반환한다.
existBy(criteria: any)
- 특정 필드나 조건에 맞는 데이터가 존재하는지 확인한다.
increment(criteria: any, propertyPath: string, value: number)
- 특정 필드 값을 증가시킨다.
decrement(criteria: any, propertyPath: string, value: number)
- 특정 필드 값을 감소시킨다.
query(queryString: string, parameters?: any[])
- SQL 쿼리를 직접 실행한다. raw SQL을 사용할 때 유용하다.
clear()
- 데이터베이스에서 모든 데이터를 제거한다.
2. 트랜잭션 관련 메서드
manager.transaction(callback)
- 트랜잭션 내에서 여러 데이터베이스 작업을 처리한다. 트랜잭션이 완료되면 커밋, 실패 시 롤백한다.
3. Repository 내장 옵션
내장 옵션들은 find, findOne, save, update, remove 등 메서드에서 사용된다.
1. Where 조건 설정
where
- 특정 조건을 만족하는 데이터를 조회하거나 수정, 삭제하는데 사용한다.
- AND 조건이 기본이며, 객체 형태로 사용하거나 여러 조건을 배열로 묶어 OR 조건을 사용할 수 있다.
where: { id: 1, isActive: true } where: [ { id: 1 }, { name: 'John' } ]
2. 페이징 관련 옵션
skip
skip: 10 // 10개를 건너뛴다.
- 조회 시 데이터를 건너뛰는 수를 설정한다.
take
take: 5 // 최대 5개의 결과를 반환한다.
- 조회할 데이터의 개수를 설정한다. LIMIT과 동일한 기능을 한다.
3. 정렬 관련 옵션
order
order: { name: 'ASC', createdAt: 'DESC' }
- 데이터를 정렬하는 방법을 설정한다. ASC(오름차순), DESC(내림차순) 옵션을 사용할 수 있다.
4. 관계 데이터 조회 옵션
relations
relations: ['profile', 'posts']
- 관계형 데이터를 함께 조회하는 옵션이다. 관계를 설정한 필드명을 지정하여 연관된 데이터를 함께 가져온다.
5. Select 필드 선택
select
select: ['name', 'email']
- 특정 필드만 선택하여 조회할 수 있다.
6. 특정 필터링 조건
join
join: { alias: 'user', leftJoinAndSelect: { profile: 'user.profile' } }
- 특정 테이블과 조인하는 옵션이다. 관계형 테이블을 join하여 데이터를 조회할 수 있다.
cache
cache: true cache: 60000 // 60초 동안 캐시
- 쿼리 결과를 캐싱하는 옵션이다. 캐시를 활성화하고, 특정 시간 동안 결과를 캐싱할 수 있다.
4. 유틸리티 함수와 옵션들
Between
where: { age: Between(20, 30) }
- 설명: 두 값 사이의 범위를 조회할 때 사용한다.
In
where: { id: In([1, 2, 3]) }
- 설명: 여러 값 중 하나를 만족하는 데이터를 조회할 때 사용한다.
Like
where: { name: Like('%John%') }
- 설명: SQL의 LIKE 연산자와 동일하게, 특정 패턴을 만족하는 데이터를 조회할 때 사용한다.
IsNull
where: { deletedAt: IsNull() }
- 설명: NULL 값을 가진 데이터를 조회할 때 사용한다.
MoreThan, LessThan, MoreThanOrEqual, LessThanOrEqual
where: { age: MoreThan(18) }
- 설명: 숫자 또는 날짜 값을 비교할 때 사용한다.
Not
where: { name: Not('John') }
- 설명: 특정 값이 아닌 데이터를 조회할 때 사용한다.
5. 마이그레이션 (Migration)
마이그레이션은 데이터베이스 스키마의 변경사항을 추적하고 관리하는 도구이다. TypeORM CLI를 사용하여 마이그레이션을 생성하고 실행할 수 있다.
현재 프로젝트에서는 app.module.ts 파일에서 synchronize: true 설정으로 인해, 데이터베이스 스키마가 자동으로 동기화되므로 마이그레이션이 필요하지 않았다. 하지만 이 방식은 개발 초기 단계나 간단한 프로젝트에만 적합하다.
프로젝트가 커지고, 스키마 변경이 자주 발생하거나 프로덕션 환경으로 배포할 경우에는 마이그레이션을 도입하는 것이 필수적이다. 마이그레이션을 통해 데이터베이스 변경 사항을 안전하게 관리할 수 있기 때문이다.
1. 마이그레이션 생성
npx typeorm migration:create src/migrations/CreateUserTable
그러면 CreateUserTable이라는 새로운 마이그레이션 파일이 생성된다. 여기서 직접 SQL 문이나 TypeORM의 스키마 변경 API를 통해 마이그레이션을 정의할 수 있다.
엔티티를 수정하고 다시 마이그레이션 파일을 생성하면 새로운 마이그레이션 파일이 하나 더 만들어진다.
각 마이그레이션 파일은 엔티티 수정에 따른 변경 사항을 추적하는 역할을 하기 때문에, 엔티티를 수정할 때마다 새로운 마이그레이션 파일이 생성된다.
2. 마이그레이션 파일 예시
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
export class CreateUserTable1623871123540 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'user',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment',
},
{
name: 'name',
type: 'varchar',
length: '100',
},
{
name: 'email',
type: 'varchar',
isUnique: true,
},
{
name: 'password',
type: 'varchar',
},
{
name: 'isActive',
type: 'boolean',
default: true,
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('user');
}
}
3. 마이그레이션 실행
npx typeorm migration:run
이 명령어는 생성된 마이그레이션 파일을 실행하여 데이터베이스 스키마를 변경한다.
Nest.js에서 MySQL과 TypeORM을 사용하면, 데이터베이스의 구조와 비즈니스 로직을 명확하게 분리하여 유지보수성을 높일 수 있다. 특히 layered architecture를 통해 모듈화된 구조를 유지하면서도 Repository 패턴을 사용해 데이터베이스와의 상호작용을 간편하게 처리할 수 있다.
'개발일지 > NUDDUCK' 카테고리의 다른 글
배포란? VM? Docker? kubernetes? (6) | 2024.10.21 |
---|---|
JWT 토큰이란? 그리고 쿠키에 토큰을 담는 방식에 관하여 (13) | 2024.10.11 |
Nest.js, JWT 전략에서 엑세스 토큰 검증 문제 해결 (2) | 2024.10.09 |