🗄️ TypeORM7. 성능 & N+1📖 개요

07-performance-n-plus-1 — 성능 & N+1

이 챕터가 답하는 질문: TypeORM은 왜 N+1이 기본값인가? 그리고 어디서 어떻게 명시적으로 끄는가? 한 줄 답 (Pyramid Top): “TypeORM에서 N+1은 버그가 아니라 기본 동작이다 — relations 옵션, leftJoinAndSelect, loadRelationCountAndMap, 쿼리 캐시 중 하나를 의식적으로 꽂아야만 사라진다.”


한 문장 답 (Pyramid Top)

TypeORM은 데코레이터로 메타데이터를 박아 SQL을 늦게 만드는 DataMapper ORM이다 — 그 늦음의 대가가 N+1이라는 기본값이다. user.posts처럼 관계를 그냥 접근하면 SQL이 N번 나간다. 그것을 막는 길은 네 갈래다 — relations 옵션으로 join 한 방을 강제하거나, QueryBuilderselect column을 명시하거나, cache: true반복 호출을 흡수하거나, loadRelationCountAndMap처럼 count 전용 IN 배칭을 쓰는 것. 이 챕터는 그 네 갈래를 발사되는 SQL과 함께 한 층씩 본다.


챕터 지도 (Mermaid)


Why — 왜 이 챕터를 별도로 빼는가

세 가지 잘못된 직관이 거의 모든 TypeORM 성능 사고의 출처다.

잘못된 직관실제어디서 다루나
”TypeORM이 알아서 join해줄 거다”아니다. relations 옵션을 명시적으로 안 주면 lazy로 N+1이 난다.01, 02
eager: true만 켜면 끝이다”eager는 모든 곳에서 항상 join이 붙어 원치 않는 곳까지 비용을 만든다.01
leftJoinAndSelect는 안전한 한 방이다”deep relation에서는 행 수가 곱셈된다 — 카르테시안 폭발.03
”캐시만 깔면 성능은 해결”캐시는 무효화 전략 없이는 폭탄이다.04
”성능 문제는 코드 본 다음에 안다”아니다. 로깅을 먼저 켜야 어디서 무엇이 발사되는지 보인다.06

이 챕터는 N+1의 진단 → 치료 → 예방 → 가시화 네 층으로 나눠 다룬다.


How — 어떻게 읽나

#파일읽는 데핵심 키워드
0101-n-plus-1-pattern.mdx12분eager/lazy 기본값 · find().posts · resolver 없이도 N+1
0202-relations-option.mdx12분relations 옵션 · LEFT JOIN 한 방 · selection 최소화
0303-query-builder-join-and-select.mdx14분leftJoinAndSelect · 카르테시안 곱 · 엔티티 트리 매핑
0404-query-cache.mdx10분cache: true · Redis · clearCache · invalidate
0505-batching-and-loadRelationCountAndMap.mdx10분loadRelationCountAndMap · WHERE id IN (…) · count-only
0606-logging-and-profiling.mdx10분logging: [‘query’, ‘slow’, ‘error’] · maxQueryExecutionTime · EXPLAIN

의존성: 02~05는 01의 정의를 가정한다. 06은 어디서든 먼저 켜는 것이 좋다.


What — 한 페이지 요약 (모든 문서의 핵심 한 줄)

문서한 줄 결론
01TypeORM에서 N+1은 기본값이다 — relations/eager/join명시하지 않으면 lazy 접근마다 SQL이 새로 나간다.
02find 호출에 relations 옵션을 주면 내부적으로 LEFT JOIN 한 방으로 푼다 — 단, 원치 않는 컬럼까지 다 가져온다.
03leftJoinAndSelect행 수가 곱셈되는 카르테시안 곱을 만든다 — users(100) × posts(50)이면 5000행을 메모리에 적재한다.
04cache: true같은 쿼리에 대한 결과를 보관한다 — Redis 통합과 invalidate 키가 없으면 stale 데이터의 출처가 된다.
05loadRelationCountAndMapcount 전용 IN 배칭이다 — 자식 내용이 필요 없는 자리에서 가장 싼 길이다.
06logging: ['query', 'slow', 'error']maxQueryExecutionTime을 켜는 그 한 줄모든 디버깅의 출발점이다.

What-if — 이 챕터를 건너뛰면

  • 01(정의)만 알고 02(relations)를 모르면: lazy 접근을 손으로 await 루프에 욱여넣어 N+1을 그대로 재현한다.
  • 02(relations)만 알고 03(QueryBuilder)을 모르면: 4-depth deep relation을 한 번에 부르고 메모리 폭발로 OOM을 만난다.
  • 03까지 알고 04(cache)를 모르면: 같은 쿼리를 분당 1000번 발사하면서 DB CPU 100%의 출처를 못 찾는다.
  • 04까지 알고 05(count map)를 모르면: 댓글 수 하나 보여주려고 모든 댓글 본문까지 가져온다.
  • 05까지 알고 06(logging)을 모르면: 어디서 무엇이 발사되는지 추측만 하다 엉뚱한 곳을 최적화한다.

Insight — 한 단락 이야기

“TypeORM의 N+1은 ORM 30년 역사가 만든 기본값이다”

Hibernate가 2003년 fetch="lazy"디폴트로 정한 그 순간부터 — N+1은 모든 ORM의 운명이 되었다. TypeORM은 그 전통을 그대로 계승한다. 데코레이터로 @OneToMany를 박는 순간, 그 관계는 접근될 때까지 SQL을 미루는 약속이 된다. 이것은 버그가 아니라 OOP가 SQL을 다루는 대가다. 2015년 Facebook이 DataLoader로 GraphQL의 N+1을 한 tick 안에서 batch로 흡수했듯, TypeORM은 relations/leftJoinAndSelect/loadRelationCountAndMap이라는 세 가지 명시 도구로 그것을 다스린다 — 단, 셋 모두 개발자가 의식적으로 호출해야 한다. TypeORM은 N+1을 없애지 않는다 — 그것을 명시적으로 끄는 도구를 제공할 뿐이다. 이 챕터가 하는 일은 그 명시 도구들이 내부적으로 어떤 SQL을 발사하는지를 6층으로 분해하는 것.


Mermaid 4색 규약


한 단락 요약

TypeORM에서 N+1은 기본값이다(01). 그것을 끄는 길은 네 갈래 — relations 옵션의 암묵적 join(02), leftJoinAndSelect명시적 join + 카르테시안 위험(03), cache반복 호출 흡수(04), loadRelationCountAndMapcount-only 배칭(05). 그 모든 것의 출발점은 logging: ['query', 'slow'] 한 줄이다(06). 이 챕터를 끝내면 “왜 DB가 느리지?” 라는 질문 대신 “내 find는 어떤 join을 발사하고, 어떤 카르테시안을 만드는가?” 라는 질문을 던지게 된다. 다음 챕터(08-theory-and-alternatives)는 Prisma와 Drizzle이 같은 문제를 어떻게 다른 추상으로 푸는지를 본다.