03 — TypeORM vs Drizzle (ORM vs SQL DSL)

한 줄 답: Drizzle은 ORM이 아니다 — SQL의 TypeScript DSL이다. 런타임에 메타데이터를 들고 다니지 않으며, TypeORM은 런타임 메타로 모든 마법을 한다. 런타임 제로 코스트가 필요한 서버리스·엣지에 Drizzle, OOP 도메인 객체와 관계 풍부 모델에 TypeORM.


Why — 왜 Drizzle이 TypeScript 진영의 떠오르는 별이 됐나

Drizzle은 2022년 등장해 2023~2024년에 폭발적으로 성장했다. GitHub stars는 2024년 ~20k에 도달했고 — State of JS 2023에서 “가장 흥미로운 ORM” 1위로 뽑혔다.

세 가지 이유.

  1. 런타임 제로 코스트: 메타데이터를 컴파일 타임에 TS 타입으로만 표현 — 런타임에는 그냥 SQL 생성 함수. 서버리스 cold start거의 없다.
  2. SQL 그대로의 사고: ORM 추상이 거의 없다. select().from().where()SQL 키워드 그대로. SQL을 아는 팀에게 학습 비용이 0에 가깝다.
  3. ESM 우선: 2022년 이후 등장한 도구라 ESM이 기본. TypeORM의 CJS 친화 + ESM 부분 지원과 대조.

Drizzle의 캐치프레이즈: “It’s like ORM but it isn’t”의도적으로 ORM이 아니길 자처한다.


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

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

import 'reflect-metadata'
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
 
@Entity()
class User {
  @PrimaryGeneratedColumn()
  id!: number
 
  @Column()
  email!: string
}
 
// 런타임: TypeORM이 reflect-metadata를 *읽어* SQL 생성
const users = await userRepo.find({ where: { email: 'x@x.com' } })
  • 런타임에 reflect-metadata가 메모리에 살아 있어야 한다.
  • TypeORM 코어는 그 메타를 읽고 SQL을 만든다 — 일종의 작은 인터프리터.

2) Drizzle — SQL DSL, 런타임 메타 없음

import { pgTable, serial, text } from 'drizzle-orm/pg-core'
import { eq } from 'drizzle-orm'
 
// 테이블 정의 — *런타임 메타가 아니라 컴파일 타임 객체*
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: text('email').notNull(),
})
 
// 사용
const result = await db.select().from(users).where(eq(users.email, 'x@x.com'))
// ↑ 거의 SQL 그대로
// → "select * from users where email = 'x@x.com'"
  • pgTable(...)런타임에 단순 객체다. 데코레이터 메타데이터 없음, reflect-metadata 없음.
  • select().from().where()SQL 빌더. 런타임에는 문자열 SQL 생성기가 동작할 뿐.
  • TypeScript 컴파일러컬럼 타입을 추론반환 타입까지 완전히 채워준다.

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


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

시나리오 A — 기본 select

TypeORM:

const users = await userRepo.find({
  where: { age: MoreThan(18) },
  order: { createdAt: 'DESC' },
  take: 10
})

Drizzle:

import { gt, desc } from 'drizzle-orm'
 
const result = await db
  .select()
  .from(users)
  .where(gt(users.age, 18))
  .orderBy(desc(users.createdAt))
  .limit(10)

판정: Drizzle이 SQL에 더 가깝다. TypeORM의 MoreThan, Like 같은 연산자 wrapper가 더 abstract. SQL에 익숙한 팀은 Drizzle이 바로 읽힌다.

시나리오 B — Join

TypeORM:

const posts = await postRepo.find({
  relations: { author: true, comments: true }
})
// 또는 QueryBuilder
const posts = await postRepo
  .createQueryBuilder('post')
  .leftJoinAndSelect('post.author', 'author')
  .leftJoinAndSelect('post.comments', 'comment')
  .getMany()

Drizzle:

const result = await db
  .select()
  .from(posts)
  .leftJoin(users, eq(posts.authorId, users.id))
  .leftJoin(comments, eq(comments.postId, posts.id))
// ← *완전히 SQL 그대로*

판정: TypeORM의 relations마법처럼 편하지만 N+1을 숨긴다. Drizzle은 N+1이 일어날 수 없다 — 모든 join이 명시적. 디버깅·성능 분석 모두 Drizzle이 투명.

시나리오 C — 서버리스 cold start

TypeORM (AWS Lambda):

// 차가운 람다 호출 첫 응답: ~300-500ms
// (reflect-metadata 로드 + DataSource.initialize + 메타 빌드)

Drizzle (AWS Lambda):

// 차가운 람다 호출 첫 응답: ~50-100ms
// (SQL 빌더 함수 import만)

판정: Drizzle 압승. 서버리스/엣지가 매출에 직결되면 Drizzle이 기본 선택. Vercel·Cloudflare Workers·AWS Lambda의 공식 권장 중 하나.

시나리오 D — 복잡한 도메인 객체 (OOP)

TypeORM:

@Entity()
class Order {
  @PrimaryGeneratedColumn()
  id!: number
 
  @Column()
  amount!: number
 
  // 도메인 메서드를 *클래스에 직접* 정의
  isOverdue(): boolean {
    return this.createdAt < daysAgo(30) && this.status === 'pending'
  }
 
  applyDiscount(rate: number): void {
    this.amount = this.amount * (1 - rate)
  }
}

Drizzle:

export const orders = pgTable('orders', { ... })
// ← *클래스가 아니다*. 메서드를 *붙일 곳이 없다*.
// 도메인 로직은 *별도 모듈*로 짜야 함
 
function isOverdue(order: Order): boolean { ... }
function applyDiscount(order: Order, rate: number): Order { ... }

판정: TypeORM 우세. DDD/OOP 스타일 도메인 모델이면 클래스에 메서드를 붙이는 TypeORM이 자연스럽다. Drizzle은 함수형 접근을 강제.

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

TypeORM:

npm run typeorm migration:generate -- -n AddAge
# ← 종종 부정확한 SQL 생성

Drizzle:

npx drizzle-kit generate
# ← schema.ts 파일 diff → SQL 마이그레이션 생성
# ← 일반적으로 *안정적*

판정: Drizzle 우세. drizzle-kitPrisma migrate에 가까운 안정성을 보인다.


What-if — 잘못 선택하면

1) OOP 도메인 모델인데 Drizzle

→ 클래스 메서드·상속·polymorphism을 못 쓴다. 함수형 스타일을 강제받음. 대응: 풍부한 도메인 객체가 필요하면 TypeORM 또는 MikroORM.

2) 서버리스인데 TypeORM

→ Lambda cold start가 300-500ms. p99 응답이 목표를 못 맞춤. 대응: Drizzle 또는 Prisma + Driver Adapters.

3) SQL에 익숙하지 않은 팀인데 Drizzle

→ Drizzle은 SQL 추상이 거의 없다. SQL을 모르면 Drizzle 자체를 못 쓴다. 대응: SQL 학습부터, 또는 TypeORM/Prisma의 추상에 의지.

4) NestJS 통합이 필요한데 Drizzle

→ Drizzle은 NestJS 공식 모듈이 없다. DI 통합을 직접 짜야 한다. 대응: NestJS 친화도가 매출이면 TypeORM (@nestjs/typeorm).

5) “Drizzle도 ORM의 일종”이라 기대

→ Identity Map·Unit of Work·자동 cascade 같은 ORM 기능을 기대했다가 없어서 좌절. 대응: 처음부터 SQL 빌더라 인정하고 그 위에 도메인 레이어를 직접 쌓는다.


Insight — 흥미로운 이야기

”Drizzle은 Knex의 후예다”

Drizzle의 DNAKnex.js (2013, JavaScript의 SQL query builder)에 가깝다. Knex는 ORM이 아니라 query builder였고 — 한때 Bookshelf.js의 기반 레이어였다.

Drizzle은 Knex의 사고를 TypeScript로 옮긴 도구다 — 완전한 TS 타입 추론ESM 우선을 더해서. *“ORM이 아니다”*라는 자처가 명예가 아니라 사양이다.

”Vercel·Neon의 밀어주기

Drizzle은 VercelNeon(Postgres serverless)의 공식 권장에 들어 있다. 두 회사는 서버리스 Postgres를 시장에 밀고 있고 — TypeORM의 cold start가 그 시장에서 치명적이라는 것을 안다.

2024년 Vercel의 Postgres 가이드DrizzlePrisma (Driver Adapters)를 동시에 추천한다. TypeORM은 등장하지 않는다. 이게 생태계의 신호다.

”Drizzle의 명시성 철학

Drizzle의 README에는 *“We do not want to be a magic library”*라고 직접 쓰여 있다. TypeORM의 데코레이터 마법, Prisma의 코드젠 마법, MikroORM의 Unit of Work 마법 — 모두를 거부하는 입장.

이게 현대 TypeScript 진영의 트렌드와 맞아떨어진다. tRPC도, Drizzle도, Hono도 — *“마법 없이 타입만”*의 같은 가족이다. 명시성을 사양으로 삼은 도구들.


요약 + 다이어그램

TypeORM vs Drizzle = ORM vs SQL DSL. Drizzle은 런타임 메타가 0이라 서버리스·엣지에 강하고, TypeORM은 런타임 메타로 OOP 도메인을 풍부하게 표현한다. 서버리스 + SQL 친숙 팀이면 Drizzle, OOP 도메인 + NestJS면 TypeORM.

다음 문서: 04-typeorm-vs-mikroorm.mdx — 그럼 진짜 Unit of Work를 가진 MikroORM은?