[Spring Boot] GraphQL 소개

이동엽·2024년 5월 2일
2

spring

목록 보기
16/21

신규 서비스에 GraphQL을 도입할 지 고려해보기 위해 학습했던 GraphQL 내용입니다.
6장에는 GraphQL에 대한 개인적인 견해를 작성해보았습니다. 피드백 주시면 감사하겠습니다.

  • 함께 작성한 사람 : Front-end Part Juno
  • 아래 나오는 모든 예시는 Github Repository에서 확인하실 수 있습니다.
  • 추후 보완할 내용
    • 보안을 위해 HTTP Method를 직접 정의하여 사용하기도 하는데, GrpahQL에서도 가능한지?
    • Swagger&RestDoc처럼 GraphiQL을 개발환경에서만 볼 수 있도록 설정하려면?
    • GraphQL 관련 플러그인이 있는가? 해당 플러그인으로 직접 스키마를 작성하지 않을 수 있는지?

1. GraphQL 소개


1-1. GraphQL 이란?

GraphQL은 API를 위한 쿼리 언어이며 이미 존재하는 데이터로 쿼리를 수행하기 위한 런타임 입니다.

  • GraphQL 은 2015년에 오픈 소스로 공개되었습니다.
  • 기존의 API 접근 방식인 REST가 가지는 한계를 극복하기 위해 탄생했습니다.
  • 특히 모바일 디바이스의 다양한 화면 크기와 네트워크 속도에 최적화된 데이터 로딩을 필요로 하는 상황에서,데이터 오버패칭과 여러 API 엔드포인트를 통한 복잡한 데이터 요청 과정을 단순화 하려는 목표를 가지고 있습니다.

1-2. GraphQL의 기본 개념 및 철학

  • GraphQL 의 핵심 철학은 클라이언트가 서버로부터 필요한 데이터를 정확히 필요한 만큼만 요청할 수 있게 하는데에 있습니다.
  • 서버는 클라이언트가 GraphQL 로 요청할 수 있는 데이터 타입과 각 타입에 대해 요청할 수 있는 필드들을 정의해 타입 시스템을 구축합니다.
  • 각 타입의 필드에 대한 요청을 해석 및 처리하는 로직은 Resolver로 구현합니다.

1-3. REST API 와의 비교


REST API 는 리소스 기반의 아키텍처입니다.

  • 각 리소스(URL 엔드포인트) 에 대해 HTTP 메서드를 이용해 CRUD 작업을 수행하고,
  • 각 리소스에 접근하기 위해서는 그에 맞는 URL 엔드포인트를 사용해야 합니다.
  • 이렇게 하면 API 의 구조를 명확하게 하고 문서화가 용이해 사용하기 쉽습니다.

  • 하지만 REST API 방식은 데이터 요구가 복잡해질 경우 여러 엔드포인트를 사용해 데이터를 집계해야하고,
  • 불필요한 데이터를 패칭하는 일이 생길 수 있습니다.

반면 GraphQL은 단일 엔드포인트를 통해 클라이언트가 필요한 데이터의 형태로 요청할 수 있습니다.


2. GraphQL의 장점

2-1. 데이터 오버패칭 및 언더패칭 해결

REST API 의 문제점인 오버패칭과 언더패칭을 해결할 수 있습니다.

오버패칭 → 클라이언트가 필요로 하는 데이터보다 더 많은 데이터를 요청하는 것
언더패칭 → 클라이언트가 필요로 하는 데이터보다 더 적은 데이터를 요청하는 것


2-2. 스키마 및 타입 시스템을 통한 데이터 검증 및 안정성 향상


GraphQL은 스키마를 통해 각 필드의 타입을 명시하며, 기본적으로 객체 형식을 사용합니다.

  • 쿼리의 형태와 응답의 형태가 거의 일치 → 서버의 API 스펙에 대해 정확히 알지 못해도 응답의 결과를 예측 가능
  • 모든 GraphQL 서비스는 쿼리 가능한 데이터들을 완벽하게 설명하는 타입들을 정의하고, 쿼리가 들어오면 해당 스키마에 대해 유효성이 검사된 후 실행됩니다.
// Request
{
  hero {
    name
    appearsIn
  }
}

// Response
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ]
    }
  }
}

객체 타입과 필드

  • GraphQL 스키마의 기본적인 구성요소는 객체 입니다.
  • 객체 타입은 서비스에서 가져올 수 있는 객체의 종류와 그 객체의 필드를 나타냅니다.
type Post {
  id: ID
  title: String
  content: String
  views: String  
  author: User
  comments: [Comment]
}
  • GraphQL의 기본 제공 스칼라 타입
    • Int: 부호가 있는 32비트 정수.
    • Float: 부호가 있는 부동소수점 값.
    • String: UTF-8 문자열.
    • Booleantrue 또는 false.
    • ID: 자주 사용되는 고유 식별자를 나타냅니다.

3. GraphQL의 단점 및 고려사항


3-1. 캐싱 복잡성

전통적인 REST API 와 비교하면 GraphQL 의 캐싱은 더 복잡합니다.

  • HTTP caching 은 메소드 별로 캐싱을 지원합니다.

  • 하지만 GraphQL 에서는 항상 POST 메서드만을 이용해 요청을 보내기 때문에 각 메소드에 따른 캐싱을 지원받을 수 없습니다.

    • 이에 대응하기 위해 GraphQL에서는 GraphQL 만의 캐싱 방식을 찾아야 합니다.

    • 대표적인 방법으로 Apollo Client 라이브러리의 cache 를 사용할 수 있습니다.


3-2. 쿼리 복잡성 및 성능 이슈


GraphQL 을 사용 함에도 불구하고 아래와 같은 문제들로 성능 문제가 발생할 수 있습니다.

  1. 오버패칭 : 클라이언트가 필요 이상으로 많은 데이터를 직접 요청할 수 있습니다.
  2. 언더패칭 : 클라이언트 측에서 필요한 데이터를 한 번에 쿼리하지 못했을 경우 필요한 데이터를 또 요청해야 합니다.
  3. 개발자가 GraphQL 쿼리를 복잡하게 만들 수도 있습니다.
post(id: $postId) {
    id
    title
    content
    views
    author {
      id
      name
      phoneNumber
    }
    comments {      // 댓글을 조회한 뒤
      id
      content
      writer {
        id
        name
        phoneNumber
      }
      post {
        id
        title
        views
        author {
          id
          name
        }
        comments {    // 게시글 조회할 때도 댓글을 중복 조회
          id
          content
          writer {
            id
            name
          }
        }
      }
    }
  }
}

3-3. 문서화의 불편함

GraphQL에서 공식적으로 지원하는 문서화 기능은 GraphiQL Docs 기능이다.

  • swagger와 비교했을 때, 모든 요청 및 응답 형태들이 매번 클릭을 해서 들어가야 보이며, 추가적인 코멘트를 작성할 수는 없어 참고하기가 불편하다.
  • example) {serverUrl}:{serverPort}/graphiql


4. 주요 개념과 특징

4-1. Query & Mutation

  • Query : 데이터를 읽기 위한 요청입니다.
    • GraphQL 을 사용하면 클라이언트는 서버에게 필요한 데이터의 구조를 명시적으로 요청하며, 서버는 해당 구조에 맞게 데이터를 응답합니다.
    • HTTP 메서드 중 GET에 해당합니다.
  • Mutation : 데이터를 생성, 수정, 삭제하기 위한 요청입니다.
    • 쿼리와 유사한 문법을 사용하지만, 서버의 데이터를 변형시킬 때 사용됩니다.
    • HTTP 메서드 중 POST, PUT, PATCH, DELETE 메서드에 해당합니다.

4-2. Schema & Type System

  • Schema

    • GraphQL에서 사용할 수 있는 데이터의 종류와 그 관계를 정의하는 문서입니다.
    • 스키마는 서버에서 지원하는 쿼리, 뮤테이션, 객체 타입등을 명시합니다.
    • Spring Boot의 경우, resources/graphql/schema.graphqls 로 작성하곤 합니다.
  • Type System

    • GraphQL 은 강력한 타입 시스템을 사용해 API 를 주고받을 데이터 형태를 정의합니다.
    • 이 시스템은 API를 더 안정적으로 만들고, 클라이언트와 서버간의 명확한 약속을 가능하게 합니다.
  • Spring Boot의 경우, 아래와 같이 스키마 파일 안에 타입을 명시할 수 있습니다.

    • 이때, viewAllPost 와 같은 명칭은 리졸버의 메서드명과 반드시 동일해야 합니다.
    • 일치하지 않을 경우 에러가 발생하지는 않지만, 리졸버를 호출하지 못해 null을 반환합니다.
type Query {
  """
  모든 게시글 조회하기
  """
  viewAllPost: [Post]
}

type Mutation {
  """
  게시글 단건 조회 (조회수 상승 로직이 있어 Mutation으로 구분)
  """
  viewPost(postId: ID): Post
  """
  게시글 저장
  """
  savePost(request: RequestPost!): Post
  """
  댓글 저장
  """
  saveComment(postId: ID, request: RequestComment!): Comment
}

4-3. 구독으로 실시간 데이터 처리

  • 구독 : 실시간으로 데이터를 받기 위한 메커니즘 입니다.
    • query와 mutation과 마찬가지로 마지막 operation type입니다.
    • GraphQL Subscription은 WebSocket을 기반으로 구현되어, 서버가 구독중인 클라이언트와 지속적인 연결을 유지
  • 실시간 채팅, 알림 시스템 등 실시간 데이터 업데이트가 필요한 애플리케이션을 구현하기 용이합니다.

4-4. 하나의 엔드포인트

  • GraphQL은 모든 쿼리와 뮤테이션, 구독 요청을 단일 엔드포인트를 통해 처리합니다.
  • 이 접근 방식은 API의 구조를 단순화하며, 클라이언트가 여러 엔드포인트를 관리하는 복잡성을 줄여줍니다.

5. 실습 (Vue.js & Spring Boot)


5-1. GraphiQL로 결과 확인하기

GraphQL은 쿼리를 확인할 수 있도록 GraphiQL이라는 GUI를 제공한다.

  • http://{serverUrl}:{serverPort}/graphiql
  • Spring Boot의 경우, application.yml에 아래와 같이 작성할 경우 GraphiQL을 활성화시킬 수 있다.
    spring:
      graphql:
        graphiql:
          enabled: true   # default : false
          path: /graphiql # default : /graphiql

5-2. 실습용 쿼리 및 뮤테이션

전체 게시글 조회 : query

query {
  viewAllPost {
    id
    title
    content
    author {
      id
      name
    }
  }
}

특정 게시글 조회 : mutation

mutation {
  viewPost(postId: 1) {
    id
    title
    content
    author {
      id
      name
    }
    comments {
      id
      content
      writer {
        id
        name
      }
    }
  }
}

게시글 작성 : mutation

mutation {
  savePost(request: {title: "hi title", content: "hi content"}) {
    title
    content
  }
}

댓글 저장 : mutation

mutation {
  saveComment(postId: 1, request: {content: "hi content"}) {
    id
    content
  }
}

6. 개인 견해

결론적으로 아래와 같은 이유로 GraphQL이 REST API를 대체하기에는 무리가 있다고 생각이 듭니다.

  • 클라이언트가 자유롭게 쿼리를 불러오도록 열어줌으로써 부작용이 발생할 수 있습니다.
    • 클라이언트가 요청을 불필요하게 무거운 쿼리를 보냄으로써 서버에 부담이 가해질 수 있습니다.
  • 마이크로 서비스(MSA)에서의 복잡성 증가
    • 백엔드 서버가 MSA인 경우 클라이언트에서 각 서비스에 API 요청을 각각 보내야 해서 또다시 언더페칭이 발생합니다.
    • 언더페칭을 해결하기 위해 복잡성을 감수하며 GraphQL을 채택했는데, 다시 언더페칭이 발생한다면 결국 복잡하다는 단점만 남게 된다고 생각합니다.
  • 하나의 엔드포인트
    • 기능별로 지연시간(letency)나 에러율 등을 모니터링하기 어려울 수 있습니다.
  • HTTP 상태 코드의 모호함
    • express-graphql에서는 요청한 데이터가 없는 경우 상태코드 500을 반환합니다.
    • Apollo Server는 순 에러인 경우에도 200을 반환합니다.
    • spring-graphql에서도 에러가 발생시, 상태 코드가 200으로 반환됩니다.


6-1. FE 측면에서의 추가 견해

  • GraphQL 을 장점 중 하나인 타입 시스템 은 이미 TypeScript 를 통해 해결할 수 있습니다.
//전체 게시글 조회 타입 정의
interface QueryResult {
  viewAllPost: Post[];
}

//게시글 타입 정의
interface Post {
  id: number;
  title: string;
  content: string;
  comments: Comment[];
}
  • 캐싱이 복잡할 뿐 더러, 성능을 위해 매우 중요한 캐싱을 서드파티 라이브러리에 의존해야 합니다.
    • 라이브러리를 사용하지 않으면 캐싱을 직접 구현해야합니다.
  • 버전 관리가 힘들 듯 합니다.
    • GraphQL 은 원하는 필드만 요청하면 됩니다. 따라서 새로운 필드나 기능이 추가되어도 기존 쿼리에 영향을 주지 않고, 클라이언트만 새로운 필드로 요청하면 됩니다
    • REST의 경우 경로에 버전(ex. api/v3)을 명시할 수 있는 반면, GraphQL은 언제 어떤 기능이 추가되었는지 추적하기 어려울 것으로 예상합니다.

6-2. BE 측면에서의 개인 견해

  • 스키마(schema.graphqls)를 작성해 구축한 타입 시스템으로 타입 안정성을 얻을 수 있다고 했지만, Long 타입을 지원하지 않는다.
    • 아래 예시에서는 게시글의 조회수를 나타내는 속성을 만들었지만, Long 타입을 지원하지 않아 String으로 명시한 상태.
    • 물론 서버에서 검증을 하면 되지만, 요청 형식에 String을 받도록 명시가 되어버리니 타입 안정성이 보장되지는 않다고 생각.
      type Post {
        id: ID
        title: String
        content: String
        views: String
        author: User
        comments: [Comment]
      }
  • 서버 입장에서 반드시 작성해야 하는 스키마 파일로 인해 휴먼 에러가 생기기 쉽다.
    • 모든 요청/응답 형태를 스키마 파일에 작성하기에는 양이 너무 많다.
    • 모든 내용은 직접 타이핑을 하는 방식이기 때문에, 실수를 불러일으키기 쉽다.
      • ex. Query 혹은 Mutation 명이 리졸버의 메서드와 다를 경우 호출되지 않으며, 에러가 발생하지는 않고 null만 리턴된다.
      • → 이는 어디서 잘못되었는지 확인하기를 어렵게 만든다.

  • 테스트 코드 작성의 불편함
    • query를 하드코딩하는 과정에서 실수를 불러일으키기 쉽습니다.
    • 또한 스펙이 바뀌었을 경우, 테스트 코드로 돌아와서 일일이 쿼리를 수정하는 수작업이 생깁니다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class PostEndpointTest {

    @Autowired
    private WebTestClient webTestClient;

    @Test
    public void viewAllPost() {

        // given
        final String requestUrl = "/graphql";
        final String query = """
                query {
                  viewAllPost {
                    id
                    title
                    comments {
                      content
                    }
                  }
                }
                """;

        // expected
        webTestClient.post()
                .uri(requestUrl)
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(query)
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .jsonPath("$.data.viewAllPost").isArray();
    }
}

7. 참고하면 좋을 이미지


8. 참고자료

profile
백엔드 개발자로 등 따숩고 배 부르게 되는 그 날까지

0개의 댓글