09-real-world-cases — 실전 사례
이 챕터가 답하는 질문: TypeORM은 실제로 어떤 프로젝트에서, 어떤 방식으로 쓰이고 있는가 — 그리고 왜 2023~2025년에 Prisma·Drizzle 이주가 흔해졌는가? 한 줄 답 (Pyramid Top): “NestJS에서 TypeORM은 사실상 기본 ORM이지만, 메인테이너 활동의 정체(2020~2022) · ESM 부재 · 데코레이터 spec 표류가 쌓이면서 — 같은 NestJS 안에서도 Prisma·Drizzle로 옮겨가는 팀이 빠르게 늘었다.”
한 문장 답
TypeORM의 운명은 NestJS와 묶여 있다.
@nestjs/typeorm이 forRoot/forFeature 패턴으로 NestJS DI에 자연스럽게 녹아들면서 TypeScript 백엔드의 사실상 표준이 되었고, 멀티테넌트 · soft delete · audit log 같은 실전 요구는 EntitySubscriber와 동적 DataSource로 풀어 왔다. 하지만 findOne() deprecate 사고(2021), ESM 지원 부재, 데코레이터 TC39 표류, issue 백로그 폭발이 누적되면서 Strapi v5는 Knex로, Medusa v2는 자체 query layer로, 수많은 NestJS 팀은 Prisma로 옮겨가고 있다.
챕터 지도 (Mermaid)
Why — 왜 사례를 한 챕터로 모으나
TypeORM 공식 문서·튜토리얼은 기능 단위로 쓰여 있다. “Entity는 이렇게, Repository는 이렇게, Migration은 이렇게.” 하지만 실제 production에서 TypeORM이 어디서 빛나고 어디서 부서지는지는 조합된 사례에서만 드러난다.
이 챕터는 6개 사례를 조립한다. 조립이 가르치는 것은 다음 셋이다.
- 사실상 표준의 형성 — 왜
@nestjs/typeorm이 TypeScript 백엔드의 기본이 되었는가 (01). - 실전 패턴 — 멀티테넌트·soft delete·audit·이벤트 훅 같은 모든 프로덕트가 결국 만나는 요구를 TypeORM이 어떻게 푸는가 (02~04).
- 이주의 구조적 원인 — 왜 Strapi·Medusa·수많은 팀이 Prisma·Drizzle로 옮겼는가, 그리고 남기로 한 팀은 무엇을 견뎌야 하는가 (05~06).
“다른 팀이 어떻게 쓰고 어떻게 떠나는지”는 우리 팀의 결정을 빠르게 만든다 — 같은 함정을 다시 밟지 않기 위해.
How — 어떻게 읽나
다음 6개 문서를 순서대로 읽으면 약 1시간 30분. 각 문서는 독립적이지만, 01 → 0204(실전 패턴) → 0506(이주의 동기) 순서로 흐른다.
| # | 파일 | 읽는 데 | 핵심 키워드 |
|---|---|---|---|
| 01 | 01-nestjs-integration | 14분 | @nestjs/typeorm · forRoot · forFeature · DI · 사실상 표준 |
| 02 | 02-multi-tenant-patterns | 15분 | schema-per-tenant · row-level · 동적 DataSource · CLS |
| 03 | 03-soft-delete-and-audit | 13분 | @DeleteDateColumn · softRemove · EntitySubscriber · audit log |
| 04 | 04-event-subscribers | 14분 | @EventSubscriber · subscribers 옵션 · 트랜잭션 경계 · async 함정 |
| 05 | 05-migration-from-typeorm-to-prisma | 16분 | Strapi v5 · Medusa v2 · 이주 단계 · 코드젠 vs 데코레이터 |
| 06 | 06-known-pain-points | 16분 | findOne() deprecate · ESM · TC39 데코레이터 · issue 백로그 |
의존성: 04는 03을 가정한다(같은 EntitySubscriber를 다른 각도로 본다). 05는 06과 동전의 양면이다 — 05는 “어디로 가는가”, 06은 “왜 떠나는가”.
What — 사례 비교 표 (한 페이지 요약)
| 주제 | 어디서 빛나나 | 어디서 부서지나 | 돌아갈 챕터 |
|---|---|---|---|
| NestJS 통합 | DI 컨테이너에 자연스럽게 녹는다 — @InjectRepository(User) 한 줄. | 동적 DataSource(멀티테넌트) 때는 DI와 충돌한다. | 03-repository-datamapper |
| 멀티테넌트 | 동적 DataSource로 런타임에 connection 선택 가능. | entityManager.transaction + 동적 DataSource는 async hooks가 없으면 깨진다. | 05-transaction-datasource |
| Soft Delete | @DeleteDateColumn 한 줄 + softRemove()로 선언적. | find()가 자동으로 deleted 제외 — 모르고 raw join하면 부활. | 01-entity-decorators |
| Audit Log | EntitySubscriber.afterInsert로 자동 기록. | 트랜잭션 롤백되면 subscriber도 롤백 — 외부 시스템 호출은 위험. | 04 |
| EventSubscriber | subscribers 옵션으로 글로벌 훅 — @BeforeInsert보다 강력. | async 함수의 await가 보장되지 않는 경우가 있었다 (issue #2074 류). | 04 |
| 이주(Prisma) | 정적 타입 정확도 · 마이그레이션 워크플로우 · 활발한 메인테이너. | TypeORM의 DataMapper · QueryBuilder · 데코레이터를 모두 버려야 한다. | 08-theory-and-alternatives |
| 알려진 문제 | (해당 없음) | findOne() deprecate(2021), ESM 부재, 데코레이터 TC39 표류. | 00-foundations |
표의 분기점은 두 개다 — (1) NestJS DI에 완전히 녹일 것인가, (2) 동적 DataSource·이벤트 훅 같은 경계 케이스에 의존할 것인가. 이 둘이 TypeORM의 운명을 가른다.
What-if — 이 챕터를 건너뛰면
- NestJS 통합 패턴을 모르면:
forRoot()만 알고forFeature()를 안 써서 Repository가 DI되지 않는다는 흔한 사고를 만난다. - 멀티테넌트 패턴을 모르면: schema-per-tenant를 한 DataSource에 동적 schema로 풀려다 connection pool이 폭발한다 (실제 NestJS 포럼·discord에 반복 등장하는 사고).
- EventSubscriber의 트랜잭션 경계를 모르면: audit log를 subscriber에서 외부 API로 쏘다가 롤백된 작업의 외부 흔적이 남는다 — 회복이 어려운 사이드이펙트.
- 이주 사례를 모르면: “다들 떠나니까 우리도 가자” 같은 비기술적 결정을 한다 — Strapi가 Knex로 간 이유와 우리 팀의 이유는 다를 수 있다.
- 알려진 문제를 모르면:
findOne(1)같은 deprecated API를 production에 넣고, runtime 에러로 새벽에 호출 받는다.
Insight — 한 단락 이야기
“2021년 4월,
findOne(1)이 죽은 날”2021년 봄, TypeORM 0.3.0 로드맵이 발표되면서 수많은 production 코드가 한 번에 deprecated 표시를 받았다.
repository.findOne(1)— 가장 흔한 호출 패턴이 primary key 직접 받기를 금지하고,findOne({ where: { id: 1 } })또는findOneBy({ id: 1 })로 바뀐다. 변경 자체는 합리적이었다 — 타입 안전을 강화하는 방향. 하지만 마이그레이션 가이드가 늦었고, 경고 메시지는 명확하지 않았고, issue가 수백 개로 쌓였다. 같은 시기에 NestJS 9가 Fastify와 RxJS 7로 가면서 호환성 매트릭스가 깨졌고, ESM이 점점 표준이 되어 가는데 TypeORM은 CJS만 지원했다. 그 누적의 결과가 2023년의 Prisma 이주 붐이다. TypeORM이 갑자기 나빠진 게 아니다 — TypeScript 생태계가 더 빨리 움직였고, TypeORM은 그 속도를 못 따라갔을 뿐이다. 이 챕터는 그 누적을 6개의 사례로 풀어 본다.
한 단락 요약
6개 사례는 TypeORM의 양손에 들린 trade-off를 보여준다. 한 손에는 NestJS DI에 자연스럽게 녹은 사실상 표준(01)과 멀티테넌트·soft delete·audit을 데코레이터로 풀어내는 우아한 실전 패턴(02
04)이 있다. 다른 손에는 findOne deprecate 사고 · ESM 부재 · 데코레이터 spec 표류 · 메인테이너 정체가 만든 Prisma·Drizzle 이주 흐름(0506)이 있다. TypeORM을 지금 도입할지·계속 쓸지·떠날지의 결정은 이 두 손 중 어느 쪽이 무거운가에 달려 있다 — 이 챕터는 그 저울의 눈금을 보여준다.
다음 문서:
01-nestjs-integration.mdx— 모든 사례의 출발점,@nestjs/typeorm이 어떻게 사실상 표준이 되었는가.