Prisma custom repository 만들기 근데 이제 NestJS를 곁들인

June·2024년 4월 7일
0

Prisma

목록 보기
1/2
post-thumbnail

Prisma 는 현존하는 Typescript 환경에서 사용할 수 있는 ORM 중 가장 엄격한 타입을 지원하는 ORM 입니다.

필요성

  • PrismaClient 가 충분히 추상화된 형태로 기능들을 잘 지원해주지만 프로젝트가 성장함에 따라 반복적으로 사용되는 데이터 엑세스 로직이 발생하여 추가적인 커스텀 메서드의 필요성을 느끼게 됩니다.
  • 전체 스키마에 대한 접근 권한을 가진 PrismaClient 전체를 특정 서비스 클래스에서 직접 사용하게되면 서비스 클래스에서 전체 모델에 대한 결합이 발생하여 매우 방대한 범위의 모델을 관리하는 하나의 서비스가 클래스가 만들어질 확률이 높습니다.
  • 데이터 엑세스 코드와 비즈니스 로직이 혼합되어 코드의 유지보수성과 가독성이 떨어지게 됩니다.
  • orm 과 강결합 된 서비스 클래스는 유닛 테스트 구성이 힘들어지는 주 원인이 되기도 합니다.

위에서 언급한 이유들로 인해 저는 보통 Prisma를 사용할 때 Custom Repository를 생성하여 service class에 주입하는 형태를 사용합니다

전체 예시 코드는 여기에서 확인할 수 있습니다.

Prisma Model 확장하기

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>;

PrismaClientuser 모델만 인자로 받아서 Object.assign을 사용한 mixin으로 확장된 user 모델의 객체를 반환하는 함수를 선언하고 이 함수의 반환 타입을 같은 이름의 타입으로 선언하여 타입을 선언 병합 합니다.

UserRepository 테스트하기


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도 제공받을 수 있습니다.

Nest.js의 module에 주입하기


@Module({
  imports: [],
  controllers: [],
  providers: [
    {
      provide: UserRepository,
      inject: [PrismaService],
      useFactory: (prismaService: PrismaService) =>
        UserRepository(prismaService.user),
    },
  ],
})
export class UserModule {}
  • 토큰으로 UserRepository 구현 시 선언 병합 했던 type을 사용합니다.
  • 일반적으로 Prisma를 nest.js에서 사용시 Global Prisma 모듈을 등록하여 사용하거나, 전역 client 변수를 만들어서 사용하게되는데 저는 Global Prisma 모듈을 등록하여 사용하기 때문에 custom providerinjectPrismaService 를 주입하고, useFactory를 통해 PrismaService 객체를 주입받아 모듈 생성 시 UserRepository 객체가 생성되도록 구현합니다.

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...
}

0개의 댓글

관련 채용 정보