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

  1. HTTP 수신POST /graphqlDispatcherServlet을 거쳐 RouterFunction에 매핑된 GraphQlHttpHandler로 전달된다. 이 때 요청 본문은 query(문자열), operationName, variables 세 부분으로 분해된다.
  2. Interceptor 전처리WebGraphQlInterceptor 체인이 실행 전에 돈다. 여기서 인증 토큰을 검증해 GraphQLContext에 담는 등 공통 처리를 한다.
  3. Parse — graphql-java가 쿼리 문자열을 Document(AST)로 파싱한다.
  4. Validate — AST를 스키마(schema.graphqls)와 대조해 검증한다. 없는 필드·타입 불일치·잘못된 인자면 거부된다.
  5. Execute 시작 — 검증된 AST를 루트(Query 또는 Mutation)부터 순회한다.
  6. 리졸버(DataFetcher) 호출 — 각 필드마다 대응하는 @QueryMapping/@SchemaMapping 메서드가 호출된다.
  7. 하위 계층 위임 - 리졸버 → @Service → @Repository → DB.
  8. 결과 조립 — 각 리졸버의 반환값을 AST 모양 그대로 트리에 채워 응답 맵을 만든다.
  9. Interceptor 후처리 → 직렬화WebGraphQlInterceptor가 응답을 후처리하고, JSON으로 직렬화되어 클라이언트에 응답이 나간다.