GraphQL 이란 ?

mallin·2022년 9월 23일
6

GraphQL

목록 보기
1/1
post-thumbnail
post-custom-banner

백엔드 개발을 하면서 숙명적으로 할 수 밖에 없는게 API 설계다.
보통은 API 를 설계할 때 REST API 를 많이 사용했다.
사용한 이유는 별로 없고, 통상적으로 사용되기 때문이였다.

근데 이번에 회사에서 새로운 프로젝트를 들어가면서 GraphQL 을 사용하게 되었다.

오잉 ? GraphQL ? 그게 뭐지 ??
https://media.giphy.com/media/xT0xeuOy2Fcl9vDGiA/giphy.gif

처음들었을 때에는 회사 분들이 자꾸 그래프 라고 하셔서 무슨 수학적인 건 줄 알았지만
사용하기로 했으니 GraphQL에 대해서 뿌셔보자 !

GraphQL 이란 ?

GraphQL 은 한 문장으로 얘기하자면 페이스북에서 만든 API 를 위한 쿼리 언어 라고 할 수 있다.

쿼리 언어라는 말이 나왔으니깐 같은 쿼리 언어인 SQL (Structed Query Language) 비교해볼 수 있는데 둘은 목적에서부터 다르다.

GraphQL 은 웹 클라이언트가 데이터를 서버로부터 효율적으로 가져오는 것이 목적인 쿼리 언어이고,
SQL 은 데이터베이스 시스템에 저장된 데이터를 효율적으로 가져오는 것이 목적인 쿼리 언어이다.

GraphQL 은 쿼리언어이기 때문에 전통적인

{
  project(name: "GraphQL") {
    tagline
  }
}

또한

  • 타입 시스템을 사용하여 쿼리를 실행하는 서버사이드 런타임이고,
  • 특정 데이터베이스나 플랫폼에 종속적이지 않다.

GraphQL 의 파이프라인 은 다음과 같다.

GraphQL Query
⬇️
Query Language Processor (Parse, Validate)
⬇️
GraphQL Resolver (implemented by you) : RDB / NoSQL
⬇️
Output (JSON)

vs REST API

① URL 엔드포인트
REST API 는 URL + METHOD 를 조합하기 때문에 정말 다양한 엔드포인트가 존재하지만,
GraphQL 은 엔드포인트가 하나만 존재한다. (/graphql)

하나의 엔드포인트를 가지고, 쿼리 조합을 통해 데이터를 요청한다.

EX) Book, Library 에 대한 API 를 설계할 때

REST API 의 경우
/library/book
/library
/library/book/{id}
처럼 설계해야 하지만

GrahpQL 의 경우에는 /graphql 엔드포인트에 요청에 따라 쿼리로 조합해서 요청하면 되기 때문에 API 가 점점 늘어남에 따라 URL 을 설계하고 만들어주지 않아도 된다.

query {
	library {
    	book {
        	name
        }
   	}
}

② response 데이터
REST API 의 경우 return 되는 response 데이터가 정해져 있다.
그렇기 때문에 이전 API response 에 다른 컬럼들이 추가되면 해당 파일을 계속 수정해줘야하고, API 별로 response 가 다 다르면 그에 따른 DTO 를 각각 다 만들어줘야 한다.

하지만, GraphQL 은 쿼리로 요청하기 때문에 클라이언트가 원하는 데이터를 쏙쏙 뽑아 커스터마이징 해서 사용할 수 있다 !

EX) 전체 책을 가져오는 API 에서는 id, title 만 return 하고 아이디별 책을 가져오는 API 에서는 id, title, content 를 가져와야 할 때

REST API 의 경우

class BookDTO {
	Long id;
    String title;
}

class BookDTO2 {
	Long id;
    String title;
    String content;
}

이렇게 DTO 를 2개 만들고 return 해주거나 ... 아니면 다른 방식을 사용해야하는다.

하지만, Graphl 에서는 Book 모델에 데이터가 있으면 클라이언트에서 커스터마이징하게 요청해서 사용할 수 있다

GraphQL 구조

GraphQL 은 객체에 대한 특정 필드를 매우 간단하게 요청할 수 있다.

{
	hero {
    	name
	}
}

이 쿼리를 간단하게 설명하자면 hero 객체에 대해서 name 필드 데이터를 리턴해줘라 인데 이렇게 요청하면 아래과 같은 결과가 나오게 된다.

{
	"data": {
    	"hero": {
        	"name": "R2-D2"
		}
	}
}

graphql 의 특징 중 하나인데 결과는 요청한 양식과 동일하게 리턴된다.

쿼리와 뮤테이션 (query / mutation)

GrahpQL 은 크게 쿼리(query) 와 뮤테이션(mutation)으로 나눌 수 있다.
간단하게 쿼리(query) 는 조회시 사용하고, 뮤테이션(mutatiton) 은 조회 외 수정 / 삭제 / 생성 에 사용된다.

위에서 있는 예제들에서는 생략했지만 원래 { 앞에 query 가 존재한다.
필요에 따라 query 를 생략하거나 작성할 수 있다.

query 를 생략한 경우 👇

{
	"data": {}
}

query 를 생략하지 않은 경우 👇

query {
	"data": {}
}

필드

위에서 얘기가 나왔던 것 처럼 GraphQL 의 경우 객체에 대한 특정 필드를 요청하는 것이 매우 간단하다.

쿼리와 결과는 정확히 동일한 형태라 항상 기대한 결과를 얻을 수 있다.

hero 의 name 필드가 보고 싶으면 아래 처럼 코드를 구현하면 되고
추가적으로 보고 싶은 데이터가 있는 경우에는 계속해서 name 밑에 필드를 더해주면 된다.

{
	hero {
    	name 
        # 보고 싶은 필드를 계속 추가하거나 삭제하면 됨 
	}
}

하위 객체인 경우에도 원하는 컬럼을 선택할 수 있는데 그럴 때에는 아래와 같이 사용하면 된다 !

{
	hero {
    	name
        friends {
        	name
        }
    }
}

인자

GraphQL 은 원하는 값을 가져오기 위해서 인자를 넘길 수도 있다.
REST 와 같은 시스템에서는 쿼리 파라미터와 URL 세그먼트 같은 단일 인자들만 전달할 수 있지만, GraphQL 에서는 모든 객체가 인자를 가질 수 있기 때문에 더욱 간편하게 인자를 주고 받을 수 있다.

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}

인자는 다양한 타입으로 요청할 수 있다.

별칭

데이터를 조회할 때 필드의 결과를 별칭을 사용해서 원하는 이름으로 바꿀 수 있다.
예를 들어서 결과 객체 필드와 쿼리의 필드 이름은 일치하지만 인자가 다른 데이터를 쿼리하려고 할 때 사용할 수 있다.

graphql query 👇

{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}

result 👇

{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

프래그먼트

보통 자바를 예시로 들었을 때 공통되는 코드가 발생하면 캡슐화 한다.
이와 비슷하게 graphql 에서는 공통되는 쿼리가 있을 때 프래그먼트를 사용할 수 있다.
간단하게 프래그먼트는 재사용 가능한 단위다 라고 생각하면 된다 !

다음은 프래그먼트를 사용한 예시다.

{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}

select 하는 필드가 동일 하기 때문에 해당 코드를 comparisonFields fragment 빼서 재사용가능하도록 했다.

또한, 쿼리나 뮤테이션에 선언된 변수는 프래그먼트에 접근할 수 있다.
예시 코드는 다음과 같다. 👇

query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}

작업 이름

지금까지는 query 키워드 / 이름을 생략한 단축 문법을 사용했지만, 생략하지 않을 수 있다.
그냥 앞에 query 키워드와 이름을 작성해주면 된다.

query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}

변수

지금까지는 모든 인자를 쿼리 문자열 안에 작성했지만, 보통의 응용프로그램의 경우 필드에 대한 인자는 동적으로 작용한다.
EX) 이름 검색하기 등등 ..

GraphQL 은 동적 값을 쿼리에서 없애고, 이를 별도로 전달하는 방법을 제공하는데 이러한 값을 변수 라고 한다

변수를 사용하기 위해서는 아래와 같은 세 가지 작업이 필요하다.

  1. 쿼리안의 정적 값을 $variableName 으로 변경한다.
  2. $variableName 를 쿼리에서 받는 변수로 선언한다.
  3. 별도의 전송규약 변수에 variableName: value 를 전달한다.
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

아래 값은 별도의 전송규약 (JSON) 변수로 넘겨주면 된다.

{
  "episode": "JEDI"
}

지시어

인자에 변수를 전달하면 이러한 문제를 상당히 해결할 수 있지만, 변수를 사용하여 쿼리의 구조와 형태를 동적으로 변경하는 방법이 필요할 수 있다.

이럴 때 지시어를 사용한다.

query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}

@include(if: Boolean) : 인자가 true 인 경우에만 이 필드를 결과에 포함
@skip(if: Boolean) : 인자가 true 이면 이 필드를 건너뜀

뮤테이션

지금까지의 이야기는 데이터를 가져오는 것에만 초점을 맞추어서 설명했지만, 이제 부터는 데이터를 수정하는 방식인 뮤테이션(mutation) 에 대해서 알아보자 !

query 와 별로 달라진 건 없고 앞에 명령어를 query 가 아니라 mutation 으로 명시하면 된다.

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

넘겨주는 variable

{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

쿼리와 뮤테이션의 중요한 차이점이 있는데,
쿼리 필드는 병렬로 실행되지만, 뮤테이션 필드는 하나씩 차례대로 실행된다.

인라인 프래그먼트

인터페이스나 유니언 타입을 반환하는 필드를 쿼리하는 경우에는 인라인 프래그먼트를 사용해야 한다.

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}

위 예제의 경우에는
name 이 Droid 면 primaryFunction 필드 조회,
name 이 Human 이면 height 필드 조회 하는 쿼리다.

넘겨주는 variable

{
  "ep": "JEDI"
}

스키마와 타입 (schema / type)

{
  hero {
    name
    appearsIn
  }
}

위 쿼리 예제는 아래와 같은 순서로 실행된다.
1. root 객체로 시작
2. hero 필드를 선택
3. hero 에 의해 반환된 객체에 대해 name, apperasIn 필드를 선택

어떤 필드를 선택할 수 있는지, 어떤 종류의 객체를 반환할 수 있는지를 알고 있어야 하기 때문에 스키마가 필요하다.

모든 GraphQL 서비스는 해당 서비스에서 쿼리 가능한 데이터들을 완벽하게 설명하는 타입들을 정의하고, 쿼리가 들어오면 해당 스키마에 대해 유효성 검사를 한 후 실행된다.

객체 타입과 필드

GraphQL 스키마의 가장 기본적인 구성 요소는 객체 타입이다.
객체 타입은 서비스에서 가져올 수 있는 객체의 종류와 그 객체의 필드를 나타낸다.

type Character {
  name: String!
  appearsIn: [Episode]!
}
  • Character 는 GrahpQL 객체 타입
  • name 과 apperesIn 은 Character 타입의 필드
  • String 은 내장된 스칼라 타입 중 하나
  • ! 은 필드가 non-nullable 임을 의미

인자

모든 필드는 0개 이상의 인수를 가질 수 있다.

type Starship {
  id: ID!
  name: String!
  length(unit: LengthUnit = METER): Float
}
  • 모든 인수에는 이름이 있다.
  • GraphQL 의 모든 인자는 특별한 이름으로 전달된다.
  • 위의 예시의 경우 length 필드는 하나의 인자를 가진다.
  • 인자는 optional 이거나 required 일 수 있다.

스칼라 타입

GraphQL 객체 타입은 이름과 필드를 가지지만, 이 필드는 구체적인 데이터로 해석되어야 한다.
스칼라 타입은 쿼리의 끝을 나타낸다.

아래는 기본 제공되는 스칼라 타입들이다.

타입설명
Int부호가 있는 32비트 정수
Float부호가 있는 부동소수점 값.
StringUTF-8 문자열.
Booleantrue or false
IDID 스칼라 타입은 객체를 다시 요청하거나 캐시의 키로써 자주 사용되는 고유 식별자를 나타냅니다. ID 타입은 String 과 같은 방법으로 직렬화되지만, ID 로 정의하는 것은 사람이 읽을 수 있도록 하는 의도가 아니라는 것을 의미

커스텀 스칼라 타입은 scalar Date 이렇게 지정할 수 있다.

열거형 타입

Enums 라고도 하는 열거형 타입은 특정 값들로 제한되는 특별한 종류의 스칼라이다.
이를 통해 아래와 같은 작업들을 할 수 있다.

  1. 타입의 인자가 허용된 값 중 하나임을 검증한다.
  2. 필드가 항상 값의 열거형 집합 중 하나가 될 것임을 타입 시스템을 통해 의사소통 한다.

GraphQL 스키마 언어에서 열거형 타입 정의는 아래처럼 정의할 수 있다.

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

리스트와 Non-Null

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

String 타입을 사용하고 타입 뒤에 ! 를 추가하여 Non-Null 로 표시했다.
즉, 서버는 항상 이 필드에 대해 null 이 아닌 값을 반환할 것을 기대하며, null 값이 발생되면 GraphQL 실행 오류가 발생한다.

{
  "errors": [
    {
      "message": "Variable \"$id\" of required type \"ID!\" was not provided.",
      "locations": [
        {
          "line": 1,
          "column": 17
        }
      ]
    }
  ]
}

타입을 대괄호 ([]) 로 묶는 것으로 표현으로 list 를 표현한다.

myField: [String!]

myField: [String!] : list 는 null 일 수 있지만, null 을 가질 수 없다.
myField: [String]! : list 는 null 일 수 없지만, null 값을 포함할 수 있다.

인터페이스

구현하기 위해 타입이 포함해야 하는 특정 필드들을 포함하는 추상 타입을 인터페이스라고 한다.

예를 들어서 모든 캐릭터는 id, name, friends, appearsIn 필드를 가진다고 했을 때 아래와 같이 구현할 수 있다.

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

Character 인터페이스를 하나 정의해놓고, 그걸 구현하는 구현체로 Human, Droid 를 가질 수 있다.

하지만 다음처럼 호출하면 오류가 발생한다.

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    primaryFunction
  }
}

Human 의 경우에는 primaryFunction 가 없기 때문에 오류가 발생하게 된다.
그렇기 때문에 특정 객체 타입의 필드를 요청하려면 위에서 배웄던 인라인 프래그먼트를 사용해야 한다.

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
  }
}

입력타입

인자로 스칼라 값만 넘기는 게 아니라 복잡한 객체도 쉽게 전달 할 수 있다.
특히 뮤테이션에서 매우매우 유용하다.
입력 타입은 type 대신 input 을 사용한다.

input ReviewInput {
  stars: Int!
  commentary: String
}

다음은 뮤테이션에서 입력 객체 타입을 사용하는 방법이다.

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

리졸버

타입의 각 필드는 GraphQL 서버 개발자가 만든 resolver 함수에 의해 실행된다.
필드가 실행되면 해당 resolver 가 호출되어 다음 값을 생성한다.

GraphQL 쿼리의 끝은 항상 스칼라 값이기 때문에, 문자열이나 숫자 같은 스칼라 값을 반환하면 실행이 완료된다.
하지만, 필드가 객체를 반환하면 쿼리는 해당 객체에 적용되는 다른 필드들을 포함하는데 이는 스칼라 값에 도달할 때까지 반복된다.

루트 필드

  • 모든 GraphQL 서버의 최상위 레벨은 사용 가능한 모든 진입점을 나타내는 타입으로, Root 타입 또는 Query 타입이라고 한다.

비동기 resolvers

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

context : GrpahQL 쿼리 인자로 제공된 id 로 사용자 데이터를 로드하는데 사용되는 데이터베이스 액세스를 위해 사용됩니다. 데이터베이스 로딩은 비동기 작업이다.

resolver 함수는 promise 를 인식해야 하지만, GraphQL 쿼리는 Promise 를 인식할 필요가 없다. GraphQL 은 완료될 때까지 기다렸다가 효율적으로 동시에 처리한다.

기본 resolvers

Human: {
  name(obj, args, context) {
    return obj.name
  }
}
  • GrpahQL 서버는 타입 시스템을 통해 작동하며, 이는 다음에 어떤 작업을 수행해야할지 결정해준다. 그렇기 때문에 human 필드가 무언가를 반환하기 전에, human 필드가 Human 을 반환할 것을 알고 있다.
  • obj 인자는 이전 필드에서 반환된 new Human 객체이다.

레퍼런스

post-custom-banner

0개의 댓글