Prisma 는 현존하는 Typescript
환경에서 사용할 수 있는 ORM
중 가장 엄격한 타입을 지원하는 ORM
입니다.
PrismaClient
가 충분히 추상화된 형태로 기능들을 잘 지원해주지만 프로젝트가 성장함에 따라 반복적으로 사용되는 데이터 엑세스 로직이 발생하여 추가적인 커스텀 메서드의 필요성을 느끼게 됩니다. PrismaClient
전체를 특정 서비스 클래스에서 직접 사용하게되면 서비스 클래스에서 전체 모델에 대한 결합이 발생하여 매우 방대한 범위의 모델을 관리하는 하나의 서비스가 클래스가 만들어질 확률이 높습니다.위에서 언급한 이유들로 인해 저는 보통 Prisma를 사용할 때 Custom Repository
를 생성하여 service class에 주입하는 형태를 사용합니다
전체 예시 코드는 여기에서 확인할 수 있습니다.
prisma
는 개별 혹은 전체 model을 확장할 수 있는 다양한 방법들을 제공합니다.([1], [2]) 여기서는 하나의 repository
가 하나의 모델만 관리하면서 사용자 선언 메서드를 추가하는 것이 목적이므로 기존 모델의 메서드들은 유지하면서 사용자 선언 메서드를 추가하는 방법을 사용합니다.
예시에서는
prisma
에서는 공식적으로 지원하지 않지만 실무에서 많이 사용하게 되는softDelete
를 사용자 선언 메서드로 추가해보도록 했습니다.
import { Logger } from '@nestjs/common';
import { Prisma, PrismaClient } from '@prisma/client';
export function UserRepository(userClient: PrismaClient['user']) {
return Object.assign(userClient, {
async softDelete(id: number) {
return userClient.update({
where: { id },
data: { deletedAt: new Date() },
});
},
});
}
export type UserRepository = ReturnType<typeof UserRepository>;
PrismaClient
의 user
모델만 인자로 받아서 Object.assign
을 사용한 mixin
으로 확장된 user 모델의 객체를 반환하는 함수를 선언하고 이 함수의 반환 타입을 같은 이름의 타입으로 선언하여 타입을 선언 병합
합니다.
describe(UserRepository, () => {
let userRepository: UserRepository;
const prisma = new PrismaClient({
datasources: {
db: {
url: `postgresql://postgres:postgres@localhost:5432/postgres`,
},
},
});
beforeAll(async () => {
await prisma.$connect();
// 객체 생성
userRepository = UserRepository(prisma.user);
});
afterAll(async () => {
await prisma.$queryRaw`TRUNCATE "user" CASCADE`;
await prisma.$disconnect();
});
it('should soft delete a user', async () => {
// given
const user = await prisma.user.create({
data: {
email: 'test@test.com',
password: 'password',
},
});
// when
await userRepository.softDelete(user.id);
// then
const deletedUser = await prisma.user.findUnique({
where: { id: user.id },
});
expect(deletedUser.deletedAt).toEqual(expect.any(Date));
});
위의 코드에서 beforeAll
블럭의 userRepository = UserRepository(prisma.user);
구문에서 prisma client의 user
모델을 전달해 userRepository 객체를 생성하는 것을 볼 수 있습니다.
이후 it
블럭에서 사용자가 확장한 softDelete
메서드를 사용하는 것을 볼 수 있습니다.
위의 캡쳐에서 볼 수 있는 것처럼 type도 정상적으로 인식되어 ide의 hint도 제공받을 수 있습니다.
@Module({
imports: [],
controllers: [],
providers: [
{
provide: UserRepository,
inject: [PrismaService],
useFactory: (prismaService: PrismaService) =>
UserRepository(prismaService.user),
},
],
})
export class UserModule {}
UserRepository
구현 시 선언 병합
했던 type
을 사용합니다.custom provider
의 inject
에 PrismaService
를 주입하고, useFactory
를 통해 PrismaService
객체를 주입받아 모듈 생성 시 UserRepository 객체가 생성되도록 구현합니다.import { Inject, Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
@Injectable()
export class UserService {
public constructor(
@Inject(UserRepository) private readonly userRepository: UserRepository,
) {}
}
모듈에 선언해준 것 처럼 type을 토큰으로 UserRepository
를 주입받아 사용할 수 있습니다.
여기까지가 간단하게 Nest.js에서 간단하게 모델별 Custom Repository
를 만드는 방법에 대해 논의해보았습니다. 이 방법은 Prisma 그 자체를 추상화하지는 않고 커스텀 메서드만을 확장하는 방식으로 활용할 수 있어 간단하게 데이터 엑세스 레이어를 일부분 분리하고자 하는 경우 유용하게 활용할 수 있을 것이라고 생각합니다.
이외에도 아래처럼 클래스로 선언하여 클래스에서 필요한 메서드들만 노출하도록 구현하는 방법이 있겠지만 필요한 메서드들을 모두 다시 재선언해야하기 때문에 Hexagonal architecture
같은 높은 수준의 추상화를 필요로 하는 프로젝트가 아니라면 이 아티클에서 구현한 방법을 고려해볼 수 있을 것이라고 생각합니다.
@Injectable()
export class UserRepository {
public constructor(private readonly prismaUser: PrismaClient['user']) {}
// custom methods...
}