게시:

GraphQL logo

GraphQL은 페이스북이 2012년에 개발하여 2015년에 발표한 API용 Query language이다.

Query Language라는 워딩 때문에 데이터베이스용 쿼리 언어나 엔진정도로 착각하기 십상이지만 그렇지 않다. GraphQL은 사전 정의된 데이터 스키마에 대한 쿼리를 실행하기 위한 서버측 런타임이며 클라이언트-서버 통신에 사용되는 사양이다. 즉, API를 위한 사양이지 데이터베이스를 위한 것이 아니다.

따라서 데이터베이스나 스토리지 엔진에 연결되지 않는 것은 물론이고 특정 스토리지 엔진에 제약을 받지 않으며 다양한 언어(웬만한 메이저 언어는 가능)로 서비스가 가능하다.

간단한 쿼리문을 살펴보자.

example of query and response
출처: https://graphql.org

쿼리문을 실행했을 때 요청 쿼리문과 응답의 구조가 거의 일치하는 모습을 볼 수 있다. 여기서 특정 필드를 추가하면 동일한 구조로 해당 필드에 대한 하위 객체를 호출하여 응답한다. 이렇듯 GraphQL의 요청과 응답 구조는 매우 직관적이다.

GraphQL을 사용하면 미리 정의된 스키마(Schema)에 대해서 질의 가능한 쿼리로 요청 시 단일 요청만으로도 필요한 데이터를 획득할 수 있게 된다. 즉, 각 자원(Resource)별로 미리 정의된 엔드포인트와 정의된 메서드대로 요청해야 하는 REST API와 달리 GraphQL은 하나의 엔드포인트로(/graphql) 사전 정의된 스키마 안에서만 요청한다면 클라이언트가 원하는 데이터를 선택적으로 요청할 수 있는 것이다.

이러한 특징으로 인하여 클라이언트에서 여러 번의 네트워크를 호출해야 하는 상황을 피할 수 있어 다중 요청에 대한 클라이언트 복잡성을 줄일 수 있고, 자원의 부분 정보만 필요한 경우 과도한 많은 데이터를 불러올 필요없이 원하는 데이터에 대해서만 엑세스할 수 있게 되어 클라이언트 입장에서 요청이 간결해진다.

GitHub 사용자라면 다음 링크를 통하여 GraphQL을 체험할 수 있다.

GitHub GraphQL

REST API vs GraphQL

REST API와 GraphQL는 API용 사양이라는 점에서 공통점을 갖지만 그 외에는 거의 대척점에 있다고 할만큼 큰 차이가 있다. REST API는 URI마다 다양한 Method를 지원하고 정해진 요청・응답방식을 갖는 반면 GraphQL은 단일 엔드포인트에 POST 요청만 할 수 있고 요청・응답형태는 클라이언트의 선택에 따라 달라진다.

Comparison of REST API and GraphQL
Comparison of REST API and GraphQL

이토록 큰 차이점을 만들어내는 이유는 REST API는 자원(Resource)을 중심으로 엔드포인트와 Method를 정의하는 반면, GraphQL은 데이터 따로 액세스 방법 따로 분리하여 정의한다는 점에서 기인되었다고 본다.

다음 표는 REST API와 GraphQL의 차이점을 정리한 표이다.

 
REST API
GraphQL
Topic
URI (Uniform Resource Interface)
Schema
Endpoint
Each Resources (n개)
1개
Method 각 URI마다 개별적으로 정의됨 (GET, POST, PUT, DELETE, OPTION, HEAD, e.t.c.)
POST
Design Resource에 대하여 요청 방법이 연결됨 스키마(Object type)와 요청형태(Query/Mutation type)를 개별적으로 정의함
Request 각 URI에 정의된 방법(Method)으로 요청. 분산된 자료 요청 시 여러 엔드포인트에 요청. 단일 엔드포인트에 쿼리를 만들어서 요청. 한 번의 네트워크 호출로 처리 가능.
Response 미리 정해진 데이터 형태로 응답(캐싱 용이). 형태 예측 불가 (문서화가 안되어 있는 경우) 요청 쿼리 형태에 맞추어 응답 반환. 형태 예측 가능
Execution 엔드포인트에 정의된 핸들링 함수를 호출하여 수행 각 필드에 대한 리졸버(Resolver)를 호출하여 수행

구조

GraphQL 공식 페이지의 설명에 따르면 모든 GraphQL 서비스는 해당 서비스에서 쿼리할 수 있는 가능한 데이터 세트를 완전히 설명하는 타입 세트(Type set)를 정의한다. 라고 설명되어 있다. 이는 GraphQL 서비스는 쿼리 가능한 데이터 세트인 스키마(Schema)를 type으로 정의한 구조를 갖는다는 의미이다.

스키마(Schema)

스키마는 GraphQL 서비스의 데이터 세트를 설명하는 타입 세트를 정의하는 일종의 클래스를 말한다. 즉, 어떤 필드(Field)를 선택할 수 있는지, 어떤 아이템을 반환할 수 있는지, 하위 객체에서 사용할 수 있는 필드는 무엇인지에 대하여 스키마를 통하여 정의한다.

스키마의 가장 기본적인 구성 요소는 서비스에서 가져올 수 있는 객체(Object)의 종류와 포함하는 필드(Field)를 나타내는 객체 유형(object type)이다. 다음 코드 스니펫은 간단한 스키마를 나타낸다.

type Character {
  name: String!
  appearsIn: [Episode!]!
}
  • Character는 GraphQL의 object type으로 일부 필드가 포함된 type이다. 스키마에 포함되는 대부분의 type은 일반적인 obeject type인 경우가 많다.
  • name, appearsInCharacter type의 필드(Field)이다. 즉, Character type에서 작동하는 GraphQL에서 나타날 수 있는 유일한 필드임을 의미한다.
  • String은 기본 Scalar type(≒ Data type) 중 하나이다. 스칼라 단일 객체로 해석되는 type으로 더 이상 하위 선택을 가질 수 없음을 의미한다.
  • String!name 필드가 Non-nullable(이하 not null) 임을 의미한다. 즉, GraphQL 서비스는 이 필드를 쿼리할 때 항상 값을 제공한다고 약속한다.
  • [Episode!]!Episode object의 배열을 나타낸다. 또한 not null이기 때문에 appearsIn 필드를 쿼리할 때 항상 배열(0개 이상의 항목을 포함한)을 기대할 수 있다. 또한 Eposiode!도 nn이므로 배열의 모든 요소가 Episode object일 것이라고 기대할 수 있다.

스키마가 정의된 다음 서비스에 쿼리가 들어오면 미리 정의된 스키마에 대하여 유효성(정의된 타입과 필드만 참조하였는지)이 검사되고 실행된다.

Query Type & Mutation Type

방금 스키마에서 다룬 object type에도 type이라는 단어가 들어갔고 여기서도 query type, mutation type을 다룬다. 앞서 GraphQL을 설명할 때 type set라고 설명한 부분을 기억하는가? 그렇다. GraphQL은 type system으로 작성한다.

query typemutation typeobject type처럼 type으로 정의된 스키마이다. 상호간 형태가 동일한 것은 물론 구조적으로 object type과도 동일하지만 사용할 용도가 다르기 떄문에 순전히 용도에 따라서 GraphQL 내부적으로 분리하여 규정한 개념이다.

다만 query type은 데이터를 접근하고 읽는데(Read) 사용하고, mutation type은 데이터를 조작하는데(Create, Update, Delete) 사용할 뿐이다. 다음 코드 스니펫은 간단한 query type 예시이다.

type Qeury {
  user(id: ID!): User
}

type User {
  id: ID!,
  name: String!,
  age: Int,
  height: Float,
  pet: [Pet]!
}

type Pet {
  name: String!,
  species: String!,
  age: Int,

진입점(entry point)으로써 특수한 타입인 Query 타입이 정의되어 있고 User 타입과 Pet 타입이 정의되어 있다. 쿼리 진입점으로 user 필드를 선택하면 User 타입과 관련된 필드를 선택할 수 있고 그중 pet 필드를 선택하면 관련된 Pet 객체가 호출되는 구조라는 것을 알 수 있다.

다음 코드 스니펫은 간단한 질의문의 구조이다.

query operationName($variableName: dataType = "defaultValue") {
  objectFieldName(argumentName: $variableName) {
    fieldName1
    fieldName2
    fieldName3(first: 10) {
      fieldName1
  }

각 요소를 하나씩 살펴보면 다음과 같다.

  • query는 질의문임을 나타낸다. mutation type인 경우 mutation이라고 표기한다. 뒤에 따라붙는 operationName 없이 축약형 구문으로 query만 선언하여도 된다. 간단한 쿼리의 경우 문제없이 동작한다.
  • operationName은 작업 이름(Operation name)으로 쿼리에 대한 의미있고 명시적인 이름이다. 디버깅 및 로깅에 유용하므로 사용하는 것이 좋다. 이는 Python이나 Javascript 프로그래밍 언어의 함수 이름으로 비유할 수 있다. 동적 매개변수를 사용하려면 필수적으로 사용해야 한다.
  • $variableName은 매개변수를 나타낸다. 해당 변수를 하위 필드의 인수로 전달할 수 있다. 변수명 앞에 $(Dollar sign)이 붙으면 동적 매개변수임을 의미한다. 이러한 경우 클라이언트가 GraphQL에 동적인 매개변수를 전달할 수 있다.
  • dataType은 변수에 대한 Scalar type이다. Scalar type에 대해서는 아래에서 다루겠다.
  • "defaultValue"는 변수에 대한 기본 값이다.
  • objectFieldName은 쿼리할 객체의 필드 이름이다. GraphQL은 어떠한 필드가 Scalar type을 반환하기 전까지 하위 필드를 계속 순회하기 때문에 쿼리 구문 상위의 객체라고 할지라도 필드라고 명명한 것이라 생각된다. 스키마에서 정의한 type에 해당된다.
  • argumentName은 필드에 전달하는 인수를 나타낸다. 모든 필드와 nested object가 고유한 인수를 취할 수 있다. 기본값을 줄 수도 있고 동적 매개변수를 취할 수도 있다.
  • $variableName은 해당 필드에서 취할 매개변수를 나타낸다.
  • fieldName1~3은 쿼리할 대상 필드를 나타낸다. 인수를 가질 수 있다.
  • fieldName3의 쿼리 결과가 만약 다수의 하위 필드라면 결과값이 배열 형태로 나타난다.

Scalar Type

GraphQL 객체는 하위 필드를 순회하다가 언젠가는 구체적인 데이터로 표현되어야 하는데 바로 Scalar type을 만나면 그렇게 된다. 기본적으로 제공되는 Scalar type은 다음과 같다.

  • Int: 부호가 있는(Signed) 32비트 정수이다.
  • Float: 부호가 있는(Signed) 배정밀도 부동 소수점이다.
  • String: UTF-8 문자 시퀀스이다.
  • Boolean: true 또는 false
  • ID: 고유 식별자이다. 객체를 다시 가져오거나 캐시의 키로 자주 사용된다고 한다.

Resolver

GraphQL 서비스에 쿼리가 수신되면 쿼리문은 서버의 GraphQL 라이브러리에서 Parsing되고 정의된 type과 field만 참조했는지 유효성 검사 후 요청한 쿼리 모양을 미러링한 결과를 반환하게 된다. 이때 데이터를 가져오는 구체적인 역할을 Resolver가 담당하게 된다.

Resolver는 각 타입의 각 필드를 처리하기 위하여 따로 구현한 함수와 같다. 이것은 Scalar 값이 나타날 때가지 계속 수행된다. 각각의 필드마다 Resolver라는 함수가 있다고 보면 된다.

쿼리가 수신되고 필드가 실행되면 해당 Resolver가 실행되고, 필드에 숫자(Int)나 문자열(String) 같은 Scalar 값이 나타나면 수행이 완료된 것으로 하고, 그렇지 않으면 해당 객체의 다음 필드가 선택된다. 즉, 하위 필드가 Scalar 값에 도달할 때 까지 계속 해당 type의 Resolver를 연쇄적으로 호출하는 것이다.

다음은 Python 웹 프레임워크인 Django와 GraphQL 라이브러리인 graphene을 활용하여 스키마와 Resolver 함수를 간단하게 구현한 예시이다.

#../linkapp/schema.py

import graphene
from graphene_django import DjangoObjectType

from .model import Link


class LinkType(DjangoObjectType):
    class Meta:
        model = Link


class Query(graphene.ObjectType):
    links = graphene.List(LinkType)

    def resolve_links(self, info, **kwargs):
        return Link.objects.all()

코드 스니펫을 살펴보면 정의된 모델을 Django-graphene에서 사용할 수 있는DjangoObjectType을 사용하여 type으로 선언하고 모든 Link를 반환하는 links 필드에 대한 resolve_links가 포함된 query type이 선언되었다. 지정된 Method에 정해진 응답을 반환하는 REST API와 달리 links 라는 필드에 대응하는 resolver 함수를 제공하는 방식임을 알 수 있다.

각 Resolver 함수에는 내부적으로 데이터베이스 구문(예시에서는 Django ORM)이 포함되어 있다. 따라서 이러한 Recursive한 호출 방식을 잘 이해하고 사용하면 효율적인 디자인이 가능하다.

Introspection

종래의 클라이언트-서버 협업 방식에서는 요청과 응답에 대한 API 명세서를 문서화 해놓는 작업이 필요했다. 명세서는 종종 버전 관리가 제대로 되지 않아 인터페이스 변경 사항을 제때 반영하지 못하기도 하고 관리가 잘된다고 하더라도 수많은 문서를 관리하는 자체가 하나의 Task가 되기 때문에 개발 본연의 업무에 대한 생산성 내지 효율성을 저해하곤 한다.

Introspection 기능은 이러한 명세서에 대한 문제를 해결한다. Introspection을 사용하면 서버에 정의된 스키마의 정보를 실시간으로 알 수 있다. 따라서 클라이언트-서버간의 별도의 명세서를 두고 관리할 필요없이 실시간으로 현재 정의된 스키마 정보를 확인하고 그에 따라 쿼리하여 프로젝트의 효율성을 높일 수 있다.

간단한 Introspection 사용 예시를 살펴보며 이상으로 본 포스팅을 마친다.

  • Request
    {
    __schema {
      types {
        name
      }
    }
    }
    
  • Response
    {
    "data": {
      "__schema": {
        "types": [
          {
            "name": "Query"
          },
          {
            "name": "String"
          }
          (...중략...)
        ]
      }
    }
    }
    

Reference

GraphQL 공식 페이지, https://graphql.org/
HOW TO GRAPHQL, https://www.howtographql.com/

댓글남기기