이번에 Nest JS 프로젝트를 새로 시작하게 되었다. 그러면서 둘 중 어느 패턴을 선택하여 적용할지에 대해 고민하게 되었는데 그 과정에서 공부한 것을 간단히 적고자 한다.
Active Record Pattern
Active Record 패턴은 모델 안에 모든 쿼리들을 정의해두고 CRUD 작업들을 모델의 메소드를 통해 실행하게 된다.
즉, Active Record 패턴은 모델 안에서 데이터베이스에 접근하는 패턴이라고 볼 수 있을 거 같다.
import {BaseEntity, Entity, PrimaryGeneratedColumn, Column} from "typeorm";
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
isActive: boolean;
static findByName(firstName: string, lastName: string) {
return this.createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName })
.andWhere("user.lastName = :lastName", { lastName })
.getMany();
}
}
Active Record패턴을 사용하려면 위 BaseEntity 를 모델에서 상속 받아야한다. 그러면 아래와 같이 미리 정의된 DB관련 메소드들을 사용할 수 있게 되며 메소드를 추가하여 커스터마이징도 가능하다.
// example how to save AR entity
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.isActive = true;
await user.save();
// example how to remove AR entity
await user.remove();
// example how to load AR entities
const users = await User.find({ skip: 2, take: 5 });
const newUsers = await User.find({ isActive: true });
const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" }); // 기본 메소드 사용
const timber = await User.findByName("Timber", "Saw"); //커스텀 메소드 사용
Data Mapper Pattern
Data Mapper 패턴을 사용하면, 모든 쿼리들을 Repository 라는 분리된 클래스로 관리하게 되고 이를 통해 CRUD작업을 하게 된다. 즉, Data Mapper패턴에서 우리의 모델들은 그저 프로퍼티를 정의하는 "Dumb" 한 상태가 된다.
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
isActive: boolean;
}
Active Record 패턴과 다르게 Data Mapper Pattern에서 모델은 위 코드에서 보듯 단순히 프로퍼티만을 정의한다.
import {User} from "../entity/User";
const userRepository = connection.getRepository(User);
// example how to save DM entity
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.isActive = true;
await userRepository.save(user);
// example how to remove DM entity
await userRepository.remove(user);
// example how to load DM entities
const users = await userRepository.find({ skip: 2, take: 5 });
const newUsers = await userRepository.find({ isActive: true });
const timber = await userRepository.findOne({ firstName: "Timber", lastName: "Saw" });
그 후 위와 같이 Repository를 불러와서 사용하거나,
import {EntityRepository, Repository} from "typeorm";
import {User} from "../entity/User";
@EntityRepository()
export class UserRepository extends Repository<User> {
findByName(firstName: string, lastName: string) {
return this.createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName })
.andWhere("user.lastName = :lastName", { lastName })
.getMany();
}
}
//-------------------//
const userRepository = connection.getCustomRepository(UserRepository);
const timber = await userRepository.findByName("Timber", "Saw");
이처럼 커스텀 Repository를 만들어 사용할 수 있다.
결론
Active Record 패턴 같은 경우, 모델 내부에 쿼리 메소드들을 정의하다보니 단순 CRUD작업이 많거나 규모가 작은 프로젝트에 빠르게 적용할 수 있는 장점이 있을 것 같다. 반대로 복잡한 쿼리 작업이 많거나 프로젝트 규모가 커질수록 유지보수가 힘들어지거 적합하지 않은 패턴이 될 것 같다.
반대로 Data Mapper 패턴 같은 경우 Repository를 통해 모델과 DB의 의존성이 낮아지니 좀 더 구조화가 되어있다는 느낌이 든다. 그래서인지 큰 프로젝트에 적합하다고 하며 유지보수도 Active Record보다 용이할 것으로 보인다.
여기까지가 TypeORM 공식문서에 소개된 내용과 예제들을 정리한 내용이다.
그렇다면 실제로 NestJS에서는 어떻게 적용할까?
NestJS에서는 TypeORM을 사용할 때 Data Mapper를 사용하는 예제만 나와있다. IoC개념을 중시하는 NestJS답게 Repository를 Dependency Injection 하여 사용하는 방법이 좀 더 Nest가 추구하는 방향과 맞아서가 아닐까 하는 생각이 든다.
NestJS에서 Data Mapper 패턴은 두 가지 방법으로 사용이 가능한 것 같다.
1. @InjectRepository 데코레이터를 이용한 방법
// 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 {}
// 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.findOne(id);
}
async remove(id: string): Promise<void> {
await this.usersRepository.delete(id);
}
}
위 처럼 module에 TypeOrmModule.forFeature([User]) 넣어주면 @InjectRepository 데코레이터를 사용하여 Repository를 DI해줄 수 있다.
2. @Inject 데코레이터를 사용한 방법
// photo.providers.ts
import { Connection } from 'typeorm';
import { Photo } from './photo.entity';
export const photoProviders = [
{
provide: 'PHOTO_REPOSITORY',
useFactory: (connection: Connection) => connection.getRepository(Photo),
inject: ['DATABASE_CONNECTION'],
},
];
// photo.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { Repository } from 'typeorm';
import { Photo } from './photo.entity';
@Injectable()
export class PhotoService {
constructor(
@Inject('PHOTO_REPOSITORY')
private photoRepository: Repository<Photo>,
) {}
async findAll(): Promise<Photo[]> {
return this.photoRepository.find();
}
}
이 방법 같은 경우에는 따로 useFactory를 이용해 Provider를 만들어서 DI를 해주는 방법이다.
두 방법 모두 실 사용에는 큰 차이가 없을 수 있지만 Repository를 가져오는 데 있어 추가적인 작업이 필요한 게 아니라면 첫 번째 @InjectRepository를 사용하는 게 더 깔끔한 방법이지 않을까 싶다.
Reference
Database | NestJS - A progressive Node.js framework
'Node.js > Nest.js' 카테고리의 다른 글
NestJS - Decorator를 활용한 Discord봇 개발환경 개선 (0) | 2023.03.16 |
---|---|
NestJS Provider(프로바이더) (0) | 2021.08.19 |
NestJS Lazy-loading Modules (0) | 2021.08.08 |