NodeJS에서의 ORM 선택 (2) : Prisma
2021년 04월 16일

서론

TypeORM의 한계를 느껴 새로운 대체 라이브러리를 찾던 도중 발견했습니다. 사실 기존에 알고 있는 Prisma는 version 1으로 사실상 deprecated 되었고, 새로 개편한 버전인 Prisma2입니다. 4월 중순 경, 기존에 TypeORM을 이 Prisma로 전면 교체하는 작업을 하였고 그에 관련된 사항들을 기록해 보았습니다.

장점 1: 듬직한 타입 지원

  • 스키마를 schema.prisma에 전부 정의합니다. 이 파일이 앞으로 죽 single source of truth가 됩니다.
  • 스키마로부터 type을 일괄 generate 하여 node_modules에 추가합니다.
    • 기본적인 model, where, orderBy, select 등 모든 파라미터를 타입으로 생성
// 예를 들어 아래와 같이 schema.prisma에 스키마를 정의해둡니다. model Book { id Int @id @default(autoincrement()) name String @db.VarChar(128) type String @db.VarChar(128) createdAt DateTime @default(now()) @db.DateTime(6) updatedAt DateTime? @default(now()) @db.DateTime(6) deletedAt DateTime? @db.DateTime(6) }
// prisma generate 시, 아래와 같이 where, order절에 들어가는 모든 타입들이 정의해 놓은 스키마 기반으로 자동 생성됩니다. export type BookWhereInput = { AND?: Enumerable<OrderWhereInput> OR?: Enumerable<OrderWhereInput> NOT?: Enumerable<OrderWhereInput> id?: IntFilter | number name?: StringNullableFilter | string | null type?: StringNullableFilter | string | null } export type BookOrderByInput = { id?: SortOrder name?: SortOrder type?: SortOrder category?: SortOrder deletedAt?: SortOrder } export const SortOrder: { asc: 'asc', desc: 'desc' }; export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
// 그렇게 타입 생성이 되면 아래와 같이 사용 가능합니다. 물론 type strict 합니다. const book = await prisma.book.findUnique({ where: { id: 1 } }) const books = await prisma.book.findMany({ where: { name: 'foo' }, orderBy: { id: 'asc' } })
  • relation 접근이 좀 더 유연합니다.
    • ORM에서 relation을 정의할때 연결된 테이블을 fk가 아닌 object로만 접근 가능해야 하도록 구현된 ORM이 많습니다. Prisma에서는 fk id, object 모두 지원합니다.
// 일부 ORM const userId = 3; const user = this.UserRepository.findOne(3) const companies = await this.CompanyRepository.find({ user: user }) // prisma const userId = 3 const companies = await this.Company.findMany({ userId : userId })

장점 2: migration

migration 및 up, down SQL를 감지 생성합니다. 이 역시 schema.prisma 기반으로 체크합니다.

  • local에서는 데이터가 매우 fraglie 함 : 의도됨
    • 변경시 수시로 리셋+seed 및 shadow database 를 통한 마이그레이션 히스토리 검증
  • production에서는 사용빈도 낮은 down 개념 제거, 적용 안된 마이그레이션만 순서대로 반영
    • production에서 migration down 등 roll-back 액션이 오히려 사이드 이펙트 및 리스크가 클 수 있습니다.

장점 3: 낮은 러닝커브, 직관적 사용

일반적으로 생각되는 기능들은 문서화가 잘 되있어서, 문서만 보고도 쉽게 사용해 볼 수 있습니다.

https://www.prisma.io/docs/concepts/components/prisma-client/crud

image1

CREATE

// CREATE async function createUser() { const user = await prisma.user.create({ data: { email: 'elsa@prisma.io', name: 'Elsa Prisma', }, }) } // CREATE WITH INCLUDE async function createUserWithPost() { let includePosts: boolean = false let user: Prisma.UserCreateInput // Check if posts should be included in the query if (includePosts) { user = { email: 'elsa@prisma.io', name: 'Elsa Prisma', posts: { create: { title: 'Include this post!', }, }, } } else { user = { email: 'elsa@prisma.io', name: 'Elsa Prisma', } } // Pass 'user' object into query const createUser = await prisma.user.create({ data: user }) }
const user = await prisma.user.findUnique({ where: { id: '60d5922d00581b8f0062e3a8', }, }) const users = await prisma.user.findMany({ where: { OR: [ { name: { startsWith: 'E', }, }, { AND: { profileViews: { gt: 0, }, role: { equals: 'ADMIN', }, }, }, ], }, })
const updateUser = await prisma.user.update({ where: { email: 'viola@prisma.io', }, data: { name: 'Viola the Magnificent', }, }) const upsertUser = await prisma.user.upsert({ where: { email: 'viola@prisma.io', }, update: { name: 'Viola the Magnificent', }, create: { email: 'viola@prisma.io', name: 'Viola the Magnificent', }, })
const deleteUser = await prisma.user.delete({ where: { email: 'bert@prisma.io', }, }) const deleteUsers = await prisma.user.deleteMany({ where: { email: { contains: 'prisma.io', }, }, })

relation (join)

TypeORM같은 경우 relation 관련하여 복잡도가 높아질수록 기능이 지원되지 않아 부분적 query builder를 사용해야만 했었습니다.

하지만 prisma는 join with condition, n-depth relation, self relation 등을 문제 없이 제공함을 확인했습니다.

// find const result = await prisma.user.findMany({ where: { email: { endsWith: 'prisma.io', }, posts: { some: { published: true, }, }, }, include: { posts: { where: { published: true, }, }, }, }) const users = await prisma.user.findMany({ where: { posts: { none: { views: { gt: 100, }, }, every: { likes: { lte: 50, }, }, }, }, })
const createUserAndPost = await prisma.user.create({ data: { email: 'elsa@prisma.io', name: 'Elsa Prisma', posts: { create: [ { title: 'How to make an omelette' }, { title: 'How to eat an omelette' }, ], }, }, })

장점 4: 2.0버전 정식 런칭 및 투자로 인한 인력 충원 예상

12M Dollar : 한화 약 138억

https://www.prisma.io/blog/prisma-raises-series-a-saks1zr7kip6

0.x대 버전을 유지하고 있고, 개발 인력이 부족해 이슈가 해결되지 않는 TypeORM과 대조해서 바라보게 됩니다.

장점 5: 잘 정리된 문서 & 꾸준한 밋업 활동 & 인력

꾸준한 소통 활동 - 유튜브, 밋업, 슬랙

한계점

  1. 아직은 지원하지 않는 기능들
  • transaction
  • vendor specific한 기능을 사용해야 할 시 raw query를 사용할 수 밖에 없습니다.
    • 이부분은 사실 ORM 자체의 한계점.
  1. prisma migrate의 안정화 필요
  • production에서는 사실 문제가 없는데, local에서 migrate, reset 시 간혹 에러가 발생하여 database 자체를 재생성행 할 때도 있습니다.
  1. 오직 타입으로만 정의되기 때문에, NestJS와 어우러지기 위해서는 class 재정의가 필요합니다.
  • generator 제작으로 해결 예정
  1. 다소 무겁습니다.

  1. 한국에서의 인지도는 아직 낮다.
  • 하지만 최근 밋업에 한국인들도 등장하고 있는 등 점점 커뮤니티도 활발해질 것으로 예상됩니다.

결론

TypeORM에서 기술적 한계를 느껴 Prisma로 전환하는 리팩토링 과정을 거졌고, 결론적으로는 코드 퀄리티 및 가독성이 대폭 상승했습니다.

기술이 되었든 그렇지 않은 것이든 실제 프로덕트에 이미 적용되어 있는 무언가를 개선하는 것은 정말 발전적인 행동이라고 생각합니다. 하지만 그렇게 위해서는 거쳐야할 과정이 있습니다. 이러한 포인트를 체크하는 것이 기술적 리서치 못지않게 중요하다는 점을 이번 마이그레이션을 통해 느꼈습니다.

  1. 팀원(개발자)들을 설득하는 과정

    • 아무리 좋은 기술이다 하더라도 이 기술이 어떤 장점이 있는지, 기존의 문제를 해결하는 데 더 도움을 줄 수 있는지 등등을 구성원들에게 인지 및 공감을 시키는 것이 첫 번째로 해야할 점입니다!
  2. 적용에 필요한 일정 산정

    • 이러한 리팩토링 성질을 띄고 있는 작업을 할 시 기존의 작업을 단기적으로 미루고 일정 및 태스크를 추가 산정해야 합니다.
    • 이러한 태스크를 수행 시 큰 그림으로 봤을 때 개발자의 생산성이 올라간다는 점을 일정 관리자 한테 납득을 시켜야 합니다.

Untitled

    - 당장의 기능이 추가된다던가 하지는 않지만, 투자할만한 가치다 있다는 점을 인지시키기

3. 리스크(소위 달리는 자동차 위에서의 엔진 교체) 를 예측 및 방지 - 어떠한 작업이 들어가더라도, 기존의 작업물 및 프로덕트에 영향을 미치면 안됩니다. - 특히 이미 런칭되어 비즈니스가 돌아가고 있는 환경이면 더블 체크 혹은 그 이상이 동반되어야 합니다.

아직까지는 프로덕션에서 큰 이슈는 발생하지 않았지만, 자잘한 이슈가 있기는 하여 그 부분은 별도로 추가적인 글을 작성할 계획입니다.