🔷 GraphQL3. N+1 & DataLoader06-alternatives-join-monger-prisma

06 · Alternatives — join-monger · Prisma · Hasura

이 문서가 답하는 질문: DataLoader와 lookahead 말고도 — GraphQL을 처음부터 SQL로 컴파일하는 접근이 있다. 어떤 것들이 있고, 언제 더 나은가? 한 줄 답: “join-monger·Prisma·Hasura·PostGraphile은 GraphQL 쿼리 전체를 한 줄의 SQL로 번역하는 접근이다 — DataLoader가 ‘실행 모델 안에서 batch’라면, 이들은 ‘실행 모델을 SQL로 대체’한다.”


Why — batch/lookahead 위의 한 층을 봐야 하는 이유

이전 5개 문서를 따라오면 — N+1을 다루는 두 가지 핵심 도구 (DataLoader, lookahead)에 익숙해진다. 그러나 어떤 자리에서는 이 둘로도 부족하거나 과도하다.

  • 관계가 복잡한 join 패턴(many-to-many through table, polymorphic, self-recursive)을 만나면 — lookahead 코드가 수백 줄이 된다.
  • 모든 GraphQL 쿼리에 동일한 패턴이 반복된다면 — 손으로 짤 가치가 있나? 라는 질문이 나온다.
  • resolver를 짤 시간 자체가 없다면 — 스키마만 보고 자동으로 GraphQL API를 만드는 도구가 있다.

이 자리에 GraphQL → SQL 컴파일 접근이 있다. 대표 4개를 본다.

위로 갈수록 자유도가 높고 손이 많이, 아래로 갈수록 자동화가 강하고 자유도가 낮다.


How — 네 가지 접근

1) join-monger / join-monkey — resolver 안에서 SQL builder를 호출

join-monster는 GraphQL → SQL 컴파일러를 기존 resolver 안에서 호출하는 라이브러리다.

import joinMonster from "join-monster";
 
const User = new GraphQLObjectType({
  name: "User",
  sqlTable: "users",
  uniqueKey: "id",
  fields: () => ({
    id: { type: GraphQLID },
    posts: {
      type: new GraphQLList(Post),
      sqlJoin: (userTable, postTable) =>
        `${userTable}.id = ${postTable}.user_id`,
    },
  }),
});
 
const resolvers = {
  Query: {
    users: (parent, args, ctx, info) => {
      return joinMonster(info, ctx, (sql) => db.query(sql), {
        dialect: "pg",
      });
      // ↑ info를 보고 SQL JOIN 생성 → 한 번에 실행
    },
  },
};
  • 스키마 정의에 *sqlTable, sqlJoin, uniqueKey*같은 메타데이터를 박는다.
  • resolver는 한 줄joinMonster(info, ...).
  • 한 GraphQL 쿼리 → 한 SQL JOIN.

장점: 기존 GraphQL 코드와 공존. 일부 타입만 join-monger로, 나머지는 손으로. 단점: 메타데이터를 스키마에 박는 결합. 복잡한 권한·필터·정렬은 코드로 빠짐.

2) Prisma + GraphQL 통합 (Pothos·Nexus)

Prisma는 자체로는 GraphQL 도구가 아니다 — TypeScript ORM이다. 다만 GraphQL과 궁합이 좋아서 @pothos/plugin-prisma, nexus-prisma 같은 통합 플러그인이 사실상 표준 조합이 됐다.

// Pothos + Prisma
const builder = new SchemaBuilder({
  plugins: [PrismaPlugin],
  prisma: { client: prisma },
});
 
builder.prismaObject("User", {
  fields: (t) => ({
    id: t.exposeID("id"),
    posts: t.relation("posts"),
  }),
});
 
builder.queryType({
  fields: (t) => ({
    users: t.prismaField({
      type: ["User"],
      resolve: (query, _, args) => prisma.user.findMany({ ...query }),
      //                                                   ↑ 이 query는 info에서 자동 추출된 include/select
    }),
  }),
});
  • info 분석은 플러그인이 자동. 개발자가 info를 안 본다.
  • 한 쿼리에 필요한 컬럼·관계만 SELECT/JOIN. 깊이 4단도 한 SQL.
  • Prisma의 type 안전성까지 그대로.

장점: 코드가 짧고 타입세이프. 학습 비용 낮음. 대부분의 일상 케이스 잡힘. 단점: Prisma에 묶임. 극단적 SQL 튜닝에는 한계 (raw SQL fallback 필요).

3) Hasura — DB를 보면 GraphQL API가 자동 생성됨

Hasura완전히 다른 패러다임이다. resolver를 짜지 않는다 — Postgres / MySQL / MS SQL의 스키마 자체를 읽어 GraphQL API를 자동 생성한다.

# 별도 resolver 코드 없이 — Hasura가 자동 노출
query {
  users(where: { email: { _ilike: "%@example.com" } }, limit: 10) {
    id
    name
    posts(order_by: { created_at: desc }) {
      title
      comments_aggregate {
        aggregate { count }
      }
    }
  }
}
  • Postgres에 FK가 있으면 → GraphQL posts 필드가 자동 생성.
  • 모든 쿼리는 한 줄의 SQL로 컴파일 → DB에 한 번 round-trip.
  • 권한은 행 단위(RLS) Postgres policy로 — Hasura의 Permission DSL이 SQL로 변환.

Hasura의 왜 빠른가 비밀: (Why we built Hasura) — GraphQL → AST → SQL JOIN 한 번 → JSON 직렬화까지 서버 메모리 zero copy. resolver 함수 호출이 없으므로 DataLoader도 필요 없다.

장점: 코드 zero. 대부분의 CRUD 케이스에서 손으로 짠 것보다 빠름. 단점: 복잡한 비즈니스 로직Action이나 Remote Schema밖으로 빼야 함. DB-first 사고가 강제됨.

4) PostGraphile — Hasura의 자유도 높은 사촌

PostGraphilePostgres → GraphQL 자동 생성. Hasura와 비슷하지만 Node.js 플러그인 시스템이 강해서 코드로 확장하기 쉽다.

import express from "express";
import { postgraphile } from "postgraphile";
 
const app = express();
app.use(
  postgraphile(DATABASE_URL, "public", {
    graphiql: true,
    enhanceGraphiql: true,
    appendPlugins: [/* custom plugins */],
  }),
);
  • 자동 GraphQL API + plugin 시스템.
  • 전체가 Postgres 기반 — Hasura가 다중 DB를 지원하는 반면, PostGraphile은 Postgres에 특화.
  • Smart Comments로 DB 코멘트에 GraphQL 메타데이터를 박는 등 DB-first가 더 강함.

What — 결정 매트릭스 (한 페이지)

요구사항DataLoaderLookahead (Prisma)join-monsterHasura / PostGraphile
resolver를 자유롭게 짤 수 있나◐ (메타 필요)
복잡한 비즈니스 로직△ (Action/RemoteSchema로)
cross-service 호출△ (Remote Schema)
자동 권한 (row-level)❌ (코드)❌ (코드)
깊은 selection의 round-tripdepth만큼1번1번1번
학습 비용낮음중간중간낮음 (개발자 입장)
운영 비용낮음낮음낮음중간 (호스팅·라이선스)
DB-first 강제
TypeScript 타입 안전수동✅ (Prisma)수동✅ (codegen)
마이크로서비스 환경잘 맞음단일 DB 단위단일 DB 단위단일 DB 단위

한 표로 어느 자리에 어느 도구


What-if — 각 접근의 실패 모드

Hasura/PostGraphile를 잘못 고른 경우

  • 복잡한 도메인 로직이 GraphQL Action으로 밀려나면서 — 로직이 Hasura YAML + Action server + Postgres function세 군데 흩어진다.
  • 행 단위 권한이 단순 RLS로 안 풀리는 케이스(예: 다른 사용자에게 위임)에서 SQL function으로 도망갔다가 Hasura 캐싱과 충돌.

Prisma + Pothos를 잘못 고른 경우

  • 극단적 read-heavy에서 Prisma가 생성하는 SQL손튜닝한 SQL보다 미세하게 느림 — N+1은 잡혔는데 각 쿼리가 5% 느림이 누적.
  • 복합 unique index부분 index 같은 고급 Postgres 기능이 Prisma migration에서 지원이 늦다.

join-monster를 잘못 고른 경우

  • 권한 필터를 SQL WHERE로 박기 시작하면 — 조건이 복잡해지면서 SQL builder 메타가 비대해진다.
  • 깊은 polymorphic 관계에서 SQL JOIN이 너무 커져 — DB optimizer가 plan을 잘못 잡는다.

DataLoader만 고집한 경우

  • depth 4~5단 selection에서 각 depth마다 round-trip. 해외 region DB에서 latency가 누적.
  • 형제 필드 사이cross-field optimization(예: posts와 comments를 한 번에)이 불가능.

Insight — 네 접근은 층위가 다르다

이 네 가지를 경쟁자로 보면 — 어느 게 *옳은가?*에서 막힌다. 층위로 보면 — 언제 어느 것을 쌓는가가 답이 된다.

  • L1(DataLoader)은 언제나 깔린다 — 가장 보편적.
  • L2(Lookahead)은 깊은 selection에서 켠다.
  • L3(SQL compiler)은 패턴이 반복되면 켠다.
  • L4(자동 API)는 CRUD가 거의 전부인 자리에 켠다.

한 번에 다 쓰는 회사도 있다 — Shopify 같은 큰 GraphQL 사용자는 CRUD 영역은 Hasura/내부 도구, 비즈니스 로직 영역은 손으로 짠 resolver + DataLoader, cross-service는 Federation — 이렇게 공존시킨다.


요약 + Mermaid

접근핵심 한 줄강점약점
DataLoadertick에 모인 호출을 batch보편적, 가벼움depth 누적
Lookaheadinfo로 selection 미리 분석depth 한 번에cross-service 불가
join-monsterresolver에서 SQL JOIN 컴파일단일 DB에 강함메타 결합
Pothos + Prisma플러그인이 lookahead 자동TS 타입 안전Prisma 의존
HasuraDB → 자동 GraphQL API코드 zero, 빠름DB-first 강제
PostGraphilePostgres → 자동 + plugin확장성Postgres only

한 줄 결론 — 네 접근은 경쟁이 아니라 층위다. 어느 층에서 N+1을 흡수할지조직의 자유도 vs 자동화 트레이드오프다. 챕터 종료 — 다음 챕터(04-transport)는 이렇게 만들어진 응답을 네트워크로 어떻게 실어 보낼지를 다룬다.