Nest.js[Active Recore & Data Mapper Pattern
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"); //커스텀 메소드 사용
결론적으로는 모델로 부터 바로 메서드를 사용할 수 있어서 편합니다. 쓰다보면 mongoose와 같은 패턴으로 설계되었구나를 체감하실 수 있습니다.
Data Mapper Pattern
Data Mapper 패턴을 사용하면, 모든 쿼리들을 Repository 라는 분리된 클래스로 관리하게 되고 이를 통해 CRUD작업을 하게 된다.
모델에서 곧장 사용할 수 없고 repository를 이용한다는 것입니다.
우선 선언은 똑같지만 보통 내부 메서드 정의가 없습니다. (못 쓴다는 게 아닙니다. 쓸 수 있습니다)
모델 내부에 메서드를 정의하는 건 대부분 active record 패턴이고, 여기서는 data mapper 패턴을 사용할 것입니다.
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
isActive: boolean;
}
repository를 생성한 뒤 repository를 경유하여 find도 하고 remove도 하는 등 메서드를 사용할 수 있게 됩니다.
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" });
커스텀 메서드를 만들고 싶을 때는 active record 방식이라면 그냥 모델 내부에 메서드를 정의하면 되고
datamapper 방식에서도 종종 그렇게 합니다.
그런데 data mapper 패턴에서는 Custom 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");
결론
Active Record
모델 내부에 쿼리 메소드들을 정의하다보니 단순 CRUD작업이 많거나 규모가 작은 프로젝트에 빠르게 적용할 수 있는 장점이 있을 것 같다. 반대로 복잡한 쿼리 작업이 많거나 프로젝트 규모가 커질수록 유지보수가 힘들어지거 적합하지 않은 패턴이 될 것 같다.
Data Mapper
패턴 같은 경우 Repository를 통해 모델과 DB의 의존성이 낮아지니 좀 더 구조화가 되어있다는 느낌이 든다.
그래서인지 큰 프로젝트에 적합하다고 하며 유지보수도 Active Record보다 용이할 것으로 보인다.
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를 사용하는 게 더 깔끔한 방법이지 않을까 싶다.
NestJS에서는 TypeORM을 사용할 때 Data Mapper를 사용하는 예제만 나와있다.
IoC개념을 중시하는 NestJS답게 Repository를 Dependency Injection 하여 사용하는 방법이 좀 더 Nest가 추구하는 방향과 맞아서가 아닐까 하는 생각이 든다.
IOC 란??
(Inversion of Control)
"제어의 역전" 이라는 의미로,
말 그대로 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라,
외부에서 결정되는 것을 의미한다.
기존 프로그래밍 언어에서 객체나 인스턴스를 new 와 같은 생성 함수로
메모리를 할당받는 작업은 개발자가 직접 수행해야 했다.
하지만 IOC 개념을 도입한 NestJS 에서는 적절한 설정을 통해 인스턴스의 생명주기를
NestJS 에게 위임하도록 하고 있다.
DI 란??
(Dependency Injection)
의존성 주입이란, 의존적인 객체를 직접 생성하지 않고 외부에서 결정한 뒤 연결하는 것을 의미한다.
DI 를 내포한 코딩을 통해 객체 지향성을 보장하며 모듈간의 결합도를 낮추고
IOC 의 구현에 적합한 플랫폼 부품을 작성 할 수 있게 된다.
Injectable()
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
저 Injectable() 이라는 데코레이터는 Nest IoC Container 에 객체의 생성과 관리를 위임하겠다는 뜻을 말한다.
이 데코레이터는 메타데이터를 붙여준다. 그 메타데이터는 Nest에게 이 클래스가 Provider라는 점을 알려준다.