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 친화도 중간. 별도 처리 필요하지만 동작.
- Drizzle — ESM이 기본. 처음부터 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.initialize는 reflect-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의 experimentalDecorators와 Stage 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 이주에 적용하면:
- 기존 TypeORM 코드는 유지
- 새 모듈은 Prisma/Drizzle로 작성
- 공통 DB 위에서 두 ORM이 공존
- Repository 레이어를 인터페이스로 추상화해 교환 가능하게
- 오래된 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로 짠다. 이유:
- 결제 도메인은 type safety가 매출
- raw SQL이 즉시 필요
- *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— 한 페이지 결정 매트릭스.