06 — TypeORM을 쓰지 말아야 할 때

한 줄 답: TypeORM이 모든 자리에 맞는 도구는 아니다. 다섯 자리에서 TypeORM은 손해가 커진다강한 타입 정확도, ESM 우선 스택, raw SQL이 즉시 필요한 OLAP, 서버리스·엣지, decorator spec 변동성에 민감한 팀. 이 중 두 개 이상 해당하면 Prisma 또는 Drizzle이 거의 항상 더 낫다.


Why — 왜 피해야 할 자리를 따로 정리하나

ORM 선택은 대규모 손해 가능성이 큰 결정이다. 6개월 작업한 코드를 다른 ORM으로 옮기는 비용은 수 주~수 개월. 처음에 잘못 고른 비용이 나중에 옮기는 비용보다 훨씬 작다.

이 문서는 TypeORM 진영에서도 동의하는 “피해야 할 자리”를 정리한다. 영업 입장이 아니라 기술 결정 회의에서 판단 근거가 될 자료.


How — 다섯 가지 피해야 할 자리

1) 강한 타입 정확도가 매출에 직결될 때

문제:

// 결제 도메인 — TypeORM
const payment = await paymentRepo.findOne({
  where: { id: paymentId },
  select: { id: true, amount: true }
  // ← currency를 select 안 함
})
 
// 컴파일러는 *currency 누락을 못 잡는다*
// payment.currency → undefined
const totalKrw = convertToKrw(payment.amount, payment.currency)
// ← currency가 undefined → 변환 실패 → 0원 결제 또는 NaN
  • currency 필드 누락TypeScript 컴파일러가 못 잡는다. TypeORM의 select 옵션이 반환 타입을 좁히지 않기 때문.
  • 결제 누락 1건당 수 만원~수 백만원 손해가 가능.

대응: Prisma. select반환 타입을 정확히 좁힌다 — 누락 필드 접근 시 컴파일 에러.

해당 도메인: 결제, 핀테크, 헬스케어, 보험, 거래 시스템.

2) ESM 우선 스택

문제:

  • TypeORM은 CJS 친화다. ESM에서는 experimentalDecorators + emitDecoratorMetadata런타임 polyfill 문제가 자주 발생.
  • tsx, esbuild, vite-node현대 TS 런너에서 데코레이터 메타데이터가 누락되는 사례 보고됨 (GitHub issue 다수).
// ESM + esbuild + TypeORM = 자주 동작 안 함
"type": "module",   // package.json
"compilerOptions": {
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true,
  "module": "ESNext"
}
// → reflect-metadata가 *부분만 동작*
// → @Column() 정보가 *런타임에 일부 누락*

대응:

  • Prisma — ESM 친화도 중간. 별도 처리 필요하지만 동작.
  • DrizzleESM이 기본. 처음부터 ESM 우선으로 설계.

3) Raw SQL이 즉시 필요한 OLAP/분석 도메인

문제: TypeORM의 추상 레이어가 OLAP 쿼리에는 방해가 된다.

// OLAP — 윈도우 함수, CTE, MATERIALIZED VIEW
const result = await dataSource.query(`
  WITH monthly_revenue AS (
    SELECT
      DATE_TRUNC('month', created_at) AS month,
      SUM(amount) AS revenue,
      LAG(SUM(amount)) OVER (ORDER BY DATE_TRUNC('month', created_at)) AS prev
    FROM orders
    GROUP BY 1
  )
  SELECT *, (revenue - prev) / prev * 100 AS growth_pct
  FROM monthly_revenue
`)
// result의 타입은? — any[]
// ← TypeORM의 *raw query는 any*
  • TypeORM이 제공하는 가치가 0인 영역. 단순히 DB 연결 객체로만 쓰임.

대응:

  • Drizzle — SQL DSL이 OLAP에 완전한 타입복잡한 쿼리를 동시에 제공.
  • Prisma 5+ TypedSQL — raw SQL에 타입을 부여.
  • Kysely — 순수 query builder, OLAP에 가장 적합한 경량 선택지.

4) 서버리스·엣지에서 cold start가 매출

문제: TypeORM의 DataSource.initializereflect-metadata 로드 + 엔티티 메타 빌드 + 연결 풀 생성동시에 한다.

// AWS Lambda — TypeORM cold start
// p99 latency: ~500ms (cold)
// p50 latency: ~30ms (warm)
// ← cold start 500ms는 *유저 체감 명백히 느림*
  • Vercel, Cloudflare Workers, Deno Deploy, AWS Lambda — 모두 cold start 손해가 크다.

대응:

  • Drizzle — cold start ~50-100ms. 거의 runtime 0 overhead.
  • Prisma + Driver Adapters (2024+) — Rust 엔진을 우회해 cold start 개선.

5) Decorator spec 변동성에 민감한 팀

문제: TC39 Decorator 제안서2014년 Stage 1에서 시작해 2026년에도 Stage 3 표류 중. TypeScript의 experimentalDecoratorsStage 3 새 spec서로 호환되지 않는다.

// TypeORM은 experimentalDecorators에 의존
// → TypeScript가 Stage 3 new decorators로 *완전 이행*하면
//   TypeORM은 *모든 코드 재작성*이 필요할 수 있음
// → 2025년 현재도 TypeORM은 *legacy decorator만* 지원
  • 5년 이상 운영할 코드베이스에서 언어 spec 변동성에 묶이는 것은 기술 부채.

대응:

  • Prisma — DSL이 TypeScript spec과 무관하다. 외부 영향 차단.
  • Drizzle데코레이터 자체를 안 쓴다. spec 변동성에서 자유.

What — 결정 흐름도

예/아니오 다섯 개 중 두 개 이상이 예면 TypeORM은 손해다.


What-if — 그래도 TypeORM을 고집하면

1) 결제 도메인에 TypeORM

select 누락 필드가 production에서 런타임 에러. Sentry 알람이 매일 쌓인다. → 6개월 후 Prisma로 강제 이주. 이주 비용 3개월.

2) Vercel 서버리스에 TypeORM

→ p99 latency 500ms 이상. Core Web Vitals 페널티. SEO 손해. → Drizzle 또는 Prisma Edge로 이주. 2주~1개월.

3) OLAP 분석 대시보드에 TypeORM

→ 거의 모든 쿼리가 raw query → TypeORM의 추상 가치 0. → Kysely 또는 Drizzle로 전환. 데이터 레이어가 얇아짐.

4) Decorator spec 변경 후

→ TypeScript 6.x에서 experimentalDecorators 제거 시 — TypeORM 코드 전면 재작성. → Prisma 같은 DSL 기반 도구가 이 위험에서 자유.

5) 일부 자리는 적합, 일부는 부적합인데 전부 TypeORM

→ Strangler Fig 패턴을 모르고 전부 TypeORM 유지. 부분 이주 기회 놓침. → 부분적으로 Drizzle/Prisma로 이주하는 Strangler Fig를 고려.


Insight — 흥미로운 이야기

”Strangler Fig — 부분 이주 패턴”

Martin Fowler가 2004년 정리한 Strangler Fig Pattern오래된 시스템을 통째로 교체하지 않고 점진적으로 새 시스템으로 옮기는 패턴이다. Strangler Fig기존 나무를 감싸고 자라다 결국 안의 나무가 죽고 새 나무가 그 자리를 차지하는 식물 — 비유의 출처.

ORM 이주에 적용하면:

  1. 기존 TypeORM 코드는 유지
  2. 새 모듈은 Prisma/Drizzle로 작성
  3. 공통 DB 위에서 두 ORM이 공존
  4. Repository 레이어를 인터페이스로 추상화교환 가능하게
  5. 오래된 TypeORM 코드를 점진적으로 새 ORM으로 옮김
// 인터페이스 — 도메인 코드는 *ORM을 모른다*
interface UserRepository {
  findById(id: number): Promise<User | null>
  save(user: User): Promise<void>
}
 
// 옛 구현 — TypeORM
class TypeOrmUserRepository implements UserRepository { ... }
 
// 새 구현 — Prisma
class PrismaUserRepository implements UserRepository { ... }
 
// 도메인 코드는 *어느 쪽이든 받는다*
class UserService {
  constructor(private userRepo: UserRepository) {}
  // ...
}

이게 현실의 ORM 이주 표준 패턴이다. 통째로 교체하지 않는다.

”Stripe는 왜 ORM을 안 쓰는가

Stripe는 결제 시스템에서 ORM을 사용하지 않는다. 직접 Ruby + 사내 query layer로 짠다. 이유:

  1. 결제 도메인은 type safety가 매출
  2. raw SQL이 즉시 필요
  3. *ORM의 추상이 방해

이게 극단적 사례지만 — 모든 자리에 ORM이 옳지는 않다증거다. 결제·핀테크의 진지한 회사들은 ORM 없이 짜는 경우가 많다.

”TypeORM의 공식 입장

TypeORM의 README에는 *“TypeORM works in NodeJS, Browser, Ionic, Cordova and Electron”*이라 적혀 있다. 그러나 공식 docs서버리스 환경에 대한 가이드거의 없다. 이게 신호다.

GitHub issues에서 *“AWS Lambda + TypeORM”*을 검색하면 cold start 문제반복적으로 보고된다. 권장 답변은 “connection을 외부 변수로 캐시하라” 같은 워크어라운드뿐. 근본 해결이 없다.


요약 + 다이어그램

TypeORM을 피해야 할 자리는 다섯이다. 강한 타입 정확도, ESM 우선, raw SQL OLAP, 서버리스 cold start, decorator spec 변동성. 둘 이상 해당하면 Prisma 또는 Drizzle이 거의 항상 더 낫다. 부분 이주는 Strangler Fig로 가능 — 통째로 바꿀 필요 없다.

다음 문서: 07-decision-matrix.mdx한 페이지 결정 매트릭스.