Repository 패턴이란,
먼저 CustomRepository를 사용하기에앞서,
Repository패턴이 무엇인지 알아보죠.
소프트웨어 디자인 패턴 중 하나로, 데이터베이스와의 상호 작용울 추상화하여
데이터 엑세스 코드를 분리하는데 사용됩니다
Repository 패턴의 핵심 아이디어는 아래와 같습니다.
1. 데이터 엑세스의 추상화 : Repository 패턴은 데이터 엑세스를 위한 인터페이스를 제공하여, 데이터베이스와의 통신을 추상화 합니다.
이로써 데이터 엑세스 코드가 실제데이터베이스와 직접 결합되자않고 인터페이스를 통해 접근할 수 있습니다.
2. 도메인로직과 데이터 엑세스의 분리 : Repository패턴을 사용하면 도메인 로직과 데이터 엑세스 코드를 분리할 수 있습니다.
도메인 로직은 데이터에 대한 비즈니스 규칙과 관련이 있고, Repository는 데이터 엑세스에 대한 구체적인 구현을 담당합니다.
3. 유지보수성 향상 : 데이터베이스 구현의 변경이나 데이터 엑세스 로직의 수정이 상대적으로 쉬어집니다.
실제 데이터베이스와의 상호 작용을 추상화하기 때문에 데이터베이스 변경이나 데이터 엑세스 코드 수정에 대한 영향을 최소화 할 수 있습니다.
4. 테스트 용이성 : 데이터베이스에 의존하지 않고도 도메인 로직을 테스트하기 쉬워집니다. (모크데이터를 만들어 단위테스트가 용이해짐)
CustomRepository의 사용이유
자주 호출되며 복잡한 요청이 필요할때 CustomRepository를 이용해
TypeORM의 Repository를 확장하여 사용자 정의 메서드를 추가해 사용합니다.
한가지 예를 들어 아래 코드는 등록하고자하는 카테고리가 없는 경우 생성해주는 작업입니다.
등록, 수정시에 사용되는 작업이라면 12줄의 코드가 총 2번 사용될텐데,
CustomRepository를 사용해 확장 메소드를 등록해놓으면 간소화할 수 있겠죠
const categoryName = name.trim().toLowerCase();
const categorySlug = categoryName.replace(/ /g, '-');
let category = await this.categories.findOne({
where: { slug: categorySlug },
});
if (!category) {
category = await this.categories.save(
this.categories.create({ slug: categorySlug, name: categoryName }),
);
}
return category;
TypeOrm 버전Up
TypeOrm 버전이 0.2.x에서 0.3.x로 올라오면서 많은 변화들이 생겼다.
그중 하나가 @EntityRepositody를 사용해서 커스텀 레포지토리를 만들어서 사용할 수 있었다면,
0.3.x버전으로 올라오면서 deprecated되어 더이상 사용할 수 없다.
0.2 버전의 Custom Repository
// cateogory.service.ts
@Injectable()
export class CategoryService {
constructor(
@InjectRepository(Category)
private categoryRepository: CategoryRepository) {}
createCategory(): Promise<CreateCategoryOutput> {
return this.categoryRepository.getOrCreateCategory();
}
}
// category.repository.ts
@EntityRepository(Category)
export class CategoryRepository extends Repository<Category> {
async getOrCreateCategory() {
const categoryName = name.trim().toLowerCase();
const categorySlug = categoryName.replace(/ /g, '-');
let category = await this.categories.findOne({
where: { slug: categorySlug },
});
if (!category) {
category = await this.categories.save(
this.categories.create({ slug: categorySlug, name: categoryName }),
);
}
return category;
}
}
0.2버전의 방식은 @EntityRepository 데코레이터를 Repository에 작성해준 뒤 Service 레이어에서 Repository 의존성을 주입 한 후 사용했습니다.
하지만 TypeOrm 버전이 0.3으로 올라감에 따라 @EntityRepository 데코레이터를 사용할 수 없게 변경되어 이에따라 Custom Repository를 사용하기 위해선 직접 데코레이터를 사용한 후 Service 레이어로 의존성을 주입해야 하도록 변경되었습니다.
0.3버전의 Custom Repository
/* src/typeorm/custom.repository.decorator.ts */
import { SetMetadata } from '@nestjs/common';
export const TPYEORM_CUSTOM_REPOSITORY = 'TPYEORM_CUSTOM_REPOSITORY';
// eslint-disable-next-line
export function CustomRepository(entity: Function): ClassDecorator {
return SetMetadata(TPYEORM_CUSTOM_REPOSITORY, entity);
}
우선 CustomRepository의 데코레이터를 정의해줍니다. SetMetaData는 메타데이터를 설정해주는 역할을 합니다.
(TYPEORM_CUSTOM_REPOSITORY라는 키의 메타데이터를 설정함)
/* src/typeorm/typeorm.module.ts */
import { DynamicModule, Provider } from '@nestjs/common';
import { getDataSourceToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { TPYEORM_CUSTOM_REPOSITORY } from './custom.repository.decorator';
/**
* @CustomRepository 데코레이터를 사용할경우
* CustomRepository에 담겨있는 메타데이터들을 가져온다. 그 후 DataSource 정보를 주입받고
* providers 에 추가한다.
* 해당 모듈을 통해 우리가 만든 데코레이터로 CustomRepository 기능을 사용할 수 있게 된다.
*/
export class TypeOrmCustomModule {
public static forCustomRepository<T extends new (...args: any[]) => any>(
repositories: T[],
): DynamicModule {
const providers: Provider[] = [];
console.log('repositories', repositories);
for (const repository of repositories) {
const entity = Reflect.getMetadata(TPYEORM_CUSTOM_REPOSITORY, repository);
if (!entity) {
continue;
}
providers.push({
inject: [getDataSourceToken()],
provide: repository,
useFactory: (dataSource: DataSource): typeof repository => {
const baseRepository = dataSource.getRepository<any>(entity);
return new repository(
baseRepository.target,
baseRepository.manager,
baseRepository.queryRunner,
);
},
});
}
return {
exports: providers,
module: TypeOrmCustomModule,
providers,
};
}
}
그 후 CustomRepository를 사용하기 위해 모듈을 생성줍니다
@CustomRepository 데코레이터를 사용할 경우 CustomRepository에 담겨있는 메타데이터들을 가져옵니다.
그 후 DataSource 정보를 주입받고 providers에 추가합니다.
해당 모듈을 통해 우리가 만든 데코레이터로 CustomRepository기능을 사용할 수 있게 됩니다.
/* /src/categories/category.module.ts */
@Module({
imports: [
TypeOrmModule.forFeature([Restaurant, Category]),
TypeOrmCustomModule.forCustomRepository([CategoryRepository]),
],
providers: [CategoryResolver, CategoryService],
})
export class CategoryModule {}
CustomRepository를 사용하려면 기존 TypeORM모듈에서 생성한 CustomModule로 바꿔주어야 합니다.
이후 Serivide에 의존성 주입후 사용하면 됩니다 !
EntityRepository 데코레이터 대신, CustomRepository를 사용하는 부분을 유념하세요 !
// category.repository.ts
@CustomRepository(Category)
export class CategoryRepository extends Repository<Category> {
async getOrCreateCategory() {
const categoryName = name.trim().toLowerCase();
const categorySlug = categoryName.replace(/ /g, '-');
let category = await this.categories.findOne({
where: { slug: categorySlug },
});
if (!category) {
category = await this.categories.save(
this.categories.create({ slug: categorySlug, name: categoryName }),
);
}
return category;
}
}