본문 바로가기

ComputerScience/Network

[GraphQL] GraphQL?

A query language for your API

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.


"CRUD API를 GraphQL로 새로 만들어주세요."

인턴 하면서 GraphQL에 대해 공부하여 직접 구현하고, 시니어 개발자 분들께 리뷰를 얻을 수 있는 좋은 이슈를 얻었습니다.

그냥 REST랑과 달리, 뭔가 db query 날리는 식으로 요청을 보내는 언어?로 이해하고 있었는데 참에 제대로 공부해서 기록하려 합니다. 

 

GraphQL?

GraphQL은 API를 위한 쿼리 언어이자 해당 쿼리를 기존 데이터로 처리하는 런타임입니다. 클라이언트에게 필요한 것만 요청할 수 있도록 하며 불필요한 데이터를 제거합니다. 이 말이 무슨 말인가 하면, REST API에서는 클라이언트가 필요로 하는 데이터 양보다 많거나 적은 데이터를 받을 수 있습니다. 그러나 GraphQL은 클라이언트가 필요로 하는 정확한 데이터를 쿼리로 지정하기 때문에 Over-fetching 및 Under-fetching 문제가 최소화됩니다.

이미지 출처: https://graphql.org

GraphQL 쿼리는 단일 요청으로 여러 개의 리소스를 가져올 수 있습니다. 이는 GraphQL 쿼리가 단순히 한 개의 리소스의 속성뿐만 아니라 관련된 리소스들 사이의 참조를 원활하게 따라갈 수 있기 때문입니다. 반면 일반적인 REST API는 여러 개의 URL에서 데이터를 로드해야 하는데, GraphQL API는 앱이 필요로 하는 모든 데이터를 단일 요청으로 가져올 수 있습니다. 이로 인해 GraphQL을 사용하는 앱은 느린 모바일 네트워크 연결에서도 빠르게 동작할 수 있습니다.

이미지 출처 : https://blog.apollographql.com/graphql-vs-rest-5d425123e34b

GraphQL API는 엔드포인트 대신 타입과 필드로 구성되어 있습니다.(Type System)
단일 엔드포인트에서 데이터의 모든 기능을 활용할 수 있습니다. GraphQL은 타입을 사용하여 앱이 가능한 것만 요청하도록 보장하고 명확하고 유용한 오류를 제공합니다. 앱은 타입을 활용하여 수동 파싱 코드를 작성하지 않고도 데이터를 다룰 수 있습니다.

즉, GraphQL의 타입 시스템을 통해 API는 강력하고 유연한 구조로 설계되며, 개발자는 타입을 통해 요청할 수 있는 데이터의 범위를 정확히 이해할 수 있습니다. 이는 앱과 API 간의 커뮤니케이션을 더욱 효율적으로 만들어주며, 개발자가 복잡한 데이터 파싱과 처리 코드를 작성하지 않아도 되도록 도와줍니다. 이러한 특징으로 인해 GraphQL은 효율적이고 유연한 API를 구축하는 데에 큰 도움이 됩니다.

이미지 출처: https://graphql.org

GraphQL? SQL? 

둘 다 쿼리 언어라는 공통점이 있지만 구조, 방식은 완전히 다릅니다.

SQL은 데이터베이스 시스템에 저장된 데이터를 가져오기 위함, GraphQL은 웹 클라이언트가 데이터를 요청하기 위함입니다.

 

SQL문 예시

SELECT name FROM hero;

 

GraphQL문 예시

{
  hero {
    name
  }
}


GraphQL? REST API?

REST와 GraphQL은 서버에 데이터 쓰기 작업을 수행하는 방식에 차이가 있습니다. REST는 HTTP 메서드를 사용하여 데이터 쓰기 작업을 정의하고, 각 작업에 대한 엔드포인트를 구분합니다. 반면에 GraphQL은 하나의 엔드포인트를 사용하여 데이터 쓰기 작업을 수행합니다. 따라서 GraphQL에서는 뮤테이션을 사용하여 데이터를 수정하고, 쿼리를 사용하여 데이터를 읽는 것이 관례적으로 권장됩니다.

왼쪽: id가 1000인 name, height를 요청하는 GraphQL 오른쪽: 실제 응답받은 JSON 데이터

Operation name

작업 이름은 작업의 의미 있는 명시적인 이름입니다. 

이는 다중 작업 문서에서만 필수이지만, 디버깅 및 서버 측 로깅에 매우 유용하므로 사용을 권장합니다. 

작업 유형은 query, mutation 또는 subscription 중 하나이며 의도한 작업 유형을 설명합니다.

다음은 query 키워드를 작업 유형으로, HeroNameAndFriends를 작업 이름으로 포함하는 예제입니다:

Mutation

뮤테이션이란, 데이터를 변경하거나 생성하는데 사용되는 구조입니다. 쿼리와 마찬가지로 필드와 인자들을 포함하며, 서버에 변경 사항을 요청합니다. REST에서는 어떤 요청이든 서버에 일부 부작용을 일으킬 수 있지만, 관례적으로 GET 요청을 사용하여 데이터를 수정하지 않도록 권장합니다. GraphQL도 비슷합니다. 기술적으로 모든 쿼리는 데이터 쓰기를 일으킬 수 있습니다. 하지만 데이터를 수정하는 모든 작업은 명시적으로 뮤테이션을 통해 보내는 것이 유용합니다.

 

Validation

GraphQL의 타입 시스템을 사용하면 GraphQL 쿼리가 유효한지 사전에 확인할 수 있습니다. 이를 통해 서버와 클라이언트는 런타임 체크 없이도 개발자에게 잘못된 쿼리가 생성되었음을 효과적으로 알릴 수 있습니다.

예를 들어,
우리가 필드를 쿼리하고 그 필드가 스칼라나 이넘(enum)이 아닌 다른 데이터를 반환하는 경우, 해당 필드에서 어떤 데이터를 가져오길 원하는지 지정해야 합니다. "Hero" 필드는 Character를 반환하며, name과 같은 필드들을 생략하면 쿼리는 유효하지 않을 것입니다:

{
  hero #wrong query
}
{
  "errors": [
    {
      "message": "Field \"hero\" of type \"Character\" must have a selection of subfields. Did you mean \"hero { ... }\"?",
      "locations": [
        {
          "line": 3,
          "column": 3
        }
      ]
    }
  ]
}


따라서 GraphQL에서는 쿼리를 작성할 때 반환되는 데이터의 구조를 명확히 지정해주어야 합니다. 원하는 데이터의 필드를 명시적으로 쿼리 하여 유효한 쿼리를 작성하는 것이 중요합니다. 그렇지 않으면 GraphQL 서버가 유효하지 않은 쿼리임을 감지하고 오류를 반환할 수 있습니다.
이러한 방식으로 GraphQL은 유효한 쿼리만 실행되도록 보장하고, 런타임 에러를 최소화하여 안정적인 개발을 지원합니다.


Resolver

유효성을 검사한 후에는 GraphQL 쿼리가 GraphQL 서버에 의해 실행되며, 일반적으로 JSON 형식으로 요청한 쿼리와 동일한 결과를 반환합니다.

GraphQL 쿼리에서는 각각의 필드마다 함수가 하나씩 존재 한다고 생각하면 됩니다.
이러한 각각의 함수를 리졸버(resolver)라고 합니다. 리졸버는 GraphQL 서버 개발자가 구현해야 합니다.

필드가 실행되면 해당 리졸버가 호출되어 다음 값을 생성합니다.

만약 필드가 문자열이나 숫자와 같은 스칼라 값을 생성한다면, 실행은 완료됩니다. 그러나 필드가 객체 값을 생성한다면, 쿼리는 해당 객체에 적용되는 다른 필드들의 선택(selection)을 포함할 것입니다. 이는 스칼라 값이 도달될 때까지 반복됩니다. GraphQL 쿼리는 항상 스칼라 값에서 끝납니다.

간단히 말하면, GraphQL 쿼리는 타입 간의 필드 함수 호출을 통해 객체 간의 관계를 탐색하는 것으로 볼 수 있습니다. (더 복잡한가요..?) 이를 통해 GraphQL은 요청된 필드와 관련된 데이터를 효율적으로 가져오고, 객체 간의 복잡한 관계를 쉽게 다룰 수 있게 됩니다. 리졸버 함수는 필드와 데이터 소스 간의 인터페이스 역할을 하며, 이를 통해 GraphQL 서버는 요청에 대한 적절한 데이터를 반환하는 기능을 구현할 수 있습니다.

자바 스크립트로 작성된 resolver 함수 예제

Query: {
  human(obj, args, context, info) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
}

 

이 예제에서는 Query 타입이라고 불리는 타입이 "human"이라는 필드를 제공합니다. 이 필드는 인자로 "id"를 받습니다. 이 필드에 대한 리졸버 함수는 아마도 데이터베이스에 접근하고, 그 데이터를 사용하여 Human 객체를 구성하여 반환할 것입니다.

예제 코드는 JavaScript로 작성되었지만, GraphQL 서버는 여러 가지 다른 언어로 구축할 수 있습니다. 리졸버 함수는 네 개의 인자를 받습니다:

1. obj: 이전 객체로, 보통 Query 타입의 필드에서 사용되지 않습니다.
2. args: GraphQL 쿼리에서 필드에 제공된 인자들입니다.
3. context: 모든 리졸버에 제공되는 값으로, 현재 로그인한 사용자와 데이터베이스 접근과 같은 중요한 문맥 정보를 포함합니다.
4. info: 현재 쿼리와 관련된 필드별 정보와 스키마 세부 정보를 포함하는 값입니다. GraphQLResolveInfo 타입과 관련하여 자세한 내용은 해당 문서를 참조하세요(https://graphql.org/graphql-js/type/#graphqlobjecttype)

이를 통해 GraphQL 서버는 쿼리에 따른 적절한 데이터를 반환하기 위해 리졸버 함수를 활용하며, 필요한 데이터를 가져와서 쿼리 결과를 구성합니다. 각 리졸버 함수는 해당 필드와 데이터 소스 사이의 인터페이스 역할을 하고, 쿼리의 진입점과 데이터베이스 및 다른 서비스 간의 상호 작용을 조율합니다.


참고 자료
https://graphql.org/learn/execution/

 

Execution | GraphQL

Execution After being validated, a GraphQL query is executed by a GraphQL server which returns a result that mirrors the shape of the requested query, typically as JSON. GraphQL cannot execute a query without a type system, let's use an example type system

graphql.org

https://tech.kakao.com/2019/08/01/graphql-basic/