02 — TypeORM vs Prisma (데코레이터 vs 코드젠)
한 줄 답: *“TypeORM vs Prisma”*는 데코레이터 vs 코드젠의 대결이다. TypeORM은 TS class에 데코레이터를 박아 런타임에 메타데이터를 들고 다니고, Prisma는
schema.prismaDSL을 빌드 타임에 TypeScript 클라이언트로 컴파일한다. 타입 정확도는 Prisma가 압승, 유연성은 TypeORM이 우세 — 어느 손해를 견딜 수 있느냐가 결정의 본질.
Why — 왜 Prisma가 TypeORM의 자리를 위협하나
| 시기 | TypeORM | Prisma |
|---|---|---|
| 2016 | Umed Khudoiberdiev 1인 개발 시작 | (Prisma 1 — Graphcool 기반, 다름) |
| 2018 | NestJS와 함께 사실상 표준화 | (구상 중) |
| 2019 | v0.2 안정화 | Prisma 2 정식 발표 — 코드젠 도입 |
| 2020 | 유지보수 정체기 진입 | Prisma Client, Migrate 안정화 |
| 2022 | v0.3 — Connection → DataSource breaking change | Prisma 4 — preview features 폭발적 추가 |
| 2024 | v0.3.x 점진 개선 | Prisma 5 — raw SQL 지원 강화, TypedSQL |
| 2024~ | GitHub stars ~33k | GitHub stars ~37k — TypeORM 추월 |
2024년 Prisma의 GitHub stars가 TypeORM을 추월했다. 같은 시기 State of JS 2023 설문에서 Prisma의 “다시 쓰겠다” 비율이 TypeORM의 두 배에 가깝다. 이주가 실제로 일어나고 있다.
세 가지 이유.
- 타입 정확도의 격차: TypeORM의
find반환 타입은 데코레이터로 추측한 부분 타입이지만, Prisma의 반환 타입은 코드젠으로 만든 완전한 타입이다. - 마이그레이션의 격차: TypeORM의
migration:generate는 기존 스키마 + 엔티티를 diff해 SQL을 만들지만, 생성 결과가 자주 부정확하다. Prisma의prisma migrate는 DSL 자체가 source of truth라 훨씬 안정적. - 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: true가name?: 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 컴파일러는 그 생성 코드를 검증한다.
nullable이string | 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)의 TypedSQL로 Prisma가 우세.
시나리오 D — 마이그레이션
TypeORM:
npm run typeorm migration:generate -- -n AddUserAge- 문제: 자주 부정확한 SQL을 생성한다. enum 변경·인덱스 이름 변경에서 false positive 빈번.
- 수동 보정이 잦다.
Prisma:
npx prisma migrate dev --name add_user_ageschema.prisma가 source of truth라 diff가 안정적.- 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 engine을 Node.js child process로 띄운다. AWS Lambda cold start에 200~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.prisma는 TypeScript가 아니다 — 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은 Connection을 DataSource로 이름 바꿨다 — 그리고 모든 코드를 다시 짜야 했다. 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이 아닌 또 다른 길.