🗄️ TypeORM8. 이론 & 대안 (Prisma · Drizzle · MikroORM)02 — TypeORM vs Prisma (데코레이터 vs 코드젠)

02 — TypeORM vs Prisma (데코레이터 vs 코드젠)

한 줄 답: *“TypeORM vs Prisma”*는 데코레이터 vs 코드젠의 대결이다. TypeORM은 TS class에 데코레이터를 박아 런타임에 메타데이터를 들고 다니고, Prisma는 schema.prisma DSL을 빌드 타임에 TypeScript 클라이언트로 컴파일한다. 타입 정확도는 Prisma가 압승, 유연성은 TypeORM이 우세 — 어느 손해를 견딜 수 있느냐가 결정의 본질.


Why — 왜 Prisma가 TypeORM의 자리를 위협하나

시기TypeORMPrisma
2016Umed Khudoiberdiev 1인 개발 시작(Prisma 1 — Graphcool 기반, 다름)
2018NestJS와 함께 사실상 표준(구상 중)
2019v0.2 안정화Prisma 2 정식 발표 — 코드젠 도입
2020유지보수 정체기 진입Prisma Client, Migrate 안정화
2022v0.3 — Connection → DataSource breaking changePrisma 4 — preview features 폭발적 추가
2024v0.3.x 점진 개선Prisma 5 — raw SQL 지원 강화, TypedSQL
2024~GitHub stars ~33kGitHub stars ~37k — TypeORM 추월

2024년 Prisma의 GitHub stars가 TypeORM을 추월했다. 같은 시기 State of JS 2023 설문에서 Prisma의 “다시 쓰겠다” 비율이 TypeORM의 두 배에 가깝다. 이주가 실제로 일어나고 있다.

세 가지 이유.

  1. 타입 정확도의 격차: TypeORM의 find 반환 타입은 데코레이터로 추측한 부분 타입이지만, Prisma의 반환 타입은 코드젠으로 만든 완전한 타입이다.
  2. 마이그레이션의 격차: TypeORM의 migration:generate기존 스키마 + 엔티티를 diff해 SQL을 만들지만, 생성 결과가 자주 부정확하다. Prisma의 prisma migrateDSL 자체가 source of truth훨씬 안정적.
  3. DX의 격차: Prisma Studio (GUI), 자동완성, prisma generate 워크플로는 현대 TypeScript 진영의 표준에 가깝다.

How — 두 도구의 근본 메커니즘이 어떻게 다른가

1) TypeORM — 런타임 메타데이터

// TypeORM — 모델 정의
import 'reflect-metadata'
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
 
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id!: number
 
  @Column()
  email!: string
 
  @Column({ nullable: true })
  name?: string
}
  • 컴파일 결과: TS class가 그대로 JS class로 남고 — 데코레이터 함수가 런타임에 호출되며 reflect-metadata에 메타가 저장된다.
  • 그 메타를 TypeORM이 런타임에 읽어 SQL을 만든다.
  • TypeScript 컴파일러는 데코레이터의 세부 옵션모른다. nullable: truename?: string동기화되어 있는지 검증할 방법이 없다.

2) Prisma — 빌드 타임 코드젠

// schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
}
npx prisma generate
# ← node_modules/@prisma/client/ 에 *완전 타입의 TS 클라이언트*가 생성됨
// 사용
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
 
const user = await prisma.user.findUnique({ where: { id: 1 } })
// ← user의 타입은 *완전히* { id: number; email: string; name: string | null }
//   select·include로 *부분 타입*을 골라도 *컴파일러가 정확히* 안다
  • 컴파일 결과: schema.prisma가 source of truth. prisma generate완전한 TS 코드를 생성.
  • TypeScript 컴파일러는 그 생성 코드검증한다. nullablestring | null그대로 매핑된다.

3) 두 메커니즘의 근본 차이


What — 구체적 시나리오로 비교

시나리오 A — select로 부분 필드만 가져오기

TypeORM:

const user = await userRepo.findOne({
  where: { id: 1 },
  select: { id: true, email: true }
})
// user의 타입은? — User | null (전체 타입 그대로)
// ← select했어도 *타입은 좁혀지지 않는다*
console.log(user?.name)   // ← TS 에러 없음. 하지만 *런타임에 undefined*

Prisma:

const user = await prisma.user.findUnique({
  where: { id: 1 },
  select: { id: true, email: true }
})
// user의 타입은? — { id: number; email: string } | null
// ← *정확히 select한 필드만*
console.log(user?.name)   // ← TS 에러 — name은 없는 필드

판정: Prisma 압승. TypeORM의 select 타입 추론은 2024년 현재도 부분적이다.

시나리오 B — 관계 include

TypeORM:

const post = await postRepo.findOne({
  where: { id: 1 },
  relations: { author: true, comments: true }
})
// post.author의 타입은? — User | null (author는 *항상 옵셔널*로 추론됨)
// ← relations로 로드했어도 *not null임을 컴파일러가 모름*
console.log(post.author.email)   // ← TS 에러 (! 강제 필요)

Prisma:

const post = await prisma.post.findUnique({
  where: { id: 1 },
  include: { author: true, comments: true }
})
// post.author의 타입은? — User (include했으므로 *not null*)
console.log(post.author.email)   // ← TS 에러 없음

판정: Prisma 압승. 관계 로딩의 타입 정확도가 근본적으로 다르다.

시나리오 C — Raw SQL 작성

TypeORM:

const result = await dataSource.query(
  `SELECT id, email FROM users WHERE age > $1`,
  [18]
)
// result의 타입은? — any[]
// ← raw SQL은 *항상 any*

Prisma:

// Prisma 5의 TypedSQL (2024)
// prisma/sql/getUsers.sql
// SELECT id, email FROM users WHERE age > $1
import { getUsers } from '@prisma/client/sql'
 
const result = await prisma.$queryRawTyped(getUsers(18))
// result의 타입은? — { id: number; email: string }[]
// ← *raw SQL도 타입이 들어옴*

판정: 이전엔 비등했지만 Prisma 5(2024)의 TypedSQLPrisma가 우세.

시나리오 D — 마이그레이션

TypeORM:

npm run typeorm migration:generate -- -n AddUserAge
  • 문제: 자주 부정확한 SQL을 생성한다. enum 변경·인덱스 이름 변경에서 false positive 빈번.
  • 수동 보정이 잦다.

Prisma:

npx prisma migrate dev --name add_user_age
  • schema.prismasource of truthdiff가 안정적.
  • Shadow database결과 검증까지 자동.

판정: Prisma 압승. 마이그레이션 안정성은 Prisma의 가장 큰 우위.

시나리오 E — Raw SQL escape hatch (즉시 필요할 때)

TypeORM:

const result = await dataSource.query(`COMPLEX SELECT ...`)
// ← 단순. 즉시 동작.

Prisma:

const result = await prisma.$queryRawUnsafe(`COMPLEX SELECT ...`)
// ← 가능하지만 *typed가 아닌* unsafe 경로

판정: TypeORM이 살짝 더 단순. raw SQL이 즉시 필요한 OLAP/분석 쿼리에서 TypeORM의 manager.query가 더 직관적.


What-if — 잘못 선택하면

1) 강한 타입 정확도가 필요한 도메인에 TypeORM

→ 결제 금액 계산에서 select 누락 필드undefined로 흘러 0원 결제. 컴파일러가 못 잡음. 대응: 결제·핀테크·헬스 도메인이면 Prisma를 먼저 고려.

2) Rust 엔진을 거부하는 환경에 Prisma

→ Prisma는 Rust로 짠 query engineNode.js child process로 띄운다. AWS Lambda cold start200~500ms 추가. 대응: Edge/Serverless 우선이면 Drizzle 또는 Prisma의 Driver Adapters(2024+) 고려.

3) NestJS 생태계에 Prisma

→ Prisma는 NestJS 1급 통합이 없다. nestjs-prisma 같은 커뮤니티 모듈이 있지만 DataSource 추상은 직접 짜야 함. 대응: NestJS의 모듈 생태계가 매출에 직결되면 TypeORM 유지.

4) 자주 변하는 raw SQL이 필요한데 Prisma

→ OLAP 쿼리·임시 분석 쿼리가 많으면 TypedSQL의 빌드 사이클느린 iteration이 된다. 대응: TypeORM의 manager.query 또는 Drizzle.

5) DSL을 거부하는 팀에 Prisma

schema.prismaTypeScript가 아니다 — DSL 학습이 필요. 이게 팀의 거부감을 부른다. 대응: 순수 TS만 고집하면 TypeORM 또는 Drizzle.


Insight — 흥미로운 이야기

”Prisma 1의 실패 그리고 Prisma 2의 재탄생

Prisma 1은 2018년 Graphcool이라는 GraphQL 백엔드 서비스로 시작했다 — 자체 Prisma Server가 필요했고, 복잡한 인프라를 요구했다. 상업적으로 실패했다.

2019년 Prisma 2는 완전히 다른 길을 갔다 — 서버 없음, 클라이언트만, 코드젠 중심. Johannes Schickling은 *“Prisma 1은 실수였다. Prisma 2가 진짜 Prisma다”*라고 공개적으로 인정했다. 이 self-correct가 결과적으로 Prisma의 시장 점령을 이끌었다.

”TypeORM 0.3의 Breaking Change

2022년 TypeORM 0.3은 ConnectionDataSource로 이름 바꿨다 — 그리고 모든 코드를 다시 짜야 했다. getRepository(User) 같은 전역 API제거됐다 (deprecated). 많은 팀이 이 시점에 *“이럴 거면 Prisma로 가자”*고 결정했다.

CHANGELOG.md를 보면 0.3.0의 변경 목록이 200+ 줄이다. 마이그레이션 가이드 (docs/changelog/0.3.0.md)는 Prisma로 이주한 팀들이 공통적으로 언급하는 결정 시점이다.

”Prisma의 Rust 엔진 논쟁”

Prisma는 query engine을 Rust로 짰다. Node.js 프로세스에서 별도 binary가 child process로 뜬다. 장점은 성능과 안정성이지만 — 단점은 바이너리 사이즈, cold start, 서버리스 디버깅 어려움.

2024년 Prisma는 Driver Adapters를 도입해 Rust 엔진을 우회하는 길을 열었다 (@prisma/adapter-neon, @prisma/adapter-planetscale 등). 이게 *Drizzle의 핵심 무기인 “Rust 없음”*을 받아치는 응답이다.


요약 + 다이어그램

TypeORM vs Prisma = 데코레이터 vs 코드젠. 타입 정확도는 Prisma가 압승, 유연성·NestJS 통합·raw SQL 즉시성은 TypeORM이 우세. 어느 손해를 견딜 수 있는가가 결정. 결제·핀테크·헬스 도메인이면 Prisma, NestJS 모놀리스/OOP 도메인이면 TypeORM.

다음 문서: 03-typeorm-vs-drizzle.mdx — 그럼 Drizzle은? ORM이 아닌 또 다른 길.