알려진 문제들
이 문서가 답하는 질문: TypeORM을 운영하는 팀이 공통적으로 부딪히는 통증은 무엇이고, 각각의 원인은 코드인가 생태계인가 spec인가? 한 줄 답 (Pyramid Top): “통증의 출처는 세 층이다 — 코드(findOne deprecated, save가 upsert), 생태계(ESM 미지원, NestJS 강결합), 외부 spec(데코레이터 9년째 stage 2~3). 코드 통증은 패치 가능하지만, 아래 두 층은 사용자가 풀 수 없다.”
한 문장 답 (Pyramid Top)
통증을 카탈로그화해서 분류하면 의사결정이 쉬워진다 — 팀이 풀 수 있는 통증인가, TypeORM 메인테이너만 풀 수 있는 통증인가, TC39 전체가 움직여야 풀리는 통증인가. 후자 두 개에 묶여 있다면 이주가 합리적이고, 첫 번째라면 패치로 살 수 있다.
챕터 지도
Why — 왜 카탈로그가 필요한가
운영 중인 팀의 오해는 보통 이렇게 생긴다 — “TypeORM이 느려서 옮긴다”, “TypeORM이 별로라서 옮긴다”. 정확하지 않다. 실제 동기는 구체적인 통증이고, 그 통증마다 해결 가능성과 해결자가 누구인가가 다르다. 카탈로그를 만들면 내가 풀 수 있는 통증과 못 푸는 통증이 분리되고, 후자가 임계점을 넘었을 때만 이주가 합리적인 선택이 된다.
How — 어떻게 분류하나
세 층으로 본다.
| 층 | 해결자 | 사용자가 할 수 있는 일 |
|---|---|---|
| L1 · 코드 | 팀 자신 | wrapper, 컨벤션, 린트 룰 |
| L2 · 생태계 | TypeORM 메인테이너 | issue 보이스, PR 기여, fork |
| L3 · spec | TC39 | 사실상 불가능 — 대안 ORM 사용 |
What — 통증 카탈로그
L1 — 코드 통증 (사용자가 패치 가능)
| 통증 | 증상 | 우회 |
|---|---|---|
findOne() deprecated 사고 | 0.2 → 0.3 전환에서 findOne(id) 시그니처가 조용히 바뀌어 — 1번 레코드 대신 첫 레코드를 반환. 일부 팀은 프로덕션에서 발견. | findOneBy({ id }) 명시, 0.3 migration guide 준수, findOne(id) 사용 금지 ESLint 룰 |
save()가 upsert | save()는 항상 SELECT 선행 → 알 수 없는 추가 쿼리. 대량 insert에서 수십 배 느림. | insert() / bulk insert 사용, save는 단건 + 동기화 자리만 |
| 타입 정확도 부분적 | find({ select })로 일부 필드만 골라도 반환 타입은 전체 엔티티 — 컴파일러가 없는 필드 접근을 잡지 못함. | DTO 매핑 강제, select 결과는 Partial<T>로 캐스팅 |
@nestjs/typeorm request-scoped 누수 | request-scoped provider에서 transaction을 안전하게 묶기 어려움 — 잘못된 패턴이 공식 docs에도 등장한 적 있음. | QueryRunner를 명시적으로 주입, transaction은 service에서만 시작 |
L2 — 생태계 통증 (메인테이너만 해결 가능)
| 통증 | 증상 | 우회 (가능하다면) |
|---|---|---|
| ESM 미지원 | TypeORM은 CJS 기반. "type": "module" 환경에서 데코레이터 메타데이터가 깨지는 케이스 다수. | typeorm-ts-node-commonjs runner, dual package, 또는 패키지 전체를 CJS로 |
| 메인테이너 활동 정체 | 2020~2022년 18개월 동안 큰 PR 머지 없음. 핵심 이슈가 수년 단위로 열려 있음. | 자신의 fork에서 패치 (실제로 일부 팀이 함), 또는 다른 ORM |
| NestJS와의 강결합 | docs/예제가 NestJS 위주 — 다른 프레임워크에서 best practice 찾기 어려움. | Express/Fastify 직접 통합 예제는 커뮤니티 블로그에 산재 |
| 마이그레이션 generate 누락 | 인덱스 변경, enum 변경, 일부 제약 변경이 generate에 잡히지 않음. | generate 결과를 항상 사람이 검토, migration:show 사용 |
L3 — spec 통증 (TC39 / 외부)
| 통증 | 증상 | 우회 |
|---|---|---|
| 데코레이터 spec 정체 | TC39 데코레이터 제안은 2014년 시작 → 12년째 stage 2~3. TypeScript가 2023년 새 stage 3 데코레이터를 도입했지만 TypeORM은 *legacy experimentalDecorators*에 묶여 있음. | tsconfig에 experimentalDecorators: true, emitDecoratorMetadata: true 강제. |
reflect-metadata polyfill | 표준 진입이 불확실 — 모든 진입점에서 import "reflect-metadata"를 한 번 호출해야 함. | 부트스트랩 첫 줄에 import, 빌드 도구에서 자동 prepend |
emitDecoratorMetadata 비용 | 모든 클래스에 type metadata 객체를 emit — 번들 크기/컴파일 시간 증가. | 새 데코레이터 spec이 정착될 때까지 감수하거나, 다른 ORM |
What-if — 통증을 분류하지 않으면
- L1을 L2로 오해하면: 옮기지 않아도 될 통증에 6~9개월을 쓴다 —
findOne사고는 ESLint 룰 한 줄로 막을 수 있는 통증이다. - L2를 L1으로 오해하면: 영원히 못 푸는 통증을 영원히 패치하려 한다 — ESM 지원을 사용자가 풀 수는 없다.
- L3를 모르면: 모든 데코레이터 ORM이 같은 위험을 공유한다는 사실을 모른다 — Sequelize-typescript, MikroORM도 같은 spec 위에 서 있다.
Insight — 이슈 트래커의 패턴
“미해결 이슈의 모양이 ORM의 건강을 보여준다”
2024~2025년 시점 TypeORM GitHub의 이슈 트래커는 3,000개 이상 열려 있고, 수년 단위로 머무는 이슈가 다수 — 그 중 일부는 재현 가능한 데이터 손실 버그다. PR도 수백 개가 review 대기. 이 모양은 두 가지를 동시에 의미한다 — 광범위한 사용자 기반(이슈가 그만큼 발생)과 메인테이너 capacity 부족.
비교 대상: Prisma의 미해결 이슈도 비슷한 수지만 평균 응답 시간과 주간 릴리스가 다르다. 결국 코드 품질의 절대값이 아니라 움직이는 속도가 신뢰의 기준이 된다.
한 가지 더 흥미로운 사실: TypeORM의 가장 큰 기여자는 NestJS 코어 팀이 아니다 — NestJS는 TypeORM을 사용하지만 유지보수에 직접 참여하지 않는다. 두 프로젝트의 운명은 기술적으로는 결합되어 있지만 조직적으로는 분리되어 있다 — 이 사실이 NestJS 사용자의 이주 의사결정을 복잡하게 만든다.
한 단락 요약
TypeORM의 통증은 *코드(팀이 패치) · 생태계(메인테이너) · spec(TC39)*의 세 층에 분포한다. L1만으로 이주를 결심하면 과잉 반응이고, L2~L3가 임계점을 넘으면 이주가 합리적이다. 분류 자체가 의사결정을 명료하게 만든다. 이 챕터를 끝내면 “TypeORM은 별로다”라는 모호한 판단 대신 “우리 통증은 L1인가 L2~L3인가, 풀 사람이 누구인가” 라는 작동 가능한 질문을 던지게 된다.