Reactive GraphQL — Query·Mutation·Variables

2026-05-03확률과 통계 마스터 노트

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 으로 인자 매핑

시리즈 다른 편

공식 문서: GraphQL Queries / Mutations 에서 더 깊이.

다음 글(3편)에서는 Subscription — 실시간 구독, WebSocket·SSE 전송, Reactive Stream과 결합까지 풀어 갑니다.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!