GraphQL

GraphQL을 알아보자

GraphQL이란?

페이스북에서 만든 쿼리 언어. SQL과 마찬가지로 쿼리언어 하지만 GraphQL과 SQL의 언어적 차이는 매우 크다. SQL은 DBMS에 저장된 데이터를 효율적으로 가져오는 것이 목적이고, GraphQL은 웹 클라이언트가 데이터를 서버로 부터 효율적으로 가져오는 것이 목적. SQL의 문장은 주로 백엔드 시스템에서 호출하고, GraphQL의 문자응 주로 클라이언트 시스템에서 호출

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

{
  hero {
    name
    friends {
      name
    }
  }
}

GraphQL 파이프라인

GraphQL 어플리케이션은 GraphQL로 작성된 쿼리를 입력으로 받아 쿼리를 처리한 결과를 다시 클라이언트로 돌려준다. GraphQL은 어떠한 특정 데이터베이스나 플랫폼에 종속적이지 않고, 심지어 네트워크 방식에도 종속적이지 않다. 일반적으로 HTTP POST / Websocket 프로토콜을 활용하지만, L4의 TCP/UDP를 활용하거나 심지어 L2 형식의 이더넷 프레임을 활용할 수도 있다.

REST API는 URL, METHOD 등을 조합하기 때문에 다양한 Endpoint가 존재하지만, GraphQL은 단 하나의 Endpoint가 존재한다. 불러오는 데이터의 종류를 쿼리 조합에 따라 결정. => REST API는 각 EndPoint마다 SQL쿼리가 달라지는 반면, GraphQL은 Grapql 스키마의 타입마다 데이터베이스 SQL이 달라진다. 하지만, 단 한번의 통신만 수행한다.

GraphQL의 구조

Query / Mutation

쿼리는 읽는데 사용하고, 뮤테이션은 데이터를 변조하는데 사용한다. GraphQL은 쿼리에 변수라는 개념이 있는데, GraphQL을 구현한 클라이언트에서는 이변수에 프로그래밍으로 값을 할당 할 수 있는 함수 인터페이스가 존재한다.

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
  }
}

위에 예시가 바로 오퍼레이션 네임 쿼리로, 쿼리용 함수를 의미한다. DBMS에서 프로시저 개념과 유사하다. 이 개념 덕분에 하나의 호출 수행으로 모든 데이터를 가져올수 있다. GraphQL 오퍼레이션 네임쿼리는 클라이언트 개발자가 작성한다. 즉, 이전 협업 방식(REST API)에서는 프론트 개발자는 백엔드 개발자가 작성하여 전달하는 API의 request / response의 형식에 의존하게되지만 GraphQL을 사용한 방에서는 이러한 의존도가 많이 사라진다.

스키마 타입

오브젝트 타입과 필드

type Character {
  name: String!
  appearsIn: [Episode!]!
}
  • 오브젝트 타입 : Character

  • 필드 : name, appearsIn

  • 스칼라 타입 : String, ID, Int 등

  • 느낌표(!) : 필수 값을 의미(non-nullable)

  • 대괄호([, ]) : 배열을 의미(array)

리졸버

DBMS에선 데이터를 가져오려면 SQL을 작성해서 구체적인 데이터를 가져왔었는데, GraphQL에선 이 과정을 리졸버에서 직접 구현해야됨. 예를 들어, 리졸버를 통해 DB에서 데이터를 가져오거나 일반 파일이나 외부 API에서 원격데이터를 가져올수도 있음. 이러한 특성을 이용하면 legacy 시스템을 GraphQL 기반으로 바꾸는데 활용 가능

GraphQL 쿼리에는 각각의 필드마다 하나의 함수가 존재한다. 이는 리졸버라고하고 만약 리턴값이 스칼라값(문자열이나 숫자와 같은 primitive 타입)인 경우에는 실행이 종료된다. 그러나 스칼라 타입이 아닌 우리가 정의한 값이면 해당 타입의 리졸버를 호출한다. (즉 연쇄적인 호출이 가능하다)

이러한 연쇄적인 호출은 DFS로 구현이 되어있을것으로 추출되고, DBMS의 관계에 대한 쿼리를 매우 쉽고 효율적으로 처리할수 있다.

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
    }
  }
}

두 쿼리 모두 동일한 쿼리명을 가지지만, 호출되는 리졸버 함수의 갯수가 아래가 더많음. 각각의 리졸버 함수에는 내부적으로 DB쿼리가 존재하므로 쿼리에 맞게 필요한 만큼 최적화해서 호출 가능. 로직은 비즈니스 로직 레이어에 작성하는 것을 권장!

  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 } })
    }
  }

Introspection

REST API의 swagger와 같은 역할을 하는것이 GraphQL의 apollo server. 여기서 쿼리의 테스트가 가능하다. 상용환경에서 이러한 스키마의 공개는 신중해야되기에 대부분의 라이브러리는 해당 기능을 키고 끄는 옵션이 존재.

Last updated