🔷 GraphQL3. N+1 & DataLoader📖 개요

03-n-plus-1-dataloader — N+1 & DataLoader

이 챕터가 답하는 질문: 왜 GraphQL은 N+1이 그렇게 쉽게 나는가? 그것을 표준적으로 어떻게 해결하나? 한 줄 답 (Pyramid Top): “N+1은 GraphQL의 고유 문제가 아니라 드러나기 쉬운 문제이고, DataLoader는 그것을 단일 event loop tick에서 batch + cache로 흡수한다.”


한 문장 답 (Pyramid Top)

GraphQL을 쓰다가 야간에 호출 받는 가장 흔한 이유는 DB가 죽는다이고, 그 99%는 N+1이다. 하지만 N+1은 GraphQL이 만든 문제가 아니다 — ORM에도, REST에도 똑같이 있다. 다만 GraphQL은 resolver가 필드마다 함수라는 구조 때문에 그 문제가 한눈에 드러난다. 사실상 표준 해결책은 DataLoader — 같은 event loop tick에서 들어온 key들을 모아서 한 번에 batch로 fetch하고, request-scope에서 caching까지 처리하는 작은 유틸리티다. 이 챕터는 N+1의 정의부터 시작해서, DataLoader의 동작 원리, event loop와의 관계, 그리고 대안들(JOIN 컴파일, Prisma, Hasura)까지를 한 층씩 벗긴다.


챕터 지도 (Mermaid)


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

GraphQL 도메인에서 가장 자주 사고를 내고, 가장 자주 잘못 진단되는 문제가 N+1이다. 세 가지 잘못된 직관이 거의 모든 야간 호출의 출처다.

잘못된 직관실제어디서 다루나
”N+1은 GraphQL 탓이다”ORM·REST에도 있는 오래된 문제다. GraphQL은 그것을 드러나게 했을 뿐.01
”DataLoader는 그냥 캐시다”DataLoader는 batch + dedupe + cache의 묶음이다. 캐시는 부산물에 가깝다.02, 03
”DataLoader가 어떻게 batch하는지는 마법이다”Node.js event loop tick + microtask queue의 평범한 조합이다.04
”DataLoader면 N+1은 다 해결된다”lookaheadJOIN 컴파일이 더 나은 자리도 있다. 둘은 함께 쓰는 경우가 많다.05, 06

이 챕터는 N+1을 진단·치료·예방의 세 층으로 나눠 다룬다. 진단은 01, 치료는 02~04, 예방·대안은 05~06.


How — 어떻게 읽나

다음 6개 문서를 순서대로 읽으면 약 70분이 걸린다. 각 문서는 독립적으로 읽혀도 되지만, 누적적이다.

#파일읽는 데핵심 키워드
0101-the-n-plus-1-problem.mdx10분1+N · ORM eager/lazy · REST waterfall · resolver tree
0202-dataloader-pattern.mdx14분batchFn · keys[] → values[] · invariant · Facebook 2015
0303-batch-and-cache.mdx12분per-request cache · dedupe · cacheKeyFn · security
0404-event-loop-and-tick.mdx12분microtask · process.nextTick · dispatch · scheduleFn
0505-lookahead-and-projection.mdx10분info.fieldNodes · graphql-parse-resolve-info · SELECT 사전 결정
0606-alternatives-join-monger-prisma.mdx12분join-monkey · Prisma · Hasura · PostGraphile · SQL 컴파일

의존성: 02는 01을, 0304는 02를, 0506은 01~04를 가정한다.


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

문서한 줄 결론
01N+1은 부모 1번 + 자식 N번 패턴이며, GraphQL은 resolver-per-field 구조 탓에 눈에 띄게 만든다.
02DataLoader는 batchFn(keys[]) → Promise<values[]> 한 시그니처와 순서 invariant 위에 서 있는 작은 라이브러리다.
03DataLoader의 cache는 전역이 아니라 per-request — 그래서 보안 누출이 없고, 신선도가 자동으로 유지된다.
04DataLoader의 batch는 마법이 아니다 — 같은 event loop tick에 쌓인 .load() 호출을 microtask queue에서 한 번에 dispatch할 뿐이다.
05Lookahead는 info.fieldNodes를 미리 보고 SELECT를 좁히는 접근 — DataLoader가 cache라면 lookahead는 JOIN이다.
06join-monger·Prisma·Hasura는 GraphQL을 SQL로 직접 컴파일하는 접근이다 — DataLoader와 대립이 아니라 층위가 다르다.

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

  • 01(정의)만 알고 02(라이브러리)를 모르면: 매 resolver마다 손으로 batching을 짜다 결국 코드가 더 망가진다.
  • 02(DataLoader)만 알고 03(scope)를 모르면: DataLoader 인스턴스를 서버 전역으로 만들고 — 다른 사용자의 데이터가 캐시 히트로 새어 나간다.
  • 03(cache)만 알고 04(tick)를 모르면: await를 잘못 끼워 batch가 분리되는데 왜 그런지 모른다.
  • 04(tick)까지 알고 05(lookahead)를 모르면: DataLoader로도 못 잡는 깊은 트리의 round-trip 폭증에서 다시 당한다.
  • 05까지 알고 06(대안)을 모르면: Hasura/Prisma가 처음부터 해결한 영역에서 재발명하느라 시간을 태운다.

Insight — 한 단락 이야기

“DataLoader는 N+1을 푸는 도구가 아니라, N+1이 드러나도 괜찮게 만드는 도구다”

2015년 Facebook의 Lee Byron이 DataLoader: A modern batch-loading utility를 공개했을 때, 그가 강조한 것은 “이건 GraphQL 라이브러리가 아니다” 였다. DataLoader는 언어/프레임워크 무관한 batch + cache 유틸리티고, REST 클라이언트에도, gRPC client에도 똑같이 쓸 수 있다. 다만 GraphQL의 resolver-per-field 구조가 N+1을 극단적으로 드러냈을 뿐 — DataLoader는 그 구조를 바꾸지 않은 채, 같은 tick에 모인 요청들한 번의 batch로 흡수한다. 추상화의 묘수는 “문제를 없애는 것”이 아니라 “문제가 생겨도 비용이 안 드는 자리에서 받아내는 것” — 이 챕터가 하는 일은 그 흡수의 메커니즘을 6층으로 분해하는 것.


Mermaid 4색 규약


한 단락 요약

N+1은 GraphQL이 만든 문제가 아니라 GraphQL이 드러나게 한 오래된 문제다(01). 사실상 표준 해결책은 DataLoader — batchFn(keys[]) → values[] 한 시그니처와(02) request-scope cache(03)와 event loop tick의 micro-batching(04)을 묶은 작은 유틸리티다. 그 너머에는 lookahead(05)와 SQL 컴파일 접근(06)이 있고, 셋은 경쟁이 아니라 층위다. 이 챕터를 끝내면 “왜 DB가 죽지?” 라는 질문 대신 “이 resolver는 어느 tick에 있고, 어느 batch를 만들 수 있나?” 라는 질문을 던지게 된다. 다음 챕터(04-transport)는 이렇게 만들어진 응답을 어떻게 네트워크에 실어 보낼지를 다룬다.