Hits
NestJS 2022. 9. 20. 오후 4:40:00

NestJS에서 Prisma 사용하기

"NestJS에서 기본적인 Prisma사용과 test를 위한 PrismaClient 추가 DB연결"
nestjsmongodbprimsajest

이 글은 2022/06/09에 작성된 글입니다

NestJS + Prisma

처음 백엔드를 구축할때 Apollo + GraphQL을 사용하기로 하면서 쓰기 시작한 Prisma를 NestJS + RestAPI로 migration하면서 NestJS와 많이 쓰는 TypeORM보다 익숙하고 편리해서 가져왔는데 막상 쓰려니 부족한 자료에 지금이라도 TypeORM으로 넘어가야하나 하는 고민을 수없이 했습니다. 하지만 가장 큰 문제는 클래스환경에 대한 Prisma의 문서부족이었습니다. 기본적인 세팅에 대한 부분은 Prisma와 NestJS공식문서에 잘 나와있었지만 조금만 깊어지면 문서가 영문으로조차 없어서 며칠동안 삽질을 했는데요. 대체적으로 클래스와 테스트에 대한 내 이해부족때문이었습니다. NestJS시작부터 글을 적을 생각이었지만 기억이 날아갈까봐 우선 Prisma적용에 대한 글부터 남깁니다.

여러모로 부족해 설명에 오류나 누락이 있을 수 있습니다. 혹시나 이 글을 보신다면 많은 피드백 부탁드립니다.

1. NestJS에 PrismaClient 생성

  1. 우선 NestJS와 Prisma 기본세팅이 돼있다는걸 가정하고 바로 PrismaClient생성으로 넘어가겠습니다. nest-cli를 이용해 prisma모듈과 서비스를 생성합니다.
    nest g mo prisma && nest g s prisma
  2. prisma.service.ts에서 아래의 코드를 입력합니다.
    // ~/src/prisma/prisma.service.ts
    import { INestApplication, Injectable, OnModuleInit } from "@nestjs/common"
    import { PrismaClient } from "../../prisma/generated" //보통은 @prisma/client에서 불러오지만 yarn berry를 써서 따로 생성된 디렉토리에서 불러왔습니다.
    
    @Injectable()
    export class PrismaService extends PrismaClient implements OnModuleInit {
      async onModuleInit() {
        await this.$connect()
      }
    
      async enableShutdownHook(app: INestApplication) {
        this.$on("beforeExit", async () => {
          await app.close()
        })
      }
    }
    여기서부터 당황스러웠습니다. 지금까지 프리즈마 클라이언트를 생성하면 const prisma = new PrismaClient()같은 방법을 썼는데 서비스를 PrismaClient에서 extends하고 이걸 onModuleInit에 implement를 합니다.extends? implement? 공식문서에 이유가 상세히 잘 나와있지만 간단하게 설명하자면 Prisma를 DB에 빠르게 연결하기 위해 onModuleInit을 사용합니다. 선택사항이지만 사용하지 않을경우 Prisma는 첫 호출이 있기 전까지 DB에 연결하지 않습니다. enableShutdownHooks는 공식문서와 이 포스트를 보면 Prisma와 NestJS가 종료메서드에 상호간섭하기 때문에 서비스에서 함수를 생성하고 main.ts에서 호출해서 종료를 강제해주는 것 같습니다.(이부분은 완벽하게 이해를 못했습니다.)
    // ~/src/main.ts
    
    import { NestFactory } from "@nestjs/core"
    import { AppModule } from "./app.module"
    import { PrismaService } from "./prisma/prisma.service"
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule)
      const prisma: PrismaService = app.get(PrismaService)
      prisma.enableShutdownHooks(app)
      await app.listen(3000)
    }
    bootstrap()
  3. prisma.module.ts에서 Global로 export해서 어느 모듈에서나 provider추가 없이 injection이 가능하도록 해줍니다.
    // ~/src/prisma/primsa.module.ts
    
    import { Global, Module } from "@nestjs/common"
    import { PrismaService } from "./prisma.service"
    
    @Global()
    @Module({
      providers: [PrismaService],
      exports: [PrismaService],
    })
    export class PrismaModule {}
  4. 이제 설정이 끝났고 다른 서비스에서 constructor에 PrismaService를 불러와 사용하면 됩니다.
    // ~/src/post/post.service.ts
    
    import { Injectable } from "@nest/common"
    import { Post } from "prisma/generated"
    import { PrismaService } from "src/prisma/prisma.service"
    //...
    
    @Injectable()
    export class PostService {
      constructor(private prisma: PrismaService) {}
    
      // ...
    
      async findOne(id: string): Promise<Post | null> {
        return this.prisma.post.findUnique({
          where: { id },
        })
      }
    
      // ...
    }

2. 테스트?

GraphQL을 버리고 처음 NextJS에서 기본 API routing으로 백엔드 기능을 구현했을때는 빠르게 최소한의 기능만 구축하기 위해서 테스트를 전혀 안하다가 NestJS로 넘어오면서 TDD를 해봐야겠다는 욕심(?)이 생겼습니다. 테스트는 기본 설정돼있는 Jest로 하려했는데 nomadcoder에서 NestJS + Jest 기본 강의만 듣고 시작한 저에게 TDD는 너무 큰 산이었습니다. 아직 간단한 CRUD밖에 없기때문에 테스트코드야 어떻게 짠다지만 DB와 ORM은 어떻게 해야하는지 전혀 감이 오지 않았습니다. 당장에 Jest자체도 낯설었습니다. 구글을 뒤져 NestJS와 Prisma를 사용해 테스트하는 예제 몇개를 구할 수 있었지만 대부분 Prisma를 mocking해서 테스트하는 방식이었습니다. 하지만 Prisma자체가 제대로 돌아갈지도 미지수인 상황에서 mocking만 해봤자 무슨의미인가 싶고 TDD를 도입하는 만큼 수동테스트를 최대한 줄이고싶었습니다. 문제는 현재 개발용 DB에 테스트용 더미데이터를 넣어뒀는데 계속 불필요한 테스트코드가 쌓이는것도 싫고, 만약 서비스를 배포한 후에도 같은 DB에 연결되면 심각한 문제를 초래하기 때문에 별도의 테스트용 DB에 연결할 필요가 있었습니다. 문제는, 지금까지는 다른 DB에 연결할 필요가 없었기 때문에 방법을 전혀 모르는 상태였다는 겁니다. 여러모로 방법을 찾아봤는데 처음 생각한 방안은 PrismaClient를 서비스 DB와 Test DB에 각각 연결되도록 generate를 두번 하는 방법이었습니다. 하지만 방법도 복잡하고 Prisma Engine이나 generate에 대한 이해가 부족해서 맞는 방법인지 확신이 없어서 다른 방법을 찾아보았습니다.

3. Prisma ↔ Test용 DB 연결

한참 공식문서를 뒤지다가 단서를 얻었습니다. PrismaClient를 생성할 때 datasource를 overriding하는 방법이었습니다.

문제는 다시 클래스로 돌아옵니다. 기존의 함수형 코딩에서는

const prisma = new PrismaClient({
  datasource: {
    db: { url: DatabaseUrl },
  },
})

처럼 간단하게 datasource overriding을 할 수 있는데 NestJS같은 Class에서는 어떻게 해야할지 모르겠는데 자료도 없어 막막했습니다. 나오지도 않는 구글을 한참 뒤지다가, Prisma랑 PrismaClient라이브러리 소스를 한참뒤지다가 겨우 단서를 찾았습니다. constructor에 super로 선언해주면 되는 간단한 문제였습니다.

코드
// ~/src/prisma/prisma.service.ts

// import ...

@Injectable()
export class PrismaService extends PrismaClient implements onModuleInit {
  constructor() {
    const url =
      process.env.NODE_ENV === "test"
        ? process.env.TEST_DATABASE_URL
        : process.env.DATABASE_URL
    super({
      datasources: {
        db: {
          url,
        },
      },
    })
  }
}

// async onModuleInit...

방법은 찾았는데 DB URL을 어떻게 입력할지를 놓고 또 한참 씨름했습니다. .env에 넣고 NODE_ENV에 따라 각각 다른 주소가 들어가도록 하는데 @nestjs/config를 쓰니 문제가 발생했습니다. .env와 .env.development.env.test에 모두 DATABASE_URL이라는 같은 환경변수로 설정하고 돌렸더니 주구장창 .env에 있는 주소만 입력돼서 난감했습니다. .env는 DATABASE_URL로 유지하고 개발과 테스트 환경변수를 다른 이름으로 지정했더니 제대로 불러오는데 서버를 직접 돌릴때는 NODE_ENV를 바꿔가며 돌려봐도 문제없이 잘 돌아가는데 jest로 테스트만 돌리면 config가 환경변수를 불러오지 못했습니다. 한참씨름하다가 그냥 .env에 TEST_DATABASE_URL로 Test DB 이름을 설정하고 process.env로 불러오는 방법을 택했더니 잘 돌아가는군요.

결과

이것저것 붙잡고 한참 씨름한 덕분에 constructor와 super에 대한 이해도와 @nestjs/config사용, 환경변수 적용이 익숙해졌습니다. 뜯어보면 별거 아니지만 누군가는 나와 같은 문제를 겪을 수도 있을 것 같아 문제 해결하고 잊어버리기 전에 급하게 적었습니다. 테스트도 결국 비용이고 대부분의 테스트코드가 mocking으로 적히는 걸로 보아 그게 효율적인 방법일 것 같아서 어느정도 DB에 직접 연결해서 테스트를 하다가 규모가 커지면 mocking해서 테스트 하는 방법으로 전환해야겠습니다.

참고자료