Development··6 min read

Prisma에서 Drizzle ORM으로 갈아탄 이야기

Prisma의 한계에 부딪히고 Drizzle로 마이그레이션한 2주간의 기록

Prisma가 느려지기 시작한 순간

프로젝트 초기에는 Prisma가 완벽했다. 스키마 정의하고, npx prisma generate 돌리면 타입 안전한 클라이언트가 뚝딱 나온다. 근데 테이블이 47개로 늘어나고, 릴레이션이 복잡해지면서 문제가 생겼다.

prisma generate가 12초 걸리기 시작했다. 코드 한 줄 바꾸고 핫 리로드가 될 때마다 Prisma Client가 재생성된다. 12초. 하루에 이게 수십 번 반복되면 체감이 엄청나다. (정확히는 12초에서 5분 40초. 스키마가 바뀌는 날은 5분 넘게 걸렸다.)

그리고 쿼리 성능 문제. Prisma가 생성하는 SQL을 로그로 찍어봤는데, 단순한 조인 쿼리에서 서브쿼리를 4개나 만들고 있었다. 수동으로 SQL을 짜면 1개의 JOIN으로 끝나는 건데.

Drizzle을 고른 이유

대안으로 Drizzle, Kysely, TypeORM을 놓고 비교했다. TypeORM은 경험상 데코레이터 지옥이라 제외. Kysely는 쿼리 빌더로서는 좋지만 마이그레이션 도구가 부실했다.

Drizzle을 고른 결정적 이유는 두 가지. 첫째, SQL에 가까운 쿼리 작성 방식. Prisma처럼 추상화가 두꺼운 게 아니라, SQL을 TypeScript로 쓰는 느낌이다. 둘째, 번들 사이즈. Prisma Client가 프로젝트에 추가하는 용량이 상당한데, Drizzle은 훨씬 가볍다.

// Prisma - 무슨 SQL이 나올지 예측이 어렵다
const users = await prisma.user.findMany({
  where: { posts: { some: { published: true } } },
  include: { posts: true, profile: true },
});
 
// Drizzle - SQL이 거의 그대로 보인다
const users = await db
  .select()
  .from(usersTable)
  .leftJoin(postsTable, eq(usersTable.id, postsTable.userId))
  .where(eq(postsTable.published, true));

마이그레이션 2주간의 기록

마이그레이션을 한 번에 하려다가 실패했다. 테이블 47개를 한 번에 옮기려니까 너무 방대했다. 결국 점진적 마이그레이션으로 전환했다. Prisma와 Drizzle을 동시에 돌리면서, 새로운 기능은 Drizzle로, 기존 기능은 점진적으로 옮기는 방식.

이게 가능한 이유는 둘 다 같은 데이터베이스를 바라보기 때문이다. 스키마만 동기화되면 문제없다. 근데 스키마 동기화가 은근 까다로웠다. Prisma의 @map이나 @@map으로 테이블/컬럼 이름을 바꿔놓은 게 있어서, Drizzle 스키마에서 이걸 정확히 맞춰야 했다. 이것 때문에 하루 반을 날렸다.

실수한 것: 트랜잭션 처리

가장 뼈아팠던 실수는 트랜잭션 처리다. Prisma에서는 $transaction으로 간단하게 처리하던 것을 Drizzle로 옮기면서 트랜잭션을 빼먹은 곳이 3군데 있었다. 스테이징에서 발견해서 다행이지, 프로덕션에 나갔으면 데이터 정합성 문제가 생겼을 거다.

교훈: ORM을 바꿀 때는 기존 코드에서 트랜잭션이 걸려 있는 곳을 전부 리스트업하고, 마이그레이션 후에 하나하나 확인해야 한다. 나는 이걸 안 하고 "대충 옮기면 되겠지" 했다가 코드 리뷰에서 걸렸다.

2주 뒤 결과

generate 시간이 12초에서 0초가 됐다. Drizzle은 런타임에 코드를 생성하지 않으니까. 핫 리로드가 즉시 반영된다. 이것만으로도 마이그레이션한 가치가 있다.

쿼리 성능은 평균적으로 1.4배 빨라졌다. 특히 복잡한 조인 쿼리에서 차이가 컸다. 서브쿼리 4개가 JOIN 1개로 바뀌면 당연히 빨라진다.

아쉬운 점도 있다. Prisma Studio 같은 GUI 관리 도구가 Drizzle에는 없다. Drizzle Studio가 있긴 한데 아직 기능이 부족하다. 그리고 Prisma의 include 문법이 직관적이었는데, Drizzle의 조인 문법은 SQL을 어느 정도 알아야 쓸 수 있다. 주니어 팀원한테 러닝 커브가 있었다. 마이그레이션 자체는 2주 걸렸는데, 팀 전체가 Drizzle에 익숙해지는 데는 한 달 반이 걸렸다.

관련 글