1. 개요
GraphQL은 페이스북에서 쿼리 언어로, 애플리케이션 프로그래밍 인터페이스(API)를 위한 쿼리 언어로, 클라이언트에게 요청한 만큼의 데이터를 제공하는데 우선순위를 둡니다.
이런 GraphQL은 선언형 데이터 fetching 언어라고 부르는데, 개발자는 무슨 데이터가 필요한 지에 대해서만 요구사항을 작성하면 되고, 어떻게 가져올지는 신경쓰지 않아도 됩니다.
sql이 데이터베이스 시스템에서 저장된 데이터를 효율적으로 가져온다면, gql은 웹 클라이언트가 데이터를 서버로부터 효율적으로 가져오는 것에 특화되어 있습니다. 그래서 보통 gql의 문장은 주로 클라이언트 시스템에서 작성하고 호출합니다.
Rest API가 url, method와 같은 다양한 조합으로 생기는 다양한 endpoint가 존재한다면, gql은 단 하나의 endpoint만 존재한다는 특징이 있습니다.
위와 같이 일반적인 Rest API는 Axios 또는 Fetch로 클라이언트 측에서 여러 번의 네트워크 호출으로 데이터를 가져오지만 GraphQL은 한번의 요청으로 데이터를 가져올 수 있습니다.
또한 http api 자체가 특정 데이터베이스나 플랫폼에 종속되지 않은 것처럼, gql도 어떤 플랫폼이나 데이터베이스에 종속적이지 않습니다.
위 코드는 gql쿼리의 예시인데, 서버사이드의 gql 어플리케이션은 gql로 작성된 쿼리를 입력으로 받아 쿼리를 처리한 결과를 다시 클라이언트로 돌려주게 됩니다.
이런 GraphQL은 2가지의 특징이 있습니다.
Over-fetching : REST API를 사용할 경우, 원하는 데이터만 데이터베이스에서 가져오는 것이 아니라 URL에 의해 할당된 데이터를 가져오기 때문에 Over-fetching하게 됩니다. 그에 반해 GraphQL은 자신이 원하는 정보를 쿼리문으로 정확하게 데이터를 가져올 수 있기 때문에 Over-fetching을 방지할 수 있습니다.
Under-fetching : 위와는 반대로 fetching이 부족한 경우가 생기는데, 한번에 여러 데이터를 받아와야 하는 경우 GraphQL에서 처리할 수 있습니다.
GraphQL에서 URL은 존재하지 않습니다. 단지 하나의 end-point만 존재한다는 점. 즉, Rest API에서 여러 번 fetching해야하는 부분을 GraphQL에서는 한 번의 query와 end-point를 통해 원하는 데이터를 가져올 수 있습니다.
2. GraphQL의 파이프라인
graphql을 사용해 API를 개발할 때, 개발자는 클라이언트가 서비스를 통해 쿼리할 가능성이 있는 모든 데이터에 대한 스키마를 생성합니다.
GraphQL 스키마는 개체 유형으로 구성되어 어떤 종류의 개체를 요청할 수 있으며, 어떤 필드가 있는지 정의합니다.
쿼리가 수신되면 GraphQL은 스키마에 대해 쿼리를 검증하고 그 다음 검증된 쿼리를 실행합니다.
API 개발자는 스키마의 각 필드를 Resolver라는 기능에 첨부합니다. 실행 중 값을 생산하기 위해 Resolver가 호출됩니다.
3. GraphQL의 구조
1) 쿼리/뮤테이션
쿼리는 데이터를 읽는데 사용하고, 뮤테이션은 데이터를 변조하는데 사용합니다.
Query : Read
Mutation(CUD) : Create, Update, Delete
2) 오퍼레이션 네임 쿼리
일반적인 쿼리용 함수라고 생각하시면 됩니다.
일반 쿼리
{
human(id: "1000") {
name
height
}
}
오퍼레이션 네임 쿼리
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
일반 쿼리와 오퍼레이션 네임 쿼리의 차이점은 함수명 앞에 “query”가 붙는다는 것과 parameter를 받는다는 점입니다. 데이터베이스의 프로시저와 많이 비슷한 모양입니다.
위와 같이 GraphQL을 구현한 클라이언트에서는 변수에 프로그래밍으로 값을 할당할 수 있는 함수 인터페이스가 존재합니다.
이 인터페이스는 클라이언트 프로그래머 측에서 작성하고 관리합니다.
따라서 기존의 Rest API에서는 백엔드 프로그래머가 작성하여 전달하는 API의 Request와 Response 구조에 대한 협업이 존재했지만 GraphQL을 사용하는 방식에서는 이러한 의존도가 많이 사라지게 됩니다.
다만, 데이터 스키마에 대한 협업 의존성은 존재합니다.
3) 스키마/타입
GraphQL의 스키마를 작성하는 방법은 C에서 type struct를 생성하는 것과 비슷합니다.
type Character {
name: String!
appearsIn: [Episode!]!
}
오브젝트 타입 : Character
필드 : name, appersln
스칼라 타입 : String, id, int 등
느낌표(!) : 필수 값을 의미(non-nullable)
대괄호([,]) : 배열을 의미(array)
4) Resolver
데이터베이스를 사용할 때는 데이터를 가져오기 위해서 SQL을 사용했는데, 반면 GraphQL에서는 데이터를 가져오는 구체적인 과정을 별도로 구현을 해야 합니다.
gql 쿼리문에 대한 파싱은 대부분의 gql 라이브러리에서 처리를 하지만, gql에서 데이터를 가져오는 구체적인 과정은 resolver가 담당하고, 이를 직접 구현을 해야 합니다.
따라서 개발자는 리졸버를 직접 구현해야하는 부담은 생기지만, 이를 통해서 데이터 source 종류에 상관 없이 구현을 할 수 있는 장점은 있습니다.
데이터 source라 하면 데이터베이스나 일반 파일, http, soap와 같은 네트워크 프로토콜을 활용해서 원격 데이터를 가져올 수도 있습니다.
이러한 특성을 사용해 legacy 시스템을 gql 기반으로 바꾸는데 활용할 수 있습니다.
GQL 쿼리에서는 각각의 필드마다 함수가 하나씩 존재한다고 생각하면 됩니다. 이 함수는 다음 타입을 반환하는데, 이런 각각의 함수를 resolver라고 합니다.
만약 필드가 스칼라 값(int, string 등)인 경우에는 실행이 종료되고, 필드의 타입이 스칼라 타입이 아닌 개발자가 정의한 타입이라면 해당 타입의 resolver를 호출하게 합니다.
위와 같은 연쇄적인 resolver의 호출은 DBMS의 관계에 대한 쿼리를 매우 쉽고, 효율적으로 처리할 수 있게 합니다.
예를 들어 gql의 query에서 어떤 타입의 필드 중 하나가 해당 타입과 1:n의 관계를 맺고 있다고 가정합니다.
type Query {
users: [User]
user(id: ID): User
limits: [Limit]
limit(UserId: ID): Limit
paymentsByUser(userId: ID): [Payment]
}
type User {
id: ID!
name: String!
sex: SEX!
birthDay: String!
phoneNumber: String!
}
type Limit {
id: ID!
UserId: ID
max: Int!
amount: Int
user: User
}
type Payment {
id: ID!
limit: Limit!
user: User!
pg: PaymentGateway!
productName: String!
amount: Int!
ref: String
createdAt: String!
updatedAt: String!
}
맨 위의 Query를 보면 user와 limit는 1대1의 관계고 user와 pyment는 1대n의 관계입니다.
{
paymentsByUser(userId: 10) {
id
amount
}
}
{
paymentsByUser(userId: 10) {
id
amount
user {
name
phoneNumber
}
}
}
두 쿼리는 동일한 쿼리명은 가지고 있지만 호출되는 리졸버 함수의 개수는 아래가 더 많습니다.
각각의 리졸버 함수에는 내부적으로 데이터베이스 쿼리가 존재하는데, 리졸버는 쿼리에 맞게 필요한 만큼만 최적화하여 호출 할 수 있다는 의미가 됩니다.
내부적으로 로직 설계를 어떻게 하느냐에 따라서 달라질 수 있겠지만, 이런 재귀적인 형태의 레졸버 체인을 잘 활용한다면 효율적인 설계가 가능해집니다.
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 } })
}
}
리졸버 함수는 위와 같이 총 4개의 인자를 받습니다.
parent : 연쇄적 리졸버 호출에서 부모 리졸버가 리턴한 객체입니다. 이 객체를 활용해서 현재 리졸버가 내보낼 값을 조절할 ttn dlTtmqslek.
args : 쿼리에서 입력으로 넣은 인자입니다.
context : 모든 리졸버에게 전달이 됩니다. 주로 미들웨어를 통해 입력된 값들이 들어 있습니다. 로그인 정보 혹은 권한과 같이 주요 컨텍스트 정보를 가지고 있습니다.
info : 스키마 정보와 현재 쿼리의 특정 필드 정보를 가지고 있습니다. 잘 사용하지 않는 필드입니다.
5) 인트로스펙션(introspection)
기존 서버-클라이언트 협업 방식에는 연동규격서라고 하는 API 명세서를 주고 받는 절차가 반드시 필요했습니다. 프로젝트 관리 측면에서 관리해야 할 대상의 증가는 작업의 복잡성 및 효율성 저해를 의미합니다.
이 API 명세서는 때때로 관리가 제대로 되지 않아, 인터페이스 변경 사항을 제때 문서에 반영하지 못하기도하고, 제 타이밍에 전달 못하곤 합니다.
이러한 REST의 API 명세서 공유와 같은 문제를 해결하는 것이 GraphQL의 인트로스펙션 기능입니다.
gpl의 인트로스펙션은 서버 자체에서 현재 서버에 정의된 스키마의 실시간 정보를 공유할 수 있게 합니다.
이 스키마 정보만 알고 있으면 클라이언트 사이드에서는 따로 연동 규격서를 요청할 필요가 없게 됩니다.
클라이언트 사이드에서는 실시간으로 현재 서버에서 정의하고 있는 스키마를 의심할 필요 없이 받아들이고, 그에 맞게 쿼리문을 작성하면 됩니다.
대부분 서버용 gql 라이브러리들은 쿼리용 IDE를 제공합니다. 이를 활용하여 인트로스펙션을 통해 직접 쿼리 및 뮤테이션, 필드 스키마를 확인할 수 있습니다.
대표적인 gql 라이브러리 셋은 릴레이와 아폴로가 있습니다.
https://www.apollographql.com/
참고
https://tech.kakao.com/2019/08/01/graphql-basic/
https://velog.io/@jangwonyoon/1.-GraphQL-%EA%B0%9C%EB%85%90