MikroORM 소개
MikroORM은 Node.js와 TypeScript를 위한 강력한 ORM(Object-Relational Mapping) 라이브러리입니다. TypeORM이나 Sequelize와 같은 다른 ORM들과 비교했을 때 다음과 같은 특징을 가지고 있습니다.
- TypeScript First: TypeScript를 기본적으로 지원하며, 타입 안정성이 뛰어납니다.
- Unit of Work 패턴: 트랜잭션 관리가 용이하며, 변경사항을 효율적으로 추적합니다. 모든 변경사항을 em.flush() 호출 시 한번에 처리(트랜잭션 자동화) 합니다.
- Identity Map: 메모리 내 객체 캐싱을 통해 성능을 최적화합니다.
- Entity Manager: 엔티티의 생명주기를 관리하고 데이터베이스 작업을 추상화합니다.
- Query Builder: 강력한 쿼리 빌더를 제공하여 복잡한 쿼리도 쉽게 작성할 수 있습니다.
다른 ORM과의 비교
TypeORM vs MikroORM
- TypeORM은 더 오래된 라이브러리로 커뮤니티가 크지만, TypeScript 지원이 MikroORM보다 제한적입니다.
- MikroORM은 더 현대적인 아키텍처와 더 나은 타입 안정성을 제공합니다.
Sequelize vs MikroORM
- Sequelize는 JavaScript 중심이지만, MikroORM은 TypeScript 중심입니다.
- MikroORM은 더 나은 타입 추론과 데코레이터 기반의 엔티티 정의를 제공합니다.
NestJS에서 MikroORM 설정하기
1. 필요한 패키지 설치
npm install @mikro-orm/core @mikro-orm/nestjs @mikro-orm/postgresql
- @mikro-orm/core: MikroORM 핵심 라이브러리
- @mikro-orm/nestjs: NestJS 전용 어댑터
- @mikro-orm/postgresql
2. MikroORM 모듈 설정
The forRoot() method accepts the same configuration object as init() from the MikroORM package.
// app.module.ts
import { Module } from '@nestjs/common';
import { MikroOrmModule } from '@mikro-orm/nestjs';
@Module({
imports: [
MikroOrmModule.forRoot({
// 데이터베이스 연결 설정
type: 'postgresql',
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'password',
dbName: 'my_database',
entities: ['dist/**/*.entity.js'],
entitiesTs: ['src/**/*.entity.ts'],
debug: true,
}),
],
})
export class AppModule {}
3. 엔티티 정의
// user.entity.ts
import { Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Property()
email!: string;
@Property()
createdAt: Date = new Date();
@Property({ onUpdate: () => new Date() })
updatedAt: Date = new Date();
}
4. Repository 구현
Repository를 구현 할 때, NestJS 프로젝트에서 아래처럼 Injectable 데코레이터 사용 방식을 권장합니다. NestJS의 DI 시스템과의 통합이 더 자연스럽기 때문입니다.
// src/user/user.module.ts
import { Module } from '@nestjs/common';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { User } from '../entities/user.entity';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { UserRepository } from './user.repository';
@Module({
imports: [
MikroOrmModule.forFeature([User]),
],
providers: [UserService, UserRepository],
controllers: [UserController],
})
export class UserModule {}
모듈에 리포지토리 등록
// src/user/user.repository.ts
import { Injectable } from '@nestjs/common';
import { EntityRepository } from '@mikro-orm/core';
import { User } from '../entities/user.entity';
import { InjectRepository } from '@mikro-orm/nestjs';
export class UserRepository extends EntityRepository<User> {
constructor(
@InjectRepository(User)
private readonly repository: EntityRepository<User>,
) {
super(repository);
}
async findByCondition(condition: Partial<User>): Promise<User[]> {
return this.repository.find(condition);
}
async findWithPagination(page: number, limit: number): Promise<[User[], number]> {
return this.repository.findAndCount(
{},
{
limit,
offset: (page - 1) * limit,
orderBy: { createdAt: 'DESC' },
},
);
}
async findOneWithPosts(id: number): Promise<User | null> {
return this.repository.findOne(
{ id },
{
populate: ['posts'],
},
);
}
}
혹은 Entity에 Repository 지정 방식으로도 사용 가능합니다. 해당 방식은 MikroORM의 기본 패턴에 더 가까운 방식입니다. 이렇게 사용하면, 더 나은 타입 안정성과 MikroORM의 기능을 최대한 활용 가능하다는 장점이 있습니다.
// User.entity.ts
import { Entity, Property } from '@mikro-orm/core';
import { EntityRepositoryType } from '@mikro-orm/core';
import { UserRepository } from './user.repository';
@Entity({ repository: () => UserRepository })
export class User {
@Property()
name!: string;
// 타입 추론을 위한 선언
[EntityRepositoryType]?: UserRepository;
}
// User.repository.ts
import { EntityRepository } from '@mikro-orm/core';
import { User } from './User.entity';
export class UserRepository extends EntityRepository<User> {
// 커스텀 메서드 구현
async findByName(name: string): Promise<User | null> {
return this.findOne({ name });
}
}
6. 서비스 구현
// src/user/user.service.ts
import { Injectable } from '@nestjs/common';
import { User } from '../entities/user.entity';
import { UserRepository } from './user.repository';
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
// 사용자 생성
async createUser(data: { name: string; email: string }): Promise<User> {
return this.userRepository.create(data);
}
// 사용자 조회
async findUser(id: number): Promise<User | null> {
return this.userRepository.findOne(id);
}
// 모든 사용자 조회
async findAllUsers(): Promise<User[]> {
return this.userRepository.findAll();
}
// 사용자 업데이트
async updateUser(id: number, data: Partial<User>): Promise<User | null> {
return this.userRepository.update(id, data);
}
// 사용자 삭제
async deleteUser(id: number): Promise<boolean> {
return this.userRepository.delete(id);
}
// 조건부 사용자 검색
async findUsersByCondition(condition: Partial<User>): Promise<User[]> {
return this.userRepository.findByCondition(condition);
}
// 페이지네이션을 사용한 사용자 조회
async findUsersWithPagination(page: number, limit: number): Promise<[User[], number]> {
return this.userRepository.findWithPagination(page, limit);
}
// 관계가 있는 엔티티와 함께 사용자 조회
async findUserWithPosts(id: number): Promise<User | null> {
return this.userRepository.findOneWithPosts(id);
}
}
7. 컨트롤러 구현
// user.controller.ts
import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';
import { UserService } from './user.service';
import { User } from './user.entity';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
async createUser(@Body() data: { name: string; email: string }): Promise<User> {
return this.userService.createUser(data);
}
@Get(':id')
async getUser(@Param('id') id: number): Promise<User | null> {
return this.userService.findUser(id);
}
@Get()
async getAllUsers(): Promise<User[]> {
return this.userService.findAllUsers();
}
@Put(':id')
async updateUser(
@Param('id') id: number,
@Body() data: Partial<User>,
): Promise<User | null> {
return this.userService.updateUser(id, data);
}
@Delete(':id')
async deleteUser(@Param('id') id: number): Promise<boolean> {
return this.userService.deleteUser(id);
}
}
MikroORM의 주요 기능
1. 관계 설정
// post.entity.ts
import { Entity, PrimaryKey, Property, ManyToOne } from '@mikro-orm/core';
import { User } from './user.entity';
@Entity()
export class Post {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@Property()
content!: string;
@ManyToOne(() => User)
user!: User;
@Property()
createdAt: Date = new Date();
}
2. 쿼리 빌더 사용
// user.service.ts
async findUsersWithPosts(): Promise<User[]> {
return this.em.createQueryBuilder(User, 'u')
.select('*')
.leftJoinAndSelect('u.posts', 'p')
.where({ 'p.title': { $like: '%NestJS%' } })
.getResult();
}
3. 트랜잭션 처리 (롤백 관리)
MikroORM의 em.transactional() 메서드를 사용하면, 콜백 내에서 예외가 발생할 경우 해당 트랜잭션 전체가 자동으로 롤백됩니다.
await em.transactional(async (em) => {
// 여러 데이터베이스 작업
// 예외 발생 시 전체 작업이 롤백됨
await em.persistAndFlush(entity1);
await em.persistAndFlush(entity2);
// throw new Error('Rollback!'); // 예외 발생 시 전체 롤백
});
더 세밀하게 제어하고 싶다면, 아래와 같이 수동 롤백을 할 수도 있습닌다.
await em.begin();
try {
// 여러 작업
await em.persist(entity1);
await em.persist(entity2);
await em.flush();
await em.commit(); // 모든 작업 성공 시 커밋
} catch (e) {
await em.rollback(); // 예외 발생 시 명시적으로 롤백
throw e;
}
성능 최적화
1. Eager Loading
// user.service.ts
async findUserWithPosts(id: number): Promise<User | null> {
return this.em.findOne(User, { id }, {
populate: ['posts'],
});
}
2. 배치 처리
// user.service.ts
async createManyUsers(users: { name: string; email: string }[]): Promise<void> {
const entities = users.map(user => this.em.create(User, user));
await this.em.persistAndFlush(entities);
}
EntityRepository에서 제공하는 기본 메서드들
기본 CRUD 작업
- findOne(where) : 조건에 맞는 단일 엔티티 조회. 없으면 null 반환
- find(where) : 조건에 맞는 엔티티 리스트 조회
- create(data) : 새 엔티티 인스턴스 생성(메모리상). DB에 반영하려면 persist 필요
- persist(entity, flush?: boolean) : 현재 트랜잭션에 엔티티를 등록(INSERT 준비). 실제 DB 반영은 flush 시점에 수행, 기본 설정하에선 persist()만으로는 DB에 변동이 저장되지 않으니, 꼭 flush()나 persistAndFlush()를 호출해야 한다.
- persistAndFlush(entity) : 등록과 동시에 즉시 DB에 반영
- flush() : 지금까지 변경된 모든 엔티티를 DB에 동기화
- remove(entity) : persist()처럼 삭제 예약만 하고, 실제 SQL DELETE는 flush() 시점에 발생한다.
- removeAndFlush(entity) : 삭제 예약 후 즉시 DB에 반영한다.
조건부 쿼리
- findOneOrFail(where) : 조건에 맞는 엔티티 조회, 없으면 예외(EntityNotFoundError) 발생
- findAndCount(where, option?) : 조건 조회 + 전체 개수 동시 반환 [entities, count], 페이징 시 유용
관계 처리 (Populate)
- populate(entities, populate) : 지연 로딩된 연관 관계를 명시적으로 불러온다. 배열 또는 단일 엔티티를 지원한다.
네이티브 쿼리
- nativeInsert(data) : 직접 SQL INSERT 실행, 반환값은 새로 생성된 PK
- nativeUpdate(where, data) : 직접 SQL UPDATE 실행. 변경된 행(row) 수 반환
- nativeDelete(where) : 직접 SQL DELETE 실행. 삭제된 행 수 반환
기타 유틸리티
- count(where) : 조건에 맞는 행 개수 계산
- exists(where) : 조건에 맞는 엔티티 존재 여부 확인
- assign(entity, data) : 엔티티 프로퍼티를 주어진 데이터로 덮어쓰기. 반환값은 수정된 엔티티
// EntityRepository에서 상속받는 주요 메서드들
class EntityRepository<T> {
// 기본 CRUD 작업
findOne(where: FilterQuery<T>): Promise<T | null>;
find(where: FilterQuery<T>): Promise<T[]>;
create(data: RequiredEntityData<T>): T;
persist(entity: T): Promise<void>;
persistAndFlush(entity: T): Promise<void>;
flush(): Promise<void>;
remove(entity: T): Promise<void>;
removeAndFlush(entity: T): Promise<void>;
// 조건부 쿼리
findOneOrFail(where: FilterQuery<T>): Promise<T>;
findAndCount(where: FilterQuery<T>, options?: FindOptions<T>): Promise<[T[], number]>;
// 관계 처리
populate(entities: T | T[], populate: PopulateOptions<T>): Promise<T | T[]>;
// 네이티브 쿼리
nativeInsert(data: RequiredEntityData<T>): Promise<Primary<T>>;
nativeUpdate(where: FilterQuery<T>, data: RequiredEntityData<T>): Promise<number>;
nativeDelete(where: FilterQuery<T>): Promise<number>;
// 기타 유틸리티
count(where: FilterQuery<T>): Promise<number>;
exists(where: FilterQuery<T>): Promise<boolean>;
assign(entity: T, data: RequiredEntityData<T>): T;
}
마이그레이션
1. 마이그레이션 생성
npx mikro-orm migration:create
2. 마이그레이션 실행
npx mikro-orm migration:up
결론
MikroORM은 NestJS 애플리케이션에서 데이터베이스 작업을 효율적으로 처리할 수 있는 강력한 ORM입니다. TypeScript의 타입 안정성과 현대적인 아키텍처를 제공하며, Unit of Work 패턴을 통한 효율적인 트랜잭션 관리가 가능합니다. 또한 강력한 쿼리 빌더와 관계 설정 기능을 통해 복잡한 데이터베이스 작업도 쉽게 구현할 수 있습니다.
MikroORM을 사용하면 다음과 같은 이점을 얻을 수 있습니다.
- 타입 안정성이 뛰어난 데이터베이스 작업
- 효율적인 트랜잭션 관리
- 직관적인 API
- 강력한 쿼리 빌더
- 쉬운 마이그레이션 관리
이러한 특징들로 인해 MikroORM은 대규모 NestJS 애플리케이션에서 특히 유용하게 사용될 수 있습니다.
'Framework > NestJS' 카테고리의 다른 글
NestJS 완전정복: CLI 활용하기 (0) | 2025.05.26 |
---|---|
NestJS 완전정복: 미들웨어, 가드, 인터셉터, 파이프의 모든 것 (0) | 2025.05.26 |
NestJS 완전정복: 모듈, 컨트롤러, 서비스의 모든 것 (0) | 2025.05.16 |
NestJS 톺아보기 : NestJS 개요 및 아키텍처 (0) | 2025.05.15 |
댓글