01 — GraphQL as a Query Language
한 줄 답: GraphQL은 RPC도 ORM도 아니다 — SQL의 사촌에 가까운 declarative query language다. 클라이언트는 원하는 결과의 모양을 쿼리로 선언하고, 엔진은 그 모양에 맞는 응답을 만든다. 함수를 호출하는 게 아니라, 데이터의 모양을 묘사한다.
Why — 왜 “query language”인지가 중요한가
이름이 Graph + QL인 이유는 SQL의 QL과 같은 family임을 강조하려는 의도였다. 하지만 실무에서는 이 본질이 자주 흐려진다.
| 흔한 오해 | 현실 |
|---|---|
| ”GraphQL = HTTP API의 한 종류” | 사양 자체는 transport와 무관한 language — HTTP는 가장 흔한 운반체일 뿐 |
| ”GraphQL = function 호출의 batched 버전” | 함수가 아니라 type-shaped value를 요청하는 것 — 결과 노드 하나하나가 type이 있다 |
| ”GraphQL = JSON 응답 templating” | 응답이 JSON일 뿐, 그 모양을 결정하는 건 type system |
“query language”라는 정의를 받아들이면 이런 질문들이 한 번에 풀린다 — “왜 응답이 요청과 같은 모양인가”(쿼리가 결과 타입을 지정하니까), “왜 서버가 fragment를 자동으로 인라이닝하나”(쿼리는 type 표현식이라 type-level 변환이 가능하니까), “왜 GraphQL을 SQL로 컴파일하려는 시도가 자꾸 등장하나”(둘이 같은 family니까).
How — 어떻게 “language”로서 동작하나
1) Declarative — 무엇을 받을지만 적는다
{
user(id: 1) {
name
posts(first: 5) {
title
author { name }
}
}
}이 쿼리는 *“어떻게 가져올지”*는 한 줄도 적지 않았다. JOIN 순서, N+1 회피, 캐시 hit 여부 — 모두 엔진의 책임. 클라이언트는 결과 모양만 선언한다. SQL의 SELECT name, age FROM users WHERE id=1과 같은 사고 모델이다.
2) Type-shaped Result — 결과는 type-level로 정의된다
쿼리 { user(id:1) { name } }는 단순히 “데이터 요청”이 아니라 type-level 함수 적용이다 — 스키마의 User 타입에서 name 필드만 골라 새 type을 만들고(structural sub-typing), 응답은 그 type의 instance다. TypeScript로 비유하면 Pick<User, 'name'>이다.
3) Algebraic Data Types — Product와 Sum이 둘 다 있다
GraphQL의 type system은 대수적 데이터 타입(ADT) 이론에 정확히 대응된다.
| GraphQL 구문 | ADT 명칭 | 의미 |
|---|---|---|
type User { name, age } | Product type | name × age (둘 다 동시에 존재) |
union SearchResult = User | Post | Sum type | User + Post (둘 중 하나) |
interface Node { id } | bounded polymorphism | ”id를 가진 모든 것” |
enum Role { ADMIN, USER } | finite sum (unit) | 가능한 값의 집합 |
[Post] | List | type의 collection |
Post! | Non-Null | optional의 부정 |
# Sum type 예시
union SearchResult = User | Post | Comment
{
search(q: "graphql") {
__typename
... on User { name }
... on Post { title }
... on Comment { body }
}
}... on TypeName은 함수형 언어의 pattern matching과 정확히 같다 — match result with | User u -> u.name | Post p -> p.title | .... GraphQL이 함수형 언어 커뮤니티에서 사랑받는 이유다.
4) Composable — 쿼리는 type-level로 합성된다
fragment UserCore on User {
id
name
}
fragment PostWithAuthor on Post {
title
author { ...UserCore }
}
{
feed { ...PostWithAuthor }
me { ...UserCore }
}fragment는 type에 묶인 부분 selection이다. 두 fragment를 한 쿼리에서 재사용해도 type 안전이 깨지지 않는다 — 컴파일러(graphql-codegen)는 fragment를 추적해 정확한 TS 타입을 만든다. 이것이 GraphQL이 대규모 클라이언트에서 강한 이유다.
What — 다른 query language와 비교
SQL과의 비교 — 가장 가까운 친척
-- SQL
SELECT u.name, p.title
FROM users u
JOIN posts p ON p.author_id = u.id
WHERE u.id = 1;# GraphQL
{
user(id: 1) {
name
posts { title }
}
}| 측면 | SQL | GraphQL |
|---|---|---|
| 결과 모양 결정 | SELECT 절 | selection set |
| 관계 표현 | JOIN | 중첩 selection (resolver가 처리) |
| 타입 시스템 | 컬럼 타입 | 강타입 스키마 (Object/Union/Interface/Enum) |
| 컴파일 단위 | 한 쿼리 = 한 평면 결과 | 한 쿼리 = 트리 결과 |
| 부수 효과 | DML (INSERT/UPDATE) | mutation |
| 표준 | ANSI SQL (방언 많음) | GraphQL spec (방언 거의 없음) |
GraphQL은 SQL의 모양 결정성은 가져오되, RDB 종속성을 버린 언어다 — 데이터 소스가 PostgreSQL이든 MongoDB든 REST API든 상관없다.
Datalog·SPARQL과의 비교 — 더 먼 친척
- Datalog: GraphQL과 같이 declarative graph query지만, 재귀가 강하고 (transitive closure) GraphQL은 약하다 (depth 제한).
- SPARQL (RDF): triple pattern matching. GraphQL은 named edge만 따라가지만 SPARQL은 unknown predicate도 매칭 가능.
- Cypher (Neo4j): graph pattern matching. GraphQL은 type 기반 traversal, Cypher는 pattern 기반 matching.
GraphQL은 이들 중 가장 약한 query language다 — 의도된 단순화. “그래프 DB의 풀 표현력”이 아니라 “API 클라이언트가 쉽게 쓸 수 있을 만큼”만 가져왔다.
RPC와의 비교 — 다른 family
RPC: client.getUser(1) → server.getUser(1) → User
GraphQL: { user(id:1) { name } } → engine → { user: { name } }RPC는 함수 호출이다 — 결과 type은 서버가 정의한 함수 시그니처가 결정한다. GraphQL은 type 표현식 평가다 — 결과 type은 쿼리가 정의한다. 같은 결과를 받더라도 어디서 모양을 결정하는가가 다르다.
→ 자세한 비교는 05-graphql-vs-rpc.mdx.
What-if — 이 본질을 놓치면
1) “GraphQL = REST의 batched 버전”으로만 보면
→ 단순히 over-fetching 해결책으로만 인식해 type system을 쓰지 않게 된다. 대응: SDL을 문서가 아니라 코드로 다룬다 — codegen으로 클라이언트 type 자동 생성.
2) “GraphQL은 함수 묶음”으로 보면
→ schema를 RPC method 카탈로그처럼 짜고, 모든 쿼리가 1-level 깊이로 머문다.
대응: 도메인 그래프를 먼저 그리고, 관계를 따라가는 쿼리를 설계 (feed.author.posts.comments.author).
3) Union/Interface를 안 쓰면
→ Sum type을 type 안전하게 표현할 길이 막힌다 — result.status: "ok" | "error" 같은 모양을 문자열 필드로 대충 처리.
대응: union Result = Success | Error 사용. 클라이언트 codegen이 exhaustive match를 강제한다.
4) Fragment를 복붙 회피용으로만 보면
→ “코드 중복 줄이기” 정도로 쓰고, type-level composition 도구임을 놓친다. 대응: fragment를 컴포넌트 단위 데이터 의존성으로 본다 — Relay 패턴이 이를 강제한다.
5) { ... }를 DSL이 아닌 JSON으로 보면
→ 동적 string concat으로 쿼리를 만들고, validation·codegen·persisted query를 전부 잃는다.
대응: 쿼리는 코드다 — .graphql 파일에 두고 tooling을 통과시킨다.
Insight — 흥미로운 이야기
”Lee Byron의 함수형 배경”
GraphQL의 핵심 설계자 중 한 명인 Lee Byron은 Facebook 합류 전 ImmutableJS의 저자였고, 함수형 프로그래밍 커뮤니티에서 활동했다. 그가 GraphQL에 끌어들인 사고 — type-level composition, referential transparency, non-null의 명시성 — 은 모두 ML/Haskell 계보다. “쿼리가 type-level expression”이라는 관점은 그의 인터뷰에서 거듭 강조된다.
”SQL로 컴파일하려는 시도들”
GraphQL이 query language임이 명확하기에, SQL로 컴파일하려는 시도가 자연스럽게 등장했다 — Hasura, PostGraphile, Prisma. 이들은 GraphQL 쿼리를 그대로 PostgreSQL CTE로 번역한다. 결과: N+1 없음, 한 방의 SQL. GraphQL이 함수 묶음이었다면 이런 컴파일은 불가능했다 — language이기 때문에 다른 language로 컴파일된다.
”Algebraic Effects와 Resolver”
resolver는 흥미롭게도 algebraic effect의 한 형태로 볼 수 있다 — 쿼리는 “이 모양을 원한다”는 순수 표현식이고, resolver는 그 표현식을 평가할 때 발생하는 effect handler다. 이 관점은 GraphQL을 type theory 커뮤니티에서 진지하게 연구하는 한 이유다 (Edinburgh, Cambridge 연구들).
요약 + 다이어그램
GraphQL은 함수 호출이 아니라 type 표현식 평가다. 클라이언트는 결과의 모양을 type-level로 선언하고, 엔진이 그 모양에 맞는 값을 만든다. Product/Sum type, fragment composition, declarative traversal — 모두 SQL과 같은 query language family의 특징.
다음 문서:
02-graphql-vs-rest.mdx— 그럼 REST와는 어떻게 다른가? (스포일러: 둘은 대체가 아니라 상호 보완이다)