Reactive GraphQL 마스터 노트 시리즈 2편. Query의 인자(Argument)와 변수(Variables) 분리, 동일 필드 다중 호출 Alias, 재사용 가능한 Fragment, @include·@skip 디렉티브, Operation Name·Operation Type, Mutation의 입력 객체와 응답 패턴, Optimistic UI까지.
이 글은 Reactive GraphQL 마스터 노트 시리즈의 두 번째 편입니다. 1편(기초)에서 큰 그림을 봤다면, 이번엔 검색·변경 작업 — Query·Mutation.
기본 쿼리는 단순. 다만 Variables·Alias·Fragment 같은 도구로 효율·재사용. 운영 환경 패턴.
처음 Query·Mutation이 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, 인자와 변수가 헷갈립니다. 둘째, Fragment가 막연합니다.
해결법은 한 가지예요. "인자 = 쿼리 안 / 변수 = 외부 파라미터" + "Fragment = 재사용 블록" 두 줄.
Query — 가장 단순
query {
user(id: "123") {
name
email
}
}
응답:
{
"data": {
"user": {
"name": "Alice",
"email": "alice@example.com"
}
}
}
query 키워드 생략 가능 (기본).
Argument — 인자
{
user(id: "123") { # id 인자
name
posts(limit: 10) { # limit 인자
title
}
}
}
각 필드에 인자. 함수 호출 비슷.
Variables — 외부 파라미터
query GetUser($id: ID!, $limit: Int = 10) {
user(id: $id) {
name
posts(limit: $limit) {
title
}
}
}
{
"query": "...",
"variables": {
"id": "123",
"limit": 5
}
}
여기서 정말 중요한 시험 함정 — 인자에 사용자 입력 직접 X. 변수 사용 = SQL Injection 같은 공격 방어. 변수 = 명시 타입·검증.
# X — 직접 삽입 (위험)
query {
user(id: "${userId}") { ... }
}
# O — 변수 사용
query GetUser($id: ID!) {
user(id: $id) { ... }
}
기본값
query GetPosts($limit: Int = 10, $offset: Int = 0) {
posts(limit: $limit, offset: $offset) {
title
}
}
변수 안 넘기면 기본값 사용.
Alias — 동일 필드 다중
{
alice: user(id: "123") {
name
}
bob: user(id: "456") {
name
}
}
응답:
{
"data": {
"alice": { "name": "Alice" },
"bob": { "name": "Bob" }
}
}
같은 필드 = 응답 키 충돌 → Alias로 구분. 다중 조회·비교 시 유용.
Fragment — 재사용
fragment UserBasic on User {
id
name
email
}
query {
alice: user(id: "123") {
...UserBasic
posts { title }
}
bob: user(id: "456") {
...UserBasic
age
}
}
여기서 시험 함정이 하나 있어요. Fragment = 재사용 블록. 같은 필드 묶음을 여러 쿼리에 적용. 코드 DRY.
Inline Fragment
{
search(query: "spring") {
... on User {
name
email
}
... on Post {
title
content
}
}
}
Union·Interface 결과의 타입별 필드 분리.
Directive — @include·@skip
query GetUser($includeEmail: Boolean!) {
user(id: "123") {
name
email @include(if: $includeEmail)
age @skip(if: $includeEmail)
}
}
조건부 필드 포함·제외.
Operation Name
query GetUser($id: ID!) { # GetUser = operation name
user(id: $id) { name }
}
선택. 단일 쿼리는 생략 OK. 다중 작업 시 필수·디버깅·로깅 친화.
Mutation — 데이터 변경
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
input CreateUserInput {
name: String!
email: String!
}
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
{
"variables": {
"input": {
"name": "Alice",
"email": "alice@example.com"
}
}
}
여기서 정말 중요한 시험 함정 — Mutation도 응답 받음. 변경된 데이터 즉시 응답 = 클라이언트 캐시 갱신·UI 즉시 반영.
다중 Mutation — 순차 실행
mutation MultiMutate {
createUser(input: {...}) { id }
createPost(input: {...}) { id }
sendEmail(to: "alice@x.com") { sent }
}
여기서 시험 함정이 하나 있어요. Mutation은 순차 실행 (Query는 병렬). 일관성 보장. Query는 독립이라 병렬.
응답 패턴
단순 응답
type Mutation {
createUser(input: CreateUserInput!): User!
}
Result + Errors 패턴
type CreateUserPayload {
user: User
errors: [Error!]
success: Boolean!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
검증 에러·비즈니스 에러를 응답으로. GraphQL 표준 패턴.
mutation {
createUser(input: {name: ""}) {
success
user { id name }
errors {
field
message
}
}
}
Optimistic UI
mutation LikePost($id: ID!) {
likePost(id: $id) {
id
likeCount
}
}
클라이언트가 응답 받기 전 UI 갱신 (likeCount + 1). 응답 받으면 실제 값으로. UX 즉시 반응.
인자 타입
type Query {
# Scalar 인자
user(id: ID!): User
searchUsers(name: String, age: Int, active: Boolean): [User!]!
# Input 객체 인자
searchPosts(filter: PostFilter): [Post!]!
}
input PostFilter {
title: String
authorId: ID
tags: [String!]
createdAfter: String
}
복잡 인자 = Input 객체.
Mutation 작명 규칙
createUser — 생성
updateUser — 전체 업데이트
patchUser — 부분 업데이트
deleteUser — 삭제
publishPost — 동작 (publish)
likePost — 동작
followUser — 관계
여기서 시험 함정이 하나 있어요. Mutation 이름 = 명령형 동사. RESTful 리소스 X. 비즈니스 동작 명확히.
Pagination
type Query {
posts(first: Int, after: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
Relay-style Connection. 표준 페이지네이션. cursor 기반.
{
posts(first: 10, after: "cursor-abc") {
edges {
node { id title }
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
Filter·Sort·Search
type Query {
searchPosts(
filter: PostFilter
orderBy: PostOrderBy
first: Int
after: String
): PostConnection!
}
input PostFilter {
title_contains: String
author_eq: ID
createdAt_gte: String
}
enum PostOrderBy {
CREATED_AT_ASC
CREATED_AT_DESC
TITLE_ASC
}
복잡 검색·정렬을 명확하게.
Error Handling
{
"data": {
"user": null
},
"errors": [
{
"message": "User not found",
"path": ["user"],
"extensions": {
"code": "NOT_FOUND",
"statusCode": 404
}
}
]
}
여기서 정말 중요한 시험 함정 — GraphQL은 항상 200 OK. 에러는 errors 필드에. HTTP status는 200·전송 문제만 아닌 한.
Resolver — 인자 처리
@Controller
public class UserController {
@QueryMapping
public User user(@Argument String id) {
return userService.findById(id);
}
@QueryMapping
public List<Post> searchPosts(
@Argument PostFilter filter,
@Argument PostOrderBy orderBy,
@Argument Integer first
) {
return postService.search(filter, orderBy, first);
}
@MutationMapping
public User createUser(@Argument CreateUserInput input) {
return userService.create(input);
}
}
@Argument로 인자 매핑.
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 2편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- Query = 조회 (
query키워드 생략 가능) - Argument = 쿼리 안 인자
- Variables = 외부 파라미터 (
$prefix) - 변수 = SQL Injection 같은 공격 방어
- 사용자 입력 = 변수 필수
- 기본값 —
$limit: Int = 10 - Alias — 같은 필드 다중 호출 시 키 구분
- Fragment — 재사용 블록 (
...UserBasic) - Inline Fragment — Union·Interface 타입별 (
... on User) - Directive —
@include(if:)·@skip(if:)조건부 - Operation Name — 디버깅·로깅 (선택)
- Mutation = 변경 + 응답 받음
- Mutation 순차 실행 (Query는 병렬)
- Result + Errors 패턴 = 검증·비즈니스 에러를 응답으로
- Optimistic UI = 응답 전 UI 갱신
- 인자 — Scalar / Input 객체 (복잡)
- Mutation 작명 = 명령형 동사 (createUser·publishPost)
- Pagination = Relay-style Connection (edges·pageInfo·cursor)
- Filter·Sort·Search = Input 객체로 명확
- 에러 = 항상 200 OK +
errors필드 path·extensions(code 등)- Spring —
@Argument으로 인자 매핑
시리즈 다른 편
- 1편 — 기본 개념·Schema
- 2편 — Query·Mutation·Variables (현재 글)
- 3편 — Subscription·실시간 구독
- 4편 — Spring for GraphQL
- 5편 — Reactive GraphQL
- 6편 — Security·Testing
- 7편 — 고급 (DataLoader·Federation·운영)
공식 문서: GraphQL Queries / Mutations 에서 더 깊이.
다음 글(3편)에서는 Subscription — 실시간 구독, WebSocket·SSE 전송, Reactive Stream과 결합까지 풀어 갑니다.