GraphQL이란?
GraphQL은 페이스북에서 만든 쿼리 언어로, 등장한지 얼마되지 않았음에도 불구하고 인기가 매우 가파르게 올라가고 있다. GraphQL(=gql)은 SQL(Structed Query Language)와 같은 쿼리 언어이다. 하지만, gql과 sql의 언어적 구조차이는 매우 크다. sql은 데이터베이스 시스템에 저장된 데이터를 효율적으로 가져오는 것이 목적이고, gql은 웹 클라이언트가 서버로부터 데이터를 효율적으로 가져오는 것이 목적이다. 따라서, sql은 주로 백앤드 시스템에서 작성하고 호출하는 반면, gql의 문장은 주로 클라이언트 시스템에서 작성하고 호출한다.
sql query
SELECT plot_id, species_id, sex, weight, ROUND(weight / 1000.0, 2) FROM surveys;
gql query
{
hero {
name
friends {
name
}
}
}
서버사이드 gql 어플리케이션은 gql로 작성된 쿼리를 입력으로 받아 쿼리를 처리한 결과를 다시 클라이언트로 돌려준다. HTTP API 자체의 특성에 따라 gql 역시 어떠한 특정 데이터베이스나 플랫폼에 종속적이지 않다. 심지어 네트워크 방식에도 종속적이지 않으며, 일반적으로 gql의 인터페이스간 송수신은 네트워크 레이어 L7의 HTTP POST 메서드와 웹소켓 프로토콜을 활용한다. 필요에 따라서는 얼마든지 L4의 TCP/UDP를 활용하거나 심지어 L2 형식의 이더넷 프레임을 활용할 수도 있다.
GraphQL vs REST
단일 엔드포인트 vs 다수 엔드포인트
REST API
REST API를 사용하면 일반적으로 URL, Method등을 조합하기 떄문에 여러 엔드포인트에 액세스하여 데이터를 수집한다. 위의 예시에서 /users/<id>는 초기 사용자 데이터를 가져오기 위한 엔드포인트가 있다. 또, /users/<id>/posts 사용자에 대한 모든 게시물을 반환하는 엔드포인트가 있으며, /users/<id>/followers는 사용자당 팔로어 목록을 반환하는 엔드포인트이다.
반면에 GraphQL에서는 구체적인 데이터 요구 사항이 포함된 단일 쿼리를 GraphQL 서버에 보내기만 하면 된다. 그런 다음 서버는 이러한 요구 사항을 만족하는 JSON 개체를 응답한다.
위 그림처럼, GraphQL API를 사용하면 여러번 네트워크 호출을 할 필요없이, 한 번의 네트워크 호출로 처리할 수 있다.
오버/언더 패칭 없음 vs 있음
REST의 가장 일반적인 문제 중 하나는 오버 및 언더 패치 문제이다. REST API의 엔드포인트는 고정된 데이터 구조를 반환하기 때문이다. 클라이언트 요청마다 요청 값만 정확히 제공하는 방식으로 API를 설계하는 것이 어렵다.
오버패칭: 불필요한 데이터 다운로드
오버패치는 클라이언트가 앱에서 실제로 필요한 것보다 더 많은 정보를 다운로드함을 의미한다. 예를 들어 사용자 이름 목록을 보여줘야 하는 상황에서, REST API를 사용해 /users 엔드포인트로 요청했을 때 사용자 데이터 정보 전체를 담은 JSON 배열을 수신한다. 그러나 이 응답에는 필요하지 않은 추가 정보(예: 사용자의 생일, 주소)가 포함될 수 있다. 클라이언트는 앞에서 설명했듯이 사용자의 이름 정보만 필요하기에 나머지 정보들은 불필요한 데이터가 된다.
언더 패칭과 n+1 문제
또 다른 문제는 언데패칭 및 n+1 리퀘스트 문제이다. 언더패칭은 일반적으로 특정 엔드포인트가 필요한 정보를 충분히 제공하지 않는 것을 의미한다. 클라이언트는 필요한 정보를 모두 가져오기 위해 추가적인 요청을 해야한다.
예를 들어 동일한 앱에서 사용자당 마지막 팔로워 3명도 표시해야 한다고 가정한다. API는 /users/<user-id>/followers라는 추가 엔드포인트를 제공해야한다. 필요한 정보를 얻기 위해서는 /user 엔드포인트에 요청하여 사용자에 대한 정보(user-id)를 가져온 다음, 각 사용자마다 /users/<user-id>/followers 엔드포인트를 이용해서 팔로워 정보를 얻어와야한다.
GraphQL 구조
스키마/타입(schema/type)
데이터베이스 스키마(schema)를 작성할 때의 경험을 SQL 쿼리 작성으로 비유한다면, gql 스키마를 작성할 때의 경험은 C, C++의 헤더파일 작성에 비유를 할 수 있다.
오브젝트 타입과 필드
type Character {
name: String!
appearsIn: [Episode!]!
}
- 오브젝트 타입 : Charcter
- 필드 : name, appearsln
- 스칼라 타입 : String, ID, Int 등
- 느낌표(!) : 필수 값을 의미(non-nullable)
- 대괄호([, ]) : 배열을 의미(array)
리졸버(resolver)
데이터베이스 사용시, 데이터를 가져오기 위해서 sql를 작성했다. 또한, 데이터베이스에는 데이터 베이스 어플리케이션을 사용해 데이터를 가져오는 구체적인 과정이 구현되어 있다. 그러나 gql에서는 데이터를 가져오는 구체적인 과정을 직접 구현해야 한다. gql 쿼리문 파싱은 대부분의 gql 라이브러리에서 처리를 하지만, gql에서 데이터를 가져오는 구체적인 과정은 리졸버가 담당하고, 이를 직접 구현해야 한다. 개발자는 리졸버를 직접 구현해야하는 부담은 있지만, 이를 통해 데이터의 source의 종류에 상관없이 구현이 가능하다. 예를 들어, 리졸버를 통해 데이터를 데이터베이스 뿐만 아니라, 일반 파일, 심지어 http, SOAP와 같은 네트워크 프로토콜을 활용해서 원격 데이터를 가져올 수도 있다.
gql 쿼리에서는 각각의 필드마다 함수가 하나씩 존재한다고 생각하면 된다. 이러한 각각의 함수를 리졸버라고 하며, 리졸버는 사용자 정의 타입이나 스칼라 값(문자열이나 숫자와 같은 primitive 타입)을 반환할 수 있다. 전자를 반환하는 경우 해당 타입의 리졸버를 호출한다, 후자(스칼라 값)를 반환하는 경우에는 실행이 종료되며 더 이상의 연쇄적인 호출이 일어나지 않는다.
이러한 연쇄적 리졸버 호출은 DFS(Depth First Search)로 구현되어 있을 것으로 추측된다. 연쇄 리졸버의 특성을 잘 활용하면 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!
}
여기에서는 User와 Limit는 1:1 관계이고 User와 Payment는 1:n의 관계이다.
{
paymentsByUser(userId: 10) {
id
amount
}
}
{
paymentsByUser(userId: 10) {
id
amount
user {
name
phoneNumber
}
}
}
두 쿼리는 동일한 쿼리명을 가지고 있지만, 호출되는 리졸버 함수의 갯수는 아래가 더 많다.
각각의 리졸버 함수에는 내부적으로 데이터 쿼리가 존재하므로 쿼리에 맞게 필요한 만큼 최적화하여 호출이 가능하다. 내부적으로 로직 설계를 어떻게 하느냐에 따라 달라질 수 있겠지만, 이러한 재귀형의 리졸버 체인을 잘 활용한다면, 효율적인 설계가 가능하다.(기존 REST API는 정해진 쿼리는 무조건 전부 호출이 된다.)
리졸버 함수는 다음과 같이 총 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로 스키마 정보와 더불어 현재 쿼리의 특정 필드 정보를 가지고 있다. 잘 사용하지 않는 필드이다.
쿼리/뮤테이션(query/mutation)
위와 같이 요청하는 쿼리문과 응답 데이터의 구조는 거의 일치한다.
쿼리는 데이터를 읽는데 사용(R)하고, 뮤테이션은 데이터를 변경(CUD)하는데 사용한다는 차이가 있다.
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라는 키워드가 붙는 쿼리문으로 매우 편리하다. 데이터베이스에서 프로시저(procedure) 개념과 유사하다고 볼 수 있다. 이 개념 덕분에 REST API를 호출할 때와는 다르게, 한 번의 인터넷 네트워크 왕복으로 원하는 모든 데이터를 가져올 수 있다. 데이터베이스의 프로시저는 DBA 혹은 백엔드 개발자가 작성하고 관리했지만, GraphQL 오퍼레이션 네임 쿼리는 클라이언트 개발자가 작성하고 관리한다.
GraphQL이 제공하는 추가 기능 덕분에 백엔드 개발자와 프론트엔드 개발자의 헙업 방식에도 영향을 준다. 이전 협업 방 방식(REST API)에서는 프론트앤드 개발자는 백엔드 개발자가 작성해 전달하는 API request/response 형식에 의존하게 되지만, GraphQL 방식에서는 이러한 의존도가 많이 사라진다.
인트로스펙션(introspection)
기존 서버-클라이언트 협업 방식에서는 연동 규격서라고 하는 API 명세서를 주고 받는 절차가 필요했다. 최근에는 swagger와 같은 문서 자동화 툴도 많이 사용한다. 프로젝트를 진행하면서 API 명세서의 관리가 제대로 되지 않거나 인터페이스 변경 사항을 바로바로 업데이트 하지 않아 백앤드와 프론트의 소통이 원활하지 못하는 경우가 발생한다.
이러한 REST의 API 명세서 공유와 같은 문제를 해결하는 것이 gql의 인트로스펙션 기능이다 gql의 인트로스펙션은 서버 자체에서 현재 서버에 정의된 스키마의 실시간 정보를 공유할 수 있게 한다. 클라이언트 사이드에서는 실시간으로 현재 서버에서 정의하고 있는 스키마에 맞게 쿼리문을 작성하면 된다.
이러한 인트로스펙션용 쿼리가 따로 존재한다. 일반 gql 쿼리문을 작성하듯이 작성하면 되며, 실제로는 굳이 스키마 인트로스펙션을 위해 gql을 작성할 필요가 없다. 대부분의 서버용 gql 라이브러리에는 쿼리용 IDE를 제공한다. 다음 화면은 apollo server라는 서버용 gql 라이브러리에 포함 되어있는 웹 IDE 화면이다.
인트로스펙션을 활용하여, 직접 쿼리 및 뮤테이션, 필드 스키마를 확인할 수 있으나, 보안상의 이슈로 상용환경에서는 이러한 스키마는 공개하지 않도록 하는 것이 좋다. 대부분의 라이브러리는 해당 기능을 켜고 끄는 옵션이 존재한다.
Reference
'FrontEnd' 카테고리의 다른 글
[Github] Github Actions를 이용한 AWS S3 배포 자동화 (0) | 2022.02.23 |
---|---|
[Github] Github Actions를 이용한 CI/CD (0) | 2022.02.21 |
[S3, CloudFront] React Access Denied(403) Error (0) | 2022.01.26 |
Virtual DOM vs Incremental DOM (0) | 2022.01.16 |
[DevOps] CI/CD란? (0) | 2022.01.15 |
댓글