GraphQL ‐ SpringBoot기반 GraphQL Server 구현 - thought-corner/Backend-PlayGround GitHub Wiki
GraphQL - SpringBoot기반 GraphQL Server 구현
- 클라이언트가 어떤 데이터를 어떤 형식으로 받고 싶은지를 직접 명시하면 서버가 정확히 그만큼 돌려주는 방식이다.
- REST가 서버에서 정한 고정된 응답을 여러 URL로 제공하는 것과 대비된다.
- REST에서 흔한 오버페칭(필요 없는 필드까지 받음)과 언더패칭(데이터를 모으고자 여러 번 호출)이 줄어든다.
- 여러 데이터 소스를 하나로 통합하는 것도 강점이다. 리졸버 뒤에 DB, REST API, 다른 MSA가 섞여 있어도 클라이언트는 하나의 일관된 그래프로 보인다. 추가로 스키마가 명확한 계약이라 자동 문서화, 타입 안전성, 프론트/백 측 병렬 개발이 쉬워진다. 스키마만 합의하면 양쪽이 독립적으로 작업할 수 있게 된다.
GraphQL Syntax
1. 스키마 정의(SDL)
- 서버가 제공하는 타입과 연산을 정의하는 부분이다.
# 객체 타입
type User {
id: ID! # ! = non-null (필수)
name: String!
email: String
age: Int
posts: [Post!]! # [ ] = 리스트
}
type Post {
id: ID!
title: String!
author: User!
}
2. 진입점 : Query / Mutation / Subscription
type Query { # 조회 (읽기)
user(id: ID!): User
users: [User!]!
}
type Mutation { # 생성·수정·삭제 (쓰기)
createUser(name: String!, email: String): User!
deleteUser(id: ID!): Boolean!
}
type Subscription { # 실시간 구독
userCreated: User!
}
Operation Name, Arguments, Variables
1. Arguments(인자)
{
user(id: "1") { # id가 인자
name
posts(first: 3) { # first도 인자 (중첩 필드에도 가능)
title
}
}
}
2. Operation Name(연산 이름)
- 연산 종류(query / mutation)와 이름을 붙이는게 좋다. 디버깅·로깅·캐싱에서 이 이름으로 식별된다.
query GetUserWithPosts { # query = 연산 타입, GetUserWithPosts = 연산 이름
user(id: "1") {
name
posts(first: 3) {
title
}
}
}
3. Variables(변수)
- 인자 값을 쿼리에 하드코딩하지 않고 외부에서 주입한다.
$이름: 타입으로 선언하고, 필드 인자 자리에$이름으로 꽂는다.
query GetUserWithPosts($userId: ID!, $postCount: Int!) {
user(id: $userId) {
name
posts(first: $postCount) {
title
}
}
}
Alias, Fragments
1. Alias(별칭)
- 같은 필드를 여러 번 쓰거나, 응답 키 이름을 바꾸고 싶을 때 쓴다.
- 같은 필드를 다른 인자로 두 번 부르려면 별칭이 필수이다.
query {
currentUser: user(id: "1") { # 응답 키가 currentUser로 나옴
name
}
adminUser: user(id: "99") { # 같은 user 필드, 다른 키
name
}
}
2. Fragments(프래그먼트)
- 반복되는 필드 묶음을 이름 붙여 재사용한다.
on 타입으로 어떤 타입에 쓸지 지정한다.
query {
currentUser: user(id: "1") { ...userFields }
adminUser: user(id: "99") { ...userFields }
}
fragment userFields on User {
id
name
email
}
Directives, Inline Fragments, __typename
1. Directives(디렉티브)
@로 시작하며, 필드를 조건부로 포함/제외한다. 변수와 함께 쓰여 동적 쿼리를 만든다.
query GetUser($detailed: Boolean!, $skipEmail: Boolean!) {
user(id: "1") {
name
email @skip(if: $skipEmail) # true면 이 필드 제외
posts @include(if: $detailed) { # true일 때만 포함
title
}
}
}
2. Inline Fragments(인라인 프래그먼트)
- 이름 없는 프래그먼트로, 주로 인터페이스나 유니온에서 "타입별로 다른 필드"를 가져올 때 사용한다.
# 스키마: union SearchResult = User | Post
query Search {
search(text: "graphql") {
... on User { # 결과가 User일 때만
name
email
}
... on Post { # 결과가 Post일 때만
title
author { name }
}
}
}
3. __typename(내성 필드)
- 어떤 객체든 붙일 수 있는 메타 필드로, 현재 객체의 실제 타입 이름을 돌려준다.
query Search {
search(text: "graphql") {
__typename # "User" 또는 "Post" 문자열 반환
... on User { name }
... on Post { title }
}
}
Meta Fields, Introspection
1. Meta Fields(메타 필드)
- 일반 스키마 필드와 별개로, GraphQL이 기본 제공하는
__로 시작하는 필드들이다. __typename,__schema,__type이 있다.
query {
user(id: "1") {
__typename # "User"
name
}
}
2. Introspection(인트로스펙션)
- GraphQL 서버는 자기 자신의 스키마를 쿼리로 조회할 수 있게 해준다. GraphiQL/Apollo Studio의 자동 문서·자동완성이 모두 이 기능으로 동작한다.
query {
__schema {
types {
name
kind # OBJECT, SCALAR, ENUM, INPUT_OBJECT ...
}
queryType { name } # 보통 "Query"
mutationType { name } # "Mutation"
}
}
GraphQL Server System Design
- HTTP 수신 —
POST /graphql이DispatcherServlet을 거쳐RouterFunction에 매핑된GraphQlHttpHandler로 전달된다. 이 때 요청 본문은 query(문자열), operationName, variables 세 부분으로 분해된다. - Interceptor 전처리 —
WebGraphQlInterceptor체인이 실행 전에 돈다. 여기서 인증 토큰을 검증해GraphQLContext에 담는 등 공통 처리를 한다. - Parse — graphql-java가 쿼리 문자열을 Document(AST)로 파싱한다.
- Validate — AST를 스키마(schema.graphqls)와 대조해 검증한다. 없는 필드·타입 불일치·잘못된 인자면 거부된다.
- Execute 시작 — 검증된 AST를 루트(Query 또는 Mutation)부터 순회한다.
- 리졸버(DataFetcher) 호출 — 각 필드마다 대응하는
@QueryMapping/@SchemaMapping메서드가 호출된다. - 하위 계층 위임 - 리졸버 → @Service → @Repository → DB.
- 결과 조립 — 각 리졸버의 반환값을 AST 모양 그대로 트리에 채워 응답 맵을 만든다.
- Interceptor 후처리 → 직렬화 —
WebGraphQlInterceptor가 응답을 후처리하고, JSON으로 직렬화되어 클라이언트에 응답이 나간다.