01 · SDL 문법
이 문서가 답하는 질문: Schema Definition Language의 문법은 무엇이고,
!와[T]의 조합이 만드는 4가지 nullability는 어떻게 다른가? 한 줄 답 (Pyramid Top): “SDL은type + field + ! + [T]4요소만으로 모든 GraphQL 계약을 표현하고, 그중 !의 위치가 의미의 90%를 결정한다.”
Why — 왜 SDL이 존재하는가
GraphQL 첫 버전(2015)은 스키마를 JavaScript 코드로만 정의했다. new GraphQLObjectType({ name: 'User', fields: { ... } }) 같은 형태. 문제는 두 가지였다:
- 언어 종속: Python·Go·Rust 서버가 같은 스키마를 공유할 방법이 없다.
- 사람이 읽기 어려움: 200줄짜리 스키마가 600줄 코드가 된다.
SDL(Schema Definition Language)은 언어 무관한 텍스트 표현을 위해 2016년 RFC로 추가되었다. 지금은 GraphQL Spec October 2021 §3에 정식 포함되어 있다.
핵심 통찰: SDL은 데이터 정의가 아니라 계약 정의다. 어떤 모양의 데이터가 어떤 이름으로 요청·반환 가능한지를 선언한다.
How — 4개의 빌딩 블록
1) type — object type 정의
type User {
id: ID!
name: String!
email: String
}User라는 object type을 선언했다. 세 필드를 가진다.
2) Root types — Query, Mutation, Subscription
GraphQL 스키마는 *진입점(entry point)*을 3개의 root type으로 지정한다.
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
type Query {
user(id: ID!): User
users: [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
type Subscription {
userCreated: User!
}- Query: 읽기 진입점 — 부작용 없어야 한다는 관례가 있다(스펙 강제 아님).
- Mutation: 쓰기 진입점 — 직렬(serial) 실행.
- Subscription: 스트림 진입점 — WebSocket·SSE로 push.
schema { ... } 블록은 root type 이름이 Query/Mutation/Subscription이면 생략 가능. 대부분의 서버가 이 관례를 따른다.
3) ! — non-null
GraphQL의 가장 자주 틀리는 부분이다. 기본은 nullable이고, !가 있어야 non-null이 된다.
type User {
id: ID! # non-null — null 절대 안 옴
name: String # nullable — null 올 수 있음
}TypeScript와 반대 방향이다. TS는 string = non-null, string | null = nullable. GraphQL은 String = nullable, String! = non-null.
4) [T] — list
type Query {
users: [User] # nullable list of nullable User
tags: [String!] # nullable list of non-null String
posts: [Post]! # non-null list of nullable Post
ids: [ID!]! # non-null list of non-null ID
}[T]와 !의 조합은 4가지 의미를 만든다.
| 표기 | 리스트 자체 | 원소 | 의미 |
|---|---|---|---|
[T] | nullable | nullable | 가장 약함. 리스트도 null, 원소도 null 가능 |
[T]! | non-null | nullable | 리스트는 보장, 원소는 null 가능 (예: [Post]! = 빈 배열일 수는 있지만 null 아님) |
[T!] | nullable | non-null | 리스트가 null이면 필드 자체가 null |
[T!]! | non-null | non-null | 가장 강함. 대부분의 list 응답은 이걸 써야 한다 |
[User!]!는 “User 리스트는 반드시 있고(빈 배열 가능), 그 안에 null은 없다”는 뜻.
What — 구체 사양
SDL의 모든 키워드 (October 2021 spec §3)
schema type interface union enum
input scalar directive extend
query mutation subscription
implements on repeatable
true false null주석
"한 줄 설명 (description, introspection에 노출)"
type User {
"사용자의 고유 ID"
id: ID!
"""
여러 줄
description
"""
bio: String
}
# 이건 그냥 주석 (introspection에 노출 안 됨)"..."와 """..."""는 description이고 introspection으로 클라이언트에 보인다. #는 진짜 주석이다.
Field argument
type Query {
user(id: ID!): User
users(
limit: Int = 20 # default value
offset: Int = 0
role: Role
): [User!]!
}인자에 default value를 주면 클라이언트가 보내지 않아도 그 값이 들어온다.
extend — 스키마 분할
# user.graphql
type Query {
user(id: ID!): User
}
# post.graphql
extend type Query {
post(id: ID!): Post
}extend는 같은 type을 여러 파일에서 나눠 정의할 때 쓴다. Federation에서 핵심.
전체 예제 — 미니 블로그 스키마
"""사용자 — 시스템의 인증 주체"""
type User {
id: ID!
name: String!
email: String!
role: Role!
posts: [Post!]!
}
enum Role {
ADMIN
EDITOR
READER
}
type Post {
id: ID!
title: String!
body: String!
author: User!
tags: [String!]!
publishedAt: String
}
type Query {
me: User
user(id: ID!): User
posts(limit: Int = 20, offset: Int = 0): [Post!]!
}
input CreatePostInput {
title: String!
body: String!
tags: [String!] = []
}
type Mutation {
createPost(input: CreatePostInput!): Post!
}이 33줄이 완결된 계약이다 — 클라이언트가 어떤 쿼리를 보낼 수 있고 어떤 응답을 받는지 전부 적혀 있다.
What-if — 자주 틀리는 패턴
함정 1) !를 빼먹어서 모든 게 nullable
type User {
id: ID
name: String
email: String
}이러면 어떤 필드도 null 아님이 보장 안 된다. 클라이언트가:
user?.id?.toLowerCase() // 매번 옵셔널 체이닝원칙: 응답 필드는 기본 non-null로 적고, 진짜로 null이 가능한 것만 ! 빼라.
함정 2) [Post]를 [Post!]!로 안 쓰기
type Query {
posts: [Post] # ⚠️ 리스트도 null, 원소도 null
}응답 처리할 때:
const data = res.posts ?? [] // 리스트 null 처리
for (const p of data) {
if (!p) continue // 원소 null 처리
console.log(p.title)
}[Post!]!로 적으면 두 줄 다 사라진다.
함정 3) Query/Mutation을 안 적기
type User { id: ID! }이건 진입점 없는 스키마다. 서버 구동 자체가 실패한다. 최소 하나의 Query field가 있어야 스펙상 valid.
함정 4) field name으로 __로 시작
type User {
__secret: String # ⚠️ 스펙 위반
}__로 시작하는 이름은 introspection 시스템 예약이다(__schema, __type, __typename). 사용자 필드에 쓰면 등록 실패.
함정 5) circular type을 lazy로 안 푸는 code-first
schema-first(SDL)는 자연스럽게 순환 참조를 푼다 — 단순히 텍스트라서:
type User { posts: [Post!]! }
type Post { author: User! }code-first(JS)는:
// ❌ ReferenceError — Post가 아직 정의 안 됨
const User = new GraphQLObjectType({
fields: { posts: { type: new GraphQLList(Post) } }
})
// ✅ fields를 함수로 감싸 lazy 평가
const User = new GraphQLObjectType({
fields: () => ({ posts: { type: new GraphQLList(Post) } })
})Insight — 흥미로운 이야기
“non-null이 default였다면 GraphQL은 다른 언어가 되었을 것”
2015년 spec 초안에서 non-null이 default이고
?가 nullable 안이 논의됐다. 거부된 이유: 부분 응답(partial response) 때문이다. GraphQL은 필드 하나가 실패해도 나머지를 응답으로 돌려준다. 실패한 필드는 null이 된다. 만약 default가 non-null이었다면, 한 필드의 실패가 부모 객체 전체를 null로 만들고, 또 그 부모가 non-null이면 그 위까지 null이 전파 — 결국 응답 전체가 무너진다. 그래서 기본 nullable은 부분 실패에 대한 회복력의 다른 이름이다.
“
[T!]!는 약 84%의 list field에서 옳다”GitHub 공개 스키마(2024년 기준 1900+ 타입)를 분석하면 list field의 약 84%가
[T!]!다. 빈 배열은 가능하지만 null이거나 null 원소가 의미 있는 경우는 드물다. 새 필드를 적을 때 기본을[T!]!로 두고, null이 필요한 이유가 있을 때만 약하게 적는 습관이 안전하다.
요약 + Mermaid
요약: SDL은
type·field·!·[T]4 요소다.Query/Mutation/Subscription이 진입점이고,!는 non-null,[T]는 list다.[T!]!가 list field의 안전한 기본값. 기본이 nullable인 이유는 부분 실패에 대한 회복력이다. 다음 문서(02-types-scalars-objects)는 이 빌딩블록 위에 어떤 타입이 들어가는지를 다룬다.