처음부터 차근차근

[GraphQL] GraphQL이란? 본문

CS/HTTP

[GraphQL] GraphQL이란?

HangJu_95 2023. 11. 24. 21:12
728x90

GraphQL이란?

GraphQL은 API를 위한 쿼리 언어이며 이미 존재하는 데이터로 쿼리를 수행하기 위한 런타임 입니다. GraphQL은 API에 있는 데이터에 대한 완벽하고 이해하기 쉬운 설명을 제공하고 클라이언트에게 필요한 것을 정확하게 요청할 수 있는 기능을 제공하며 시간이 지남에 따라 API를 쉽게 진화시키고 강력한 개발자 도구를 지원합니다.

 

GraphQL은 페이스북에서 만든 쿼리 언어이며, 최근 핫한 쿼리 언어 중 하나입니다.

Graph QL(이하 gql)은 SQL과 마찬가지로 쿼리 언어이다.

SQL은 데이터베이스 시스템에 저장된 데이터를 효율적으로 가져오는 것이 목적이지만, gql은 웹 클라이언트가 데이터를 서버를 효율적으로 가져오는 것이 목적이다. 즉, gql의 문장은 주로 클라이언트 시스템에서 작성하고 호출한다.

SELECT plot_id, species_id, sex, weight, ROUND(weight / 1000.0, 2) FROM surveys;

SQL 쿼리 예시

{
  hero {
    name
    friends {
      name
    }
  }
}

gql 쿼리 예시

 

서버사이드 gql App은 해당하는 쿼리를 입력받아 쿼리를 처리한 결과를 다시 클라이언트로 돌려준다.

HTTP API 자체가 특정 데이터베이스나 플랫폼에 종속적이지 않은 것처럼 gql도 종속적이지 않다. 일반적으로 gql의 인터페이스간 송수신은 네트워크 레이어 L7의 HTTP POST 메서드와 웹소켓 프로토콜을 활용하지만, 필요에 따라 L4의 TCP/UDP를 활용하거나 심지어 L2 형식도 가능하다.

GraphQL 파이프라인 https://tech.kakao.com/2019/08/01/graphql-basic/

REST API와 비교

REST API는 URL, METHOD등을 조합하기 때문에 다양한 Endpoint가 존재하지만, gql은 단 하나의 Endpoint가 존재한다.

또한 gql API에서는 불러오는 데이터의 종류를 쿼리 조합을 통해서 결정한다.

HTTP와 gql의 기술 스택 비교 https://tech.kakao.com/2019/08/01/graphql-basic/
REST API와 GraphQL API의 사용 (출처 : https://blog.apollographql.com/graphql-vs-rest-5d425123e34b)

위 그림처럼, gql API를 사용하면 여러번 네트워크 호출을 할 필요 없이, 한번의 네트워크 호출로 처리할 수 있다.

GraphQL의 구조

Query/Mutation

  • Query : 데이터를 읽는데 사용한다.
  • Mutation : 데이터를 변조(CUD) 하는데 사용한다.

이 둘은 개념적인 규약을 정해 놓은 것 뿐이다.

GraphQL 쿼리문(좌측)과 응답 데이터 형식(우측)

Query에 대해서 자세히 알아보자.

현재까지는 Query 키워드와 query이름을 모두 생략한 단축 문법을 사용했지만, 실제 애플리케이션에서는 코드를 덜 헷갈리게 작성하는 것이 좋다.

다음 예시를 한번 살펴보자.

// gql의 기본 쿼리문
{
  human(id: "1000") {
    name
    height
  }
}

// Operation name이 붙은 쿼리
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

두 쿼리문의 차이를 먼저 설명하자면 첫번째는 일반 쿼리이고, 두번째는 Operation 네임 쿼리이다.

gql에는 변수라는 개념이 있는데, 이는 REST API에서 정보를 불러올 때 id값이나 혹은 다른 인자값으로 데이터를 불러올 때 사용하는 용도와 같다.

 

오퍼레이션 네임 쿼리는 비유하자면 쿼리용 함수로 매우 편리하다.

이 개념 덕분에 REST API를 호출할 때와 다르게, 한번의 인터넷 네트워크 왕복으로 원하는 모든 데이터를 가져 올 수 있다.

Operation은 GraphQL 실행 엔진으로 해석할 수 있는 단일 쿼리(Query), Mutation 또는 Subscription을 의미한다.

아래의 그림을 통해 오퍼레이션에 대해 좀 더 자세히 알아보자면

  • 오퍼레이션 타입 : 어떤 유형의 작업을 수행하고자 하는지 표시
  • 오퍼레이션 이름 : 오퍼레이션에 이름을 부여하는 것이며, 프로그래밍 언어의 함수 이름과 유사하다.
  • 변수 정의 : 쿼리에 필요한 변수를 정의한다.

오퍼레이션 이름이 있는 쿼리 https://smoh.tistory.com/296

// 데이터를 한번에 가져오는 오퍼레이션 쿼리
query getStudentInfomation($studentId: ID){
  personalInfo(studentId: $studentId) {
    name
    address1
    address2
    major
  }
  classInfo(year: 2018, studentId: $studentId) {
    classCode
    className
    teacher {
      name
      major
    }
    classRoom {
      id
      maintainer {
        name
      }
    }
  }
  SATInfo(schoolCode: 0412, studentId: $studentId) {
    totalScore
    dueDate
  }
}

 

 

다만, 쿼리가 실행되는 순서에는 중요한 차이점이 존재하는데, Query 필드가 병렬로 실행되는 동안 Mutation 필드는 순차적으로 실행된다. 즉, 하나의 요청에서 두 개의 Mutation을 보내면 순차적으로 실행되어 Race condition이 되지 않도록 한다.

Schema/type

gql 스키마를 작성할 때 나는 DTO를 작성하는 느낌하고 비슷하게 느껴졌다.

GraphQL의 쿼리 언어는 기본적으로 객체의 필드를 선택하는 것을 알 수 있다. 

서버에 요청할 수 있는 데이터에 정확한 표현을 갖는 것이 좋기 때문에 스키마가 존재하며, 어떤 종류의 객체를 반환할 수 있는지, 하위 객체에서 사용할 수 있는 필드는 무엇인지 스키마를 통해 알 수 있다.

 

GraphQL 스키마의 가장 기본적인 구성 요소는 객체(Object) 타입입니다. 

객체 타입은 서비스에서 가져올 수 있는 객체의 종류와 그 객체의 필드를 나타냅니다. GraphQL 스키마 언어에서는 다음과 같이 표현할 수 있습니다.

type Character {
  name: String!
  appearsIn: [Episode!]!
}

위 스키마에 대해 좀 더 알아보자.

 

  • Character: GraphQL의 객체 타입. 즉, 필드가 있는 타입이 됩니다. 대부분의 스키마는 객체 타입입니다.
  • name과 appersIn: Character 타입의 필드.  name과 appearsIn은 GraphQL 쿼리의 Character 타입 어디서든 사용할 수 있는 필드가 됩니다.
  • String: 내장된 스칼라 타입 중 하나. 스칼라 객체로 해석되는 타입을 의미하며 쿼리에서 하위 선택을 할 수 없습니다.
  • !: 해당 필드가 NULL을 허용하지 않음을 의미합니다. 이 필드를 쿼리 할 때 GraphQL 서비스가 항상 값을 반환함을 의미합니다. 
  • [Episode]!: Episode 객체의 배열을 나타내며 NULL이 아님을 의미합니다. 따라서 appearIn 필드를 쿼리 하면 항상 0개 이상의 아이템을 가진 배열을 기대할 수 있습니다.

Resolver

gql에서 데이터를 가져오는 구체적인 과정은 resolver가 담당하고, 이를 직접 구현해야 한다.

프로그래머는 이를 통해서 source의 종류에 상관 없이 데이터를 가져오는 과정을 구현이 가능하다.

데이터를 데이터베이스에서 가져올 수 있고, 일반 파일에서 가져 올 수 있고, 심지어 http, SOAP와 같은 네트워크 프로토콜을 활용해서 원격 데이터를 가져올 수 있다.

이러한 특성을 이용하면 legacy 시스템을 gql 기반으로 바꾸는데 활용할 수 있다.

 

gql 쿼리에서는 각각의 필드마다 함수가 하나씩 존재 한다고 생각하면 된다. 이 함수는 다음 타입을 반환합니다. 이러한 각각의 함수를 리졸버(resolver)라고 한다. 만약 필드가 스칼라 값(문자열이나 숫자와 같은 primitive 타입)인 경우에는 실행이 종료된다. 즉 더 이상의 연쇄적인 리졸버 호출이 일어나지 않는다. 하지만 필드의 타입이 스칼라 타입이 아닌 우리가 정의한 타입이라면 해당 타입의 리졸버를 호출되게 된다.

 

이러한 연쇄적 리졸버 호출은 DFS(Depth First Search)로 구현 되어있을것으로 추측합니다. 이점이 바로 gql이 Graph라는 단어를 쓴 이유가 아닐까 생각합니다. 연쇄 리졸버 호출은 여러모로 장점이 있습니다. 연쇄 리졸버 특성을 잘 활용하면 DBMS의 관계에 대한 쿼리를 매우 쉽고, 효율적으로 처리 할 수 있습니다. 예를들어 gql의 query에서 어떤 타입의 필드 중 하나가 해당 타입과 1:n의 관계를 맺고 있다고 가정해보겠습니다.

 

한마디로 요약하자면, Resolvers는 Query에서 특정 필드에 대한 요청이 있을 때, 그것을 어떤 로직으로 처리할지 GraphQL에게 알려주는 역할을 맡는다.

{
  paymentsByUser(userId: 10) {
    id
    amount
  }
}
{
  paymentsByUser(userId: 10) {
    id
    amount
    user {
      name
      phoneNumber
    }
  }
}

위 두 쿼리는 동일한 쿼리명을 가지고 있지만, 호출 되는 리졸버 함수의 갯수는 아래가 더 많습니다. 각각의 리졸버 함수에는 내부적으로 데이터베이스 쿼리가 존재합니다. 이 말인즉, 쿼리에 맞게 필요한 만큼만 최적화하여 호출 할 수 있다는 의미입니다. 내부적으로 로직 설계를 어떻게 하느냐에 따라서 달라 질 수 있겠지만, 이러한 재귀형의 리졸버 체인을 잘 활용 한다면, 효율적인 설계가 가능 합니다. (기존에 REST API 시대에는 정해진 쿼리는 무조건 전부 호출이 되었습니다.)

 

Resolver 함수는 다음과 같이 총 4개의 인자를 받는다.

  Query: {
    paymentsByUser: async (parent, { userId }, context, info) => {
        const limit = await Limit.findOne({ where: { UserId: userId } })
        const payments = await Payment.findAll({ where: { LimitId: limit.id } })
        return payments        
    },  
  },
  Payment: {
    limit: async (payment, args, context, info) => {
      return await Limit.findOne({ where: { id: payment.LimitId } })
    }
  }
  • 첫번째 인자는 parent로 연쇄적 리졸버 호출에서 부모 리졸버가 리턴한 객체입니다. 이 객체를 활용해서 현재 리졸버가 내보낼 값을 조절 할 수 있습니다.
  • 두번째 인자는 args로 쿼리에서 입력으로 넣은 인자입니다.
  • 세번째 인자는 context로 모든 리졸버에게 전달이 됩니다. 주로 미들웨어를 통해 입력된 값들이 들어 있습니다. 로그인 정보 혹은 권한과 같이 주요 컨텍스트 관련 정보를 가지고 있습니다.
  • 네번째 인자는 info로 스키마 정보와 더불어 현재 쿼리의 특정 필드 정보를 가지고 있습니다. 잘 사용하지 않는 필드입니다.

introspection

기존 서버-클라이언트 협업 방식에서는 연동규격서라고 하는 API 명세서를 주고 받는 절차가 반드시 필요했다.

예시로 Swagger를 통해서 API 명세서를 주고 JSON으로 관리한는 방법이나, 간단하게 POSTMAN을 통해 API 명세서를 작성하는 방법이 있다.

이러한 부분은 관리해야 할 대상의 증가로 인해 작업의 복잡성 및 효율성을 저해한다. 또한 떄때로 제대로 관리가 되지 않아 인터페이스 변경 사항을 제때 문서에 반영하지 못하기도 하고, 제 타이밍에 전달 못하곤 한다.

이러한 문제를 해결해 주는 것이 gql의 인트로스펙션 기능이다. 이 기능은 서버 자체에서 현재 서버에 저으이된 스키마의 실시간 정보를 공유 할 수 있게 하며, 이 스키마 정보만 알고 있으면 클라이언트 사이드에서는 따로 연동 규격서를 요청할 필요가 없게 된다.

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

위의 화면을 참고하면, 프로그래머는 인트로스펙션을 활용하여 직접 쿼리 및 뮤테이션, 필드 스키마를 확인할 수 있다.

GraphQL을 활용 할 수 있게 도와주는 다양한 라이브러리들

gql 자체는 쿼리 언어이기 때문에 이것만으로는 할 수 있는 것이 없다. GraphQL을 사용하기 위해서는 gql 서버를 연동해야 하는데 대표적으로는 Apollo가 존재한다.

NestJS에서도 GraphQL을 사용할 때 Apollo를 권장한다.

 

 

참조

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

https://graphql-kr.github.io/

https://smoh.tistory.com/296

 

'CS > HTTP' 카테고리의 다른 글

[REST API] REST API 설계 원칙  (0) 2023.11.25
[REST API] REST API란?  (0) 2023.10.20