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의 자유도 높은 사촌
PostGraphile도 Postgres → 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 — 결정 매트릭스 (한 페이지)
| 요구사항 | DataLoader | Lookahead (Prisma) | join-monster | Hasura / PostGraphile |
|---|---|---|---|---|
| resolver를 자유롭게 짤 수 있나 | ✅ | ✅ | ◐ (메타 필요) | ❌ |
| 복잡한 비즈니스 로직 | ✅ | ✅ | ✅ | △ (Action/RemoteSchema로) |
| cross-service 호출 | ✅ | ❌ | ❌ | △ (Remote Schema) |
| 자동 권한 (row-level) | ❌ (코드) | ❌ (코드) | ❌ | ✅ |
| 깊은 selection의 round-trip | depth만큼 | 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
| 접근 | 핵심 한 줄 | 강점 | 약점 |
|---|---|---|---|
| DataLoader | tick에 모인 호출을 batch | 보편적, 가벼움 | depth 누적 |
| Lookahead | info로 selection 미리 분석 | depth 한 번에 | cross-service 불가 |
| join-monster | resolver에서 SQL JOIN 컴파일 | 단일 DB에 강함 | 메타 결합 |
| Pothos + Prisma | 플러그인이 lookahead 자동 | TS 타입 안전 | Prisma 의존 |
| Hasura | DB → 자동 GraphQL API | 코드 zero, 빠름 | DB-first 강제 |
| PostGraphile | Postgres → 자동 + plugin | 확장성 | Postgres only |
한 줄 결론 — 네 접근은 경쟁이 아니라 층위다. 어느 층에서 N+1을 흡수할지는 조직의 자유도 vs 자동화 트레이드오프다. 챕터 종료 — 다음 챕터(04-transport)는 이렇게 만들어진 응답을 네트워크로 어떻게 실어 보낼지를 다룬다.