Reactive GraphQL 마스터 노트 시리즈 1편. GraphQL이 REST의 한계를 푸는 방식, Over-fetching·Under-fetching 문제 해결, Schema-first 설계와 SDL 문법, 8 기본 타입과 Custom 타입, Query·Mutation·Subscription 3 작업, Resolver의 역할, REST vs GraphQL 결정적 차이까지.
이 글은 Reactive GraphQL 마스터 노트 시리즈의 첫 번째 편입니다. REST의 over-fetching·under-fetching 한계. GraphQL이 해결책. Facebook이 만들고 오픈소스화 → 모던 API 표준 중 하나.
이 시리즈 7편은 기본 개념·Query/Mutation·Subscription·Spring GraphQL·Reactive·Security/Testing·고급까지. 1편의 목표 — GraphQL이 무엇이고 왜 손에 잡히게.
이 시리즈는 GraphQL 공식 사양, Spring for GraphQL 가이드, GraphQL Java 문서 등 공개 자료를 참고해 한국어 학습 노트로 풀어쓴 자료입니다.
Spring Boot 프로젝트에 spring-boot-starter-graphql 추가하고 GraphiQL UI로 첫 쿼리 실행해 보면 흐름이 한 번에 잡혀요. 30분이면 첫 GraphQL API가 손에 들어옵니다.
처음 GraphQL이 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, REST와 어떻게 다른지 막연합니다. 둘째, Schema·Resolver·Type 단어들이 한 번에 등장합니다.
해결법은 한 가지예요. "GraphQL = 클라이언트가 원하는 데이터 정확히 명시" 한 줄. 사용자가 SQL 같은 쿼리. 서버는 정확히 그 데이터만. Over·Under-fetching X.
REST의 한계
Over-fetching (과조회)
GET /users/123
# 응답
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"phone": "010-1234-5678",
"address": "...",
"preferences": {...},
"meta": {...}
}
이름만 필요해도 전체 받음. 모바일 환경 = 데이터 낭비.
Under-fetching (과소조회)
GET /users/123 # 사용자
GET /users/123/posts # 게시물
GET /users/123/comments # 댓글
3 호출 = 3 RTT. 모바일 = 느림.
응답 형태 고정
GET /users/123 → 항상 같은 형태
새 필드 필요 → 새 엔드포인트 또는 versioning
여기서 정말 중요한 시험 함정 — REST 한계 해결 = GraphQL의 등장 배경. Facebook 모바일 앱에서 출발 (2012·오픈소스 2015).
GraphQL — 클라이언트가 명시
query {
user(id: 123) {
name
posts {
title
comments {
text
}
}
}
}
응답:
{
"data": {
"user": {
"name": "Alice",
"posts": [
{
"title": "Hello",
"comments": [{ "text": "Great!" }]
}
]
}
}
}
핵심:
- 클라이언트가 필드 명시
- 단일 요청에 여러 데이터 (User + Posts + Comments)
- 응답 = 요청 형태 그대로
Schema-first
type Query {
user(id: ID!): User
users: [User!]!
}
type User {
id: ID!
name: String!
email: String
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String
author: User!
}
여기서 정말 중요한 시험 함정 — Schema = 계약 (Contract). 서버·클라이언트의 약속. 변경 시 양쪽 모두 의식. REST의 OpenAPI/Swagger와 비슷하지만 더 강력.
SDL — Schema Definition Language
# 기본 타입
type User {
id: ID! # ID, !는 non-null
name: String! # 필수
email: String # nullable
age: Int
active: Boolean!
joinedAt: String
}
# 리스트
type Query {
users: [User] # 리스트 (null 가능)
posts: [Post!] # 리스트, 항목 non-null
comments: [Comment!]! # 리스트 자체 non-null + 항목 non-null
}
8 Scalar 타입
ID — 고유 식별자 (String 비슷)
String — 텍스트
Int — 32-bit 정수
Float — 부동 소수점
Boolean — true/false
Custom — 사용자 정의 (Date·BigDecimal·UUID 등)
Enum
enum Role {
ADMIN
USER
GUEST
}
Interface
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
Union
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
여러 타입 중 하나.
Input
input CreateUserInput {
name: String!
email: String!
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
Mutation 인자용. 입력 객체.
3 Operation Types
type Query { # 조회 (GET 비슷)
users: [User!]!
}
type Mutation { # 변경 (POST/PUT/DELETE 비슷)
createUser(input: CreateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
type Subscription { # 실시간 구독 (WebSocket)
userAdded: User!
}
자세한 건 2·3편.
Resolver — 데이터 가져오는 함수
@Controller
public class UserController {
@QueryMapping
public User user(@Argument String id) {
return userService.findById(id);
}
@SchemaMapping(typeName = "User", field = "posts")
public List<Post> posts(User user) {
return postService.findByAuthor(user.getId());
}
}
Schema의 각 필드 = Resolver 함수. 필드 요청될 때 호출.
여기서 정말 중요한 시험 함정 — Resolver는 필드 단위. user.posts 요청 시 별도 Resolver 호출. 모든 필드가 자기 데이터 가져옴. N+1 문제 발생 → DataLoader (7편).
REST vs GraphQL
| 측면 | REST | GraphQL |
|---|---|---|
| 엔드포인트 | 다수 (/users·/posts) |
단일 (/graphql) |
| 데이터 형태 | 서버 결정 | 클라이언트 결정 |
| Over-fetching | 자주 | 없음 |
| Under-fetching | 자주 | 없음 |
| 응답 형태 | 고정 | 쿼리 형태 |
| 캐싱 | HTTP 표준 | 어려움 |
| 학습 곡선 | 낮음 | 중간 |
| 도구 | 많음 | 매우 많음 |
여기서 정말 중요한 시험 함정 — GraphQL ≠ REST 대체. 둘 사용처 다름. 클라이언트별 다양한 데이터 = GraphQL / 캐싱·표준화 = REST. 둘 결합도 OK.
사용 사례
GraphQL 적합
- 모바일 앱 (over-fetching 회피)
- 다양한 클라이언트 (웹·모바일·태블릿)
- 복잡한 데이터 그래프 (사용자→게시물→댓글)
- 빠른 변경 API
- 공개 API (외부 개발자)
GraphQL 부적합
- 단순 CRUD
- 파일 업로드 (REST가 자연스러움)
- HTTP 캐싱 활용 (REST가 우월)
- 복잡한 분석 쿼리 (SQL이 더 강력)
GraphiQL·Apollo Studio
http://localhost:8080/graphiql
GraphQL 전용 IDE. Schema 자동 발견·자동완성·문서. 개발 표준.
첫 GraphQL — Spring Boot
의존성
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
Schema (resources/graphql/schema.graphqls)
type Query {
hello: String!
}
Resolver
@Controller
public class HelloController {
@QueryMapping
public String hello() {
return "Hello, GraphQL!";
}
}
쿼리
POST /graphql
Content-Type: application/json
{
"query": "{ hello }"
}
# 응답
{
"data": { "hello": "Hello, GraphQL!" }
}
Introspection
{
__schema {
types {
name
fields { name type { name } }
}
}
}
GraphQL 자체에 스키마 조회 기능. GraphiQL이 자동 활용.
여기서 시험 함정이 하나 있어요. 운영 환경 Introspection OFF. 보안 — 스키마 노출 = 공격 표면. 개발만.
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 1편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- GraphQL = 클라이언트가 원하는 데이터 명시 (SQL 비슷)
- REST 한계 해결 — Over-fetching / Under-fetching / 형태 고정
- Facebook 2012·오픈소스 2015
- 단일 엔드포인트 (
/graphql) - 응답 = 요청 형태 그대로
- Schema-first = 계약 (Contract)
- SDL (Schema Definition Language)
- 8 Scalar — ID·String·Int·Float·Boolean·Custom
!= non-null·필수- 리스트 —
[User](널 가능) /[User!]!(양쪽 non-null) - Enum·Interface·Union·Input 추가 타입
- 3 Operations — Query·Mutation·Subscription
- Resolver = 필드 단위 함수
- N+1 문제 → DataLoader (7편)
- REST vs GraphQL — 사용처 다름·결합 OK
- 적합 — 모바일·다양한 클라이언트·복잡 그래프·공개 API
- 부적합 — 단순 CRUD·파일 업로드·HTTP 캐싱
- GraphiQL = 전용 IDE (자동완성·문서)
- Spring Boot —
spring-boot-starter-graphql - Schema =
resources/graphql/*.graphqls - Resolver =
@QueryMapping·@SchemaMapping - Introspection = 스키마 조회 (운영 OFF 권장)
시리즈 다른 편
- 1편 — 기본 개념·Schema (현재 글)
- 2편 — Query·Mutation·Variables
- 3편 — Subscription·실시간 구독
- 4편 — Spring for GraphQL
- 5편 — Reactive GraphQL
- 6편 — Security·Testing
- 7편 — 고급 (DataLoader·Federation·운영)
공식 문서: GraphQL Specification / Spring for GraphQL 에서 더 깊이.
다음 글(2편)에서는 Query·Mutation — 조회·변경 작업 패턴, 인자·변수·alias·fragment까지 풀어 갑니다.