05 — Mental Model
한 줄 답: GraphQL은 graph가 아니라 graph traversal language다. 응답은 항상 요청 selection set과 동형이며, 그 invariant 하나가 GraphQL의 모든 매력과 함정을 설명한다.
Why — 왜 멘탈 모델이 핵심인가
기술의 문법은 외울 수 있다. 하지만 왜 그렇게 동작하는가를 모르면 새 문제 앞에서 항상 막힌다. GraphQL의 모든 의외성 — partial response, N+1, single endpoint, 200 OK에 errors — 은 하나의 멘탈 모델에서 따라 나온다.
이 문서는 그 멘탈 모델을 세 개의 invariant로 압축한다.
- GraphQL은 graph가 아니라 graph traversal이다.
- 응답은 요청 selection set과 동형(isomorphic)이다.
- Partial response는 기능이지 버그가 아니다.
이 셋을 받아들이면 GraphQL의 모든 동작이 자연스럽게 설명된다.
How — 세 가지 invariant
Invariant 1 — Graph가 아니라 Graph Traversal
흔한 오해:
“GraphQL은 Neo4j 같은 그래프 DB 위에서 동작하는 언어다.”
틀렸다. GraphQL의 ‘Graph’는 데이터의 형태가 아니라 접근 방식을 가리킨다.
| 측면 | Graph DB (Cypher 등) | GraphQL |
|---|---|---|
| 다루는 것 | 노드·엣지 저장소 | 노드·엣지 질의 언어 |
| 백엔드 | 그래프 DB 전용 | 무엇이든 (SQL, NoSQL, REST, gRPC…) |
| 쿼리 의미 | ”이 패턴과 매칭되는 모든 경로" | "이 진입점에서 시작해 이 트리를 따라가라” |
GraphQL은 진입점(root)에서 시작해 selection set이 그리는 트리를 따라 traversal한다. 트리의 각 노드에서 resolver가 호출된다.
핵심: 데이터가 그래프 모양일 필요가 없다. resolver가 어떤 데이터 소스에서든 값을 가져오면 된다. GraphQL은 백엔드 무지향이다.
Invariant 2 — 응답은 요청 selection set과 동형
이것이 GraphQL의 가장 강력한 invariant다.
응답의 JSON 구조는 요청의 selection set과 정확히 같은 모양이다.
# 요청
{
user(id: 123) {
name
posts {
title
}
}
}// 응답 — 모양이 *정확히* 일치
{
"data": {
"user": {
"name": "Alice",
"posts": [
{ "title": "Hello" },
{ "title": "World" }
]
}
}
}이 동형성에서 따라 나오는 결과:
| 결과 | 설명 |
|---|---|
| 클라이언트 타입 추론 | 요청에 적힌 필드만 응답에 있다 → 컴파일러가 타입을 정확히 추론 |
| 모양에 대한 협상 불필요 | 서버가 예상 외 필드를 보낼 일이 없다 |
| 버전 관리가 필드 단위 | URL 버전 없이도 deprecated 필드만 표시하면 됨 |
| 테스트 가능 | 요청과 응답을 대칭으로 검증 |
reasonably 큰 함정도 같이 따라 나온다 — 클라이언트가 안 적은 필드는 받을 수 없다. 백엔드가 “이건 항상 같이 보내야 안전한데”라고 생각해도, 클라이언트가 적지 않으면 안 보낸다.
동형성의 예외 — 메타 필드
{
user(id: 123) {
name
__typename ← introspection
}
}→ 응답에 __typename: "User"가 추가된다. 메타 필드는 동형성의 의도된 예외다.
Invariant 3 — Partial Response는 기능이다
REST는 보통 all-or-nothing이다. 한 필드라도 실패하면 5xx로 전부 실패. GraphQL은 다르다.
각 resolver가 독립적으로 실패할 수 있다. 일부가 실패해도 나머지는 정상 응답된다.
{
user(id: 123) {
name # 성공
email # 실패 (권한 없음)
posts {
title # 성공
}
}
}{
"data": {
"user": {
"name": "Alice",
"email": null, // null로 채워짐
"posts": [{ "title": "..." }]
}
},
"errors": [
{
"message": "Permission denied",
"path": ["user", "email"] // 어디서 실패했는지
}
]
}핵심 규칙:
- 실패한 필드가 nullable이면 그 위치만
null(예시처럼) - 실패한 필드가 non-null이면 부모를 null로 전파 → 가장 가까운 nullable 조상까지 거슬러 올라감
- HTTP status는 보통 200 (실행은 됐으니까)
이 invariant 덕에 부분 장애를 견디는 UI를 짤 수 있다 — 이메일 영역은 비워두고, 게시글은 그대로 표시.
What — 정확한 사양 (Execution §6, October 2021)
Resolver 호출 알고리즘 (단순화)
ExecuteQuery(query, schema, variables):
1. Validate query against schema
2. Resolve root operation type (Query/Mutation/Subscription)
3. ExecuteSelectionSet(root, selectionSet, variables, initialValue)
ExecuteSelectionSet(type, selectionSet, vars, parentVal):
For each field in selectionSet:
- 병렬 실행 (query/subscription) 또는 순차 실행 (mutation)
- Resolve field's resolver(parentVal, args, context, info) → value
- If field type is object/interface/union:
ExecuteSelectionSet(fieldType, subSelectionSet, vars, value)
- Else: coerce scalar
Return Map { fieldName → value }핵심:
- Query / Subscription: 같은 selection set 안의 필드는 병렬 실행 가능 (resolver가 부수 효과 없다고 가정)
- Mutation: 같은 selection set 안의 필드는 순차 실행 (부수 효과가 있으니 순서가 의미 있음)
- 중첩 selection set은 부모 resolver의 결과를 받아 자식 resolver에 전달
Null Propagation
type User {
id: ID!
name: String! # non-null
email: String # nullable
}email실패 →email: null+ errors 배열에 항목name실패 →name자체를 null로 못 만듦 →user를 null로 전파 →user도 non-null이면data전체가 null
// name(non-null) 실패 시
{
"data": null, // 전체가 null로 전파
"errors": [{"message": "...", "path": ["user", "name"]}]
}이 규칙이 non-null의 진짜 의미다 — “이 자리에 null이 절대 오지 않는다”는 클라이언트의 약속. 깨지면 위로 전파해서라도 약속을 지킨다.
Subscription의 특수성
subscription OnNewMessage($room: ID!) {
messageAdded(room: $room) {
id text author
}
}- 하나의 root field만 허용 (selection set에 필드 한 개)
- 이벤트마다 전체 selection set이 실행되어 응답이 push됨
- 트랜스포트는 spec 외 — WebSocket(
graphql-ws) 또는 SSE(graphql-sse)가 사실상 표준
What-if — 멘탈 모델을 잘못 잡으면
1) “GraphQL = DB query”라고 보면
증상: 한 쿼리당 DB 쿼리 1번 기대 → 실제론 N+1.
원인: GraphQL은 resolver 트리 실행이지 SQL이 아니다.
대응: DataLoader 패턴으로 같은 필드 호출을 배치. (KB 03-n-plus-1-dataloader)
2) “응답이 항상 fully populated”라고 보면
증상: 클라이언트가 data.user.email을 무조건 string으로 받으려다 NPE.
원인: nullable 필드는 언제든 null일 수 있다 (partial response).
대응: nullable·non-null을 스키마 단계에서 신중히 결정. 클라이언트는 nullable을 항상 방어.
3) “Errors 배열을 무시해도 된다”고 보면
증상: 200 OK인데 data.user.posts가 비어 있고 왜인지 모름.
원인: posts resolver가 실패해서 null로 강등됐는데 errors를 안 봄.
대응: 클라이언트 라이브러리(Apollo Client·urql·Relay)는 기본으로 errors를 노출. 항상 확인.
4) “HTTP status code로 성공/실패를 판단”하면
증상: 200인데 실제론 실패한 요청을 성공으로 분류. 원인: GraphQL은 거의 항상 200 (실행 자체가 성공했으면). 대응: 모니터링은 operation name + errors 배열 기반. HTTP status는 transport-level 실패만.
5) Mutation을 병렬로 보내면
증상: race condition·중복 실행. 원인: spec은 한 selection set 안의 mutation만 순차 보장. 서로 다른 요청 간에는 보장 없음. 대응: 동시 mutation은 클라이언트가 큐잉하거나, 서버가 idempotency key를 받음.
6) Subscription을 사용해 상태 동기화
증상: 새 클라이언트가 과거 이벤트를 못 받아 화면이 비어 있음. 원인: subscription은 미래 이벤트 push용 — 현재 상태 조회가 아니다. 대응: 초기 상태는 query로 받고, 그 이후 subscription으로 갱신.
Insight — 흥미로운 이야기
”GraphQL이 graph가 아니라 traversal인 이유”
Lee Byron이 한 인터뷰에서 한 말 — “이름을 다시 짓는다면 ‘GraphQL’ 대신 ‘ShapeQL’이라고 했을 것이다.” GraphQL의 본질은 데이터의 모양을 클라이언트가 선언하는 것이지, 그래프 구조 자체가 아니다. ‘Graph’는 Facebook 내부의 Open Graph에서 따온 명칭일 뿐, 데이터베이스의 그래프와 무관하다.
이 사실을 받아들이면 어떤 백엔드 위에도 GraphQL이 얹힐 수 있다는 게 명백해진다 — 실제로 Hasura는 PostgreSQL 위에, AWS AppSync는 DynamoDB 위에, Apollo Federation은 여러 서비스의 합성에서 동작한다.
”동형성(isomorphism)이 만든 React-GraphQL 결혼”
Facebook이 React와 GraphQL을 같은 시기에 같은 팀에서 만든 것은 우연이 아니다. React 컴포넌트는 자기가 그릴 트리의 모양을 선언하고, GraphQL 쿼리도 자기가 받을 데이터의 모양을 선언한다. 둘의 모양이 동형이라는 게 그 결혼의 핵심 — Relay 같은 라이브러리가 컴파일 타임에 컴포넌트의 fragment를 합성해 데이터와 UI를 동형으로 만든다. 이 패턴은 후에 colocated data fetching이라는 이름으로 일반화됐다.
”Partial response는 mobile에서 왔다”
2012년 Facebook iOS에서 셀룰러 네트워크는 순간 끊기는 게 일상이었다. 한 필드 실패로 전체 News Feed가 못 뜨면 UX가 죽는다. 부분 성공이 곧 UX라는 모바일 현실이 GraphQL의 partial response 디자인을 강제했다. 같은 이유로 non-null을 신중히 쓰라는 community 가이드가 굳어졌다 — non-null은 null 전파를 일으켜 partial response의 이점을 깎는다.
”Why graph traversal scales — Federation의 출발점”
쿼리가 그래프 traversal이라는 사실은 분산 시스템과 잘 맞는다. selection set의 각 sub-tree를 다른 서비스가 처리해도 결과를 합치기만 하면 된다. Apollo Federation은 이 통찰을 production 수준의 합성 시스템으로 만들었고, 2022년 Federation 2가 사실상 산업 표준이 됐다. GraphQL이 마이크로서비스 BFF의 기본 선택이 된 것은 이 invariant 덕이다.
요약 + 다이어그램
GraphQL은 세 invariant 위에 서 있다 — graph가 아니라 traversal이고, 응답은 요청과 동형이며, partial response는 기능이다. 이 셋만 받아들이면 단일 endpoint·200 OK·null propagation·federation까지 모두 같은 원리로 설명된다.
챕터 종료. 이제
01-schema-sdl챕터로 — 언어를 알았으니 타입으로 모양을 잡을 차례다.