GraphQL 이란?
GraphQL
은 클라이언트에서 서버의 데이터를 효율적으로 가져오기 위한 목적이다. SQL
은 백엔드에서 데이터베이스의 데이터를 효율적으로 가져오기 위한 목적이다.
서버에서 작성된 gql
어플리케이션은 gql
을 쿼리로 입력받아 처리한 결과를 다시 응답으로 돌려줍니다.gql
은 특정 데이터베이스나 플랫폼에 종속적이지 않습니다.
GrapqhQL 파이프라인
REST API와 GraphQL 비교
REST API
를 사용하면 엔드포인트마다 요청을 여러번 보내야 하는데, GraphQL
은 단 하나의 요청으로 여러 데이터를 가져올 수 있다.
GraphQL의 구조
쿼리/뮤테이션
CRUD
중 R
만을 담당하는것이 쿼리이고, 나머지 CUD
를 담당하는것이 뮤테이션이다.
// 일반 쿼리
{
human(id: "1000") {
name
height
}
}
// operation name query
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
일반 쿼리와 오퍼레이션 네임 쿼리의 다른 점은 변수를 매개변수로 받느냐 마냐이다.
query getStudentInfomation($studentId: ID){
personalInfo(studentId: $studentId) {
// 개인정보들
}
classInfo(year: 2018, studentId: $studentId) {
// 수업정보들
}
SATInfo(schoolCode: 0412, studentId: $studentId) {
// SAT에 대한 정보들
}
}
REST API로 위의 3가지 정보들을 불러오려고 했다면 서버로 3번의 요청을 보내야 했을것이다. 하지만 GraphQL에서는 위의 오퍼레이션 네임 쿼리(getStudentInformation
)를 사용해서 단한번의 요청으로 3가지 정보들을 모두 불러올수있다.
스키마와 타입(Schema/Type)
REST API
로 백엔드와 프론트엔드 개발자가 서로 협업하려면 API 인터페이스를 먼저 작성하고 작업을 하던지 아니면 백엔드 개발자가 제공하는 request, response 형태에 맞춰 개발을 해야 했다. 하지만, GraphQL
을 사용하면 이런 백엔드/프론트 의존성을 줄일수 있다.(물론 GraphQL 스키마 정의에는 의존성이 여전히 존재한다.)
GraphQL의 스키마를 작성하는건 C/C++에서 헤더파일을 만들어두고 CPP 파일에서 헤더를 include해서 구현하는것과 비슷하다.
오브젝트 타입과 필드
type Character {
name: String!
appearsIn: [Episode!]!
}
- Character라고 하는 객체 타입을 정의했다.
- 이 객체 안에는 name, appearsIn 두개의 필드가 존재한다.
- name 필드는 String 스칼라 타입이다.
- name 필드는 필수값이다(!)
- appearsIn필드는 Episode 스칼라 타입의 배열이며 이 배열도 필수로 존재해야하고, 배열내의 적어도 하나의 Episode가 있어야 한다.
리졸버(resolver)
아까 살펴봤던 GraphQL 파이프라인
을 한번더 다시 보자. GraphQL쿼리
는 파싱되어 리졸버
로 들어가게된다. 한마디로 리졸버
는 GraphQL
에서 실제로 데이터 처리를 담당하는 친구이다. 이것은 개발자가 직접 구현해야 하며, 개발자가 원하는 방식대로 구현할수있고 자신이 원하는 데이터베이스를 사용해서 데이터 가져오는 로직을 작성할 수 있다. 그렇기 때문에 데이터를 RDB에서 가져올수도 있고 파일에서 가져올수도 있고 심지어 다른 네트워크 통신을 통해서 가져올 수도있다.
한마디로, GraphQL
쿼리문 자체는 인터페이스라고 생각하면 편한것같다.
GraphQL의 쿼리문이 처리되는 과정
다음과 같은 GraphQL 쿼리문이 서버로 도착했다고 가정하자.
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
구조를 먼저 살펴보면, hero
라고 하는 필드가 있고 하위에 name
, friends
서브 필드가 있으며 friends
서브 필드에는 name
이라는 서브서브필드가 있다. 이 쿼리문은 파싱되어 리졸버에게 전달된다.
리졸버의 역할은 각 필드에 해당하는 데이터를 실제로 내주는것이다. 필드 1개당 리졸버 1개가 존재한다. 만약에 리졸버가 스칼라값(문자열이나 숫자)를 리턴한다면 리졸버의 호출이 종료된다. 스칼라값을 만나기 전까지 리졸버는 계속 실행된다. 즉 GraphQL의 이런 방식은 알고리즘에서의 DFS
와 비슷하다는 느낌을 받았다. 끝장을 보기 전까지는 계속해서 파고든다.
리졸버를 제대로 활용해보자.
type User{
// 유저에 대한 필드
name: String!
phoneNumber: String!
}
type Limit {
// 유저 1명이 가질수 있는 제한
id: ID!
amout: Int
user: User
}
type Payment {
// 유저 1명이 지불한 지불정보들
limit: Limit!
}
User, Limit, Payment라고하는 3개의 오브젝트 타입이 존재한다. 유저는 1개의 제한만을 가질수 있고, 여러개의 지불정보를 가질 수 있다. 이 상황에 다음과 같이 쿼리문이 들어왔다고 해보자.
{
paymentsByUser(userId: 10) {
id
amount
}
}
{
paymentsByUser(userId: 10) {
id
amount
user {
name
phoneNumber
}
}
}
대충 어떤식으로 동작할지 감이 오는가? 위 두개의 쿼리는 동일한 쿼리명을 갖고 있지만 아래 쿼리문이 더 많은 리졸버를 호출한다. 아래 쿼리문에서는 id, amount, user 3개의 필드를 갖고 있는데 id와 amout 필드 담당인 리졸버는 스칼라값을 리턴하고 종료되지만, 마지막 user필드에 대해서는 한번더 파고들어서 user의 name과 phoneNumber리졸버 까지 호출하게 된다.
그에반해, 위의 쿼리는 id, amount 두 필드에 해당하는 리졸버만 호출하게 된다.
리졸버 함수는 다음과 같이 구현하면 된다.
{
paymentsByUser(userId: 10) { // 리졸버 명세
id
amount
}
} 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
},
}
이름 없는 GraphQL 쿼리의 paymentsByUser라고 하는 필드를 아까 정의 했었고 아래에서는 그 필드를 실제로 가져오기 위한 리졸버를 실제로 구현하고 있다. 마치 위의 인터페이스를 아래에서 구현하고 있는듯한 느낌이다.
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
},
}
그래서 이 쿼리 부분을 자세히 살펴보자. 총 4개의 인자를 받고있다.
- 첫번째 인자는 부모 리졸버가 최종적으로 리턴한 값이 들어온다.
(아까 paymentsByUser쿼리문에서 user필드를 만났을때 계속해서 파고든다고 했는데 사실은 부모 리졸버는 종료되고 자식 리졸버가 대를 이어 계속 파고 드는것 같다.)
- 두번째 인자는 쿼리문에 넘겼던 매개변수(
userId
)가 들어온다. - 세번째 인자는 모든 리졸버가 공통으로 사용하는
context
가 들어오는데, 보통 미들웨어를 거쳐서 입력된 로그인과 같은 정보들이 들어온다. - 네번째 인자는 스키마 정보나 필드 정보등이 들어있다.(잘 사용되지 않음.)
인트로스펙션(introspection)
프론트와 백엔드가 협업하기 전에 API 인터페이스를 먼저 작성하고 작업에 들어간다. 그렇게 하지 않으면 API가 개발될때까지 프론트 개발이 지연되기 때문이다. 그런데, 이렇게 API 인터페이스 문서를 작성하는것도 문제이다. 왜냐면, 이 문서 자체도 계속해서 업데이트를 해줘야하는데 이게 한두개 API가 있을땐 괜찮은데 API가 많아지다보면 이 문서 자체를 관리하기 쉽지가 않다.
GraphQL의 인트로스펙션 기능을 사용하면 현재 서버에 저장된 스키마의 정보를 실시간으로 확인할수있다. 그러니까 문서를 따로 만들필요가 없는것이다. 백엔드개발자가 스키마를 정의해주면 프론트엔드개발자는 그걸 실시간으로 확인해서 개발하면 된다. 다시말해서, 스키마만 정의되면 프로젝트를 병렬적으로 작업할 수 있다는 얘기고 이건 생산성 향상에 큰 효과가 있다는것을 의미한다.
GraphQL을 서버에서 사용하기 위해서는 apollo server라는 라이브러리를 사용하는데 여기서 기본적으로 제공하는 웹 IDE에서 현재 실시간 스키마 정보를 확인할 수 있다.
GraphQL로 비즈니스 로직 작성하기
비즈니스 로직 자체는 graphql 리졸버에 담아서는 안된다. 이 로직은 비즈니스 로직 레이어에 작성을 하고 리졸버에서는 그 레이어에 작성한 함수를 호출해서 응답을 내주는 역할만을 해야 한다. 예시를 살펴보자.
<script src=”https://gist.github.com/simsimjae/8028388bdae0f5efb58dcebb94c8022b.js"></script>
requestPaymentSession
과 requestPaymentApprove
라는 두개의 리졸버를 정의했다. 각 리졸버는 비즈니스 레이어에 정의한 함수를 호출하여 응답을 내주고 있다.
정리
GraphQL은 퍼포먼스적인 장점도 있지만, 생산성 향상 측면을 더 주목해야한다. 기존 백엔드 — 프론트 협업 문화를 많이 바꿀것이다. 프론트엔드쪽의 할일이 더 많아지는것 같지만 애자일하게 프로젝트를 진행하는데 많은 도움이 될 것 같다. 왜냐면, API 명세서를 쉽고 빠르게 공유할수 있으며 관리하기도 편하기 때문에, 서비스를 더욱더 빨리빨리 만들어낼수있기 때문이다.