🗄️ TypeORM8. 이론 & 대안 (Prisma · Drizzle · MikroORM)01 — ORM이라는 패턴 (왜 ORM이 존재하는가)

01 — ORM이라는 패턴 (왜 ORM이 존재하는가)

한 줄 답: ORM은 도구가 아니라 패턴이다 — 객체와 관계형의 impedance mismatch를 메우려는 시도의 한 가족이고, Martin Fowler가 2002년 Patterns of Enterprise Application Architecture(PEAA)에서 정리한 DataMapper · ActiveRecord · Unit of Work · Identity Map 네 패턴이 그 핵심이다. TypeORM은 DataMapper의 느슨한 구현이며, Unit of Work는 구현하지 않는다.


Why — 왜 ORM이라는 것이 생겨났나

1) Impedance Mismatch — 객체와 관계형의 근본적 어긋남

“Object-Relational Impedance Mismatch”라는 표현은 1995년 Scott Ambler가 처음 정리했고, 2006년 Ted Neward는 이를 *“the Vietnam of Computer Science”*라 불렀다 — 이긴 적 없는 전쟁이라는 의미.

객체 모델관계형 모델충돌
클래스 상속테이블 (상속 없음)STI/CTI/MTI 우회 필요
객체 참조 (포인터)외래키 (값)어떻게 dereference?
컬렉션 (List, Set)행 집합 (Result Set)지연 로딩? eager?
캡슐화된 식별성 (객체 동일성)기본키 (값 동일성)같은 row를 두 번 fetch하면 두 객체?
트랜잭션 (없음)트랜잭션 (필수)어디서 커밋?
nullable·undefined·옵션 타입NULL (3-value logic)NULL 한 줄로 표현 가능?

이 다섯 가지 어긋남을 전부 해결하는 ORM은 없다. 다 일부만 해결하고 나머지는 사용자에게 떠넘긴다. TypeORM이 어디를 떠넘기는지가 이 챕터의 핵심.

2) Martin Fowler의 PEAA — 패턴 카탈로그

2002년 출판된 Patterns of Enterprise Application Architecture는 ORM 패턴의 공식 사전이다. 네 개의 핵심 패턴.


How — 네 패턴이 어떻게 다른가

1) ActiveRecord — 객체가 자기 save를 알고 있다

// Sequelize · Rails — ActiveRecord
const user = await User.findByPk(1)
user.email = 'new@example.com'
await user.save()   // ← 객체가 *자기* save를 호출
  • 장점: 직관적, 짧음, 작은 앱에 빠름
  • 단점: 도메인 객체가 영속성을 알고 있다 — 테스트 어려움, 큰 코드베이스에서 결합 폭발

2) DataMapper — 객체는 영속성을 모른다

// TypeORM · MikroORM · Hibernate — DataMapper
const user = await userRepository.findOne({ where: { id: 1 } })
user.email = 'new@example.com'
await userRepository.save(user)   // ← *외부 게이트웨이*가 save
  • 장점: 도메인 객체가 POJO에 가까움 — 단위 테스트 쉬움
  • 단점: 코드가 길어짐, Repository 추상이 하나 더

3) Unit of Work — 변경을 모아 한 번에 커밋

// MikroORM — *진짜* Unit of Work
const user = await em.findOne(User, 1)
const order = await em.findOne(Order, 100)
 
user.email = 'new@example.com'
order.status = 'paid'
 
await em.flush()
// ← em.flush() 한 번이 *user.update + order.update*를 한 트랜잭션에 묶음
//   사이에 save()를 명시적으로 호출하지 않았음
  • 핵심: 변경된 객체를 자동 추적하고, flush 시점에 한 번에 SQL로 변환
  • TypeORM은 이게 없다: repository.save(user)명시적으로 호출해야 한다

4) Identity Map — 같은 row는 같은 객체

// MikroORM — Identity Map
const a = await em.findOne(User, 1)
const b = await em.findOne(User, 1)
console.log(a === b)   // ← true (같은 객체)
  • TypeORM에서는 기본적으로 false — 두 번 fetch하면 두 객체. EntityManager가 동일성 보장을 안 한다.

What — TypeORM은 이 패턴들의 어디에 있나

TypeORM의 패턴 좌표

패턴TypeORM 구현정도
ActiveRecordBaseEntity로 옵션 제공옵션 (권장 안 함)
DataMapperRepository<Entity>기본 (권장)
Unit of Work없음save/remove를 명시적으로구현 안 함
Identity Map부분relations 옵션 안에서만부분
Lazy LoadingPromise<T> 타입 컬럼으로옵션
Eager Loadingrelations: ['x'] 또는 eager: true기본
Migrationmigration:generate (스키마 diff)기본

**TypeORM은 “DataMapper처럼 쓰되 Unit of Work는 사용자가 직접 짜라”의 태도다 — Hibernate의 완전한 Unit of Work나 MikroORM의 진짜 UoW와는 다르다.

왜 TypeORM은 Unit of Work를 안 만들었나

세 가지 이유.

  1. TypeScript의 변경 추적이 어렵다: Hibernate/JPA는 Java의 바이트코드 enhancement로 setter를 가로챈다. TypeScript에는 그런 메커니즘이 런타임에 존재하지 않는다 — Proxy 또는 reflect-metadata로 부분 모방만 가능.
  2. 명시성이 TypeScript의 가치관과 맞는다: save()를 명시적으로 호출하는 것이 *“마법이 없는 코드”*에 가깝다. 명시적 React, 명시적 useEffect — 같은 가치관.
  3. 초기 구현 비용이 높았다: Umed Khudoiberdiev가 2016년 1인 개발로 시작했을 때 Unit of Work는 후순위였고, 이후 v0.3 (2022) 리팩토링 때도 우선순위가 아니었다.

결과적으로 TypeORM은 *“DataMapper의 느슨한 구현”*이다 — 패턴의 *이름은 빌렸지만 *전형은 대부분 사용자에게 떠넘긴다.


What-if — 이 패턴 분류를 모르면

1) ActiveRecord 스타일로 TypeORM을 쓴다

// 안티 패턴 — DataMapper인데 ActiveRecord처럼
class User extends BaseEntity {
  // ...
}
const user = await User.findOne(...)
await user.save()    // ← 가능하지만 *권장 안 됨*

→ 도메인 객체가 영속성에 결합된다. 단위 테스트가 DB 모킹을 요구한다. 큰 코드베이스에서 책임 경계가 무너진다.

2) Unit of Work가 있다고 착각한다

const user = await repo.findOne({ where: { id: 1 } })
user.email = 'new@example.com'
// ← save() 호출 안 함
// ← 트랜잭션 끝나도 *DB에 반영 안 됨*

→ “값을 바꿨는데 왜 DB에 안 들어가지?” — TypeORM 입문자의 전형적 함정.

3) Identity Map이 있다고 착각한다

const a = await repo.findOne({ where: { id: 1 } })
const b = await repo.findOne({ where: { id: 1 } })
a.email = 'x@x.com'
await repo.save(a)
console.log(b.email)   // ← 옛 값 (a와 다른 객체)

→ 두 객체가 별개다. 한 쪽 변경이 다른 쪽에 반영 안 됨. MikroORM이라면 a === b라 자동 반영.


Insight — 흥미로운 이야기

”Hibernate의 그림자”

Hibernate (2001, Java)는 ORM의 고전이고 Patterns of Enterprise Application Architecture가 사실상 그것을 정리한 책이다. Gavin King이 만든 Hibernate는 Unit of WorkIdentity Map바이트코드 enhancement로 자동화했다 — Java의 부분 dynamic이 가능했기 때문.

TypeORM은 처음 *“TypeScript의 Hibernate”*를 표방했지만 — TypeScript에 바이트코드 enhancement가 없어서 반쪽에서 멈췄다. 이게 TypeORM의 근본적 한계고, MikroORM이 그 반쪽을 마저 짠 ORM이다.

”Active Record의 Rails 유산”

David Heinemeier Hansson이 2004년 Rails에 Active Record를 도입했을 때 *“객체가 자기 save를 알아야 한다”*는 대담한 선택이었다. 1995년 Scott Ambler의 글에서는 DataMapper가 더 옳다는 입장이었다.

Rails는 프로토타이핑 속도를 우선했고, 큰 코드베이스에서의 결합나중 문제로 미뤘다. Sequelize는 이 패턴을 JavaScript로 그대로 들고 왔고 — 큰 TypeScript 코드베이스에서 결합 폭발을 그대로 물려받았다. TypeORM이 DataMapper를 기본으로 채택한 이유.

”Decorator Spec의 9년 표류”

TC39의 Decorator 제안서는 2014년 Stage 1에서 시작했다. 2026년 현재 Stage 3까지 왔지만 — *TypeScript의 experimentalDecorators*와 Stage 3의 새 spec서로 호환되지 않는다. TypeORM은 옛 spec에 모든 것을 걸었고, 이게 향후 마이그레이션 부담의 출처다.

tc39/proposal-decorators 저장소를 보면 Decorator Metadata가 별도 Stage 3로 갈라져 있다 — TypeORM이 의존하는 reflect-metadata이 둘 중 어느 쪽도 아닌 옛 polyfill이다.


요약 + 다이어그램

ORM은 도구가 아니라 패턴이다. 객체-관계 impedance mismatch를 메우는 시도의 한 가족이고 — Martin Fowler의 DataMapper · ActiveRecord · Unit of Work · Identity Map 네 패턴이 그 핵심. TypeORM은 DataMapper의 느슨한 구현이고 — Unit of Work는 구현하지 않으며 Identity Map은 부분이다. 이 반쪽이 매력의 출처이자 Prisma·MikroORM·Drizzle이 노리는 빈자리다.

다음 문서: 02-typeorm-vs-prisma.mdx — 그럼 가장 강력한 경쟁자 Prisma는 어떤 길을 갔나?