Beyond RESTNetflix Tech Blog에서 포스팅된 내용을 정리한 글이다.

글의 요점은 Netflix 엔지니어들 사이에서 개발 업무 퍼포먼스 향상을 고민하고 있고, 그 한가지 방식으로 GraphQL 마이크로서비스(GQLMS) 개념을 도입하였다는 것이다. 또 이를 보다 효과적으로 사용하기 위해 겪었던 시행착오들과 향후 보완해나가야 될 사항들에 대한 내용이다.

GraphQL 이란

  • 페이스북에서 만든 쿼리 언어이다.

  • sql이 서버가 DB의 데이터를 효율적으로 가져오는 것이 목적이라면, GraphQL(이하 gql)은 클라이언트가 데이터를 서버로부터 효율적으로 가져오는 것이 목적이다.

  • 특정 DB나 플랫폼, 통신 계층에 종속적이지 않다.

  • URL, METHOD 등을 조합하여 다양한 Endpoint가 존재하는 REST API와 달리 gql API는 쿼리 조합을 통해서 단 하나의 Endpoint만 존재한다.

GraphQL과 REST의 차이점에 대해서 더 자세히 보려면 이 사이트를 참고하자.

GraphQL 구조

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

GraphQL 스키마

type Character {
  name: String!
  appearsIn: [Episode]!
}
  • Character 는 GraphQL 객체 타입으로, 즉 필드가 있는 타입이다. 스키마의 대부분의 타입은 객체 타입이다.
  • name 과 appearIn 은 Character 타입의 필드이다.
  • String 은 내장된 스칼라 타입 중 하나이다.
  • String! 은 필드가 non-nullable 임을 의미한다. 즉, 이 필드를 쿼리할 때 GraphQL 서비스가 항상 값을 반환한다는 것을 의미한다. 타입 언어에서는 이것을 느낌표로 나타낸다.
  • [Episode]! 는 Episode 객체의 배열(array) 을 나타낸다. 또한 non-nullable 이기 때문에 appearIn 필드를 쿼리할 때 항상(0개 이상의 아이템을 가진) 배열을 기대할 수 있다.

스키마 타입

스키마 대부분은 일반 객체 타입이지만 특수한 두 가지 타입이 있다.

schema {
  query: Query
  mutation: Mutation
}

Query 타입(R)

query {
  hero {
    name
  }
  droid(id: "2000") {
    name
  }
}

Query 타입 응답

{
  "data": {
    "hero": {
      "name": "R2-D2"
    },
    "droid": {
      "name": "C-3PO"
    }
  }
}

Mutaion 타입(CUD)

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

Mutaion 타입 응답

{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}

GraphQL 인터페이스

여러 타입 시스템과 마찬가지로 GraphQL도 인터페이스를 지원한다. 인터페이스 는 이를 구현하기 위해 타입이 포함해야하는 특정 필드들을 포함하는 추상 타입이다.

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

이것은 Character 를 구현한(implements) 모든 타입은 이러한 인자와 리턴 타입을 가진 정확한 필드를 가져야한다는 것을 의미한다.

다음은 Character 를 구현한 몇 가지 타입 예제이다.

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 인터페이스의 모든 필드를 가지고 있다. 또한 특정 캐릭터에 추가 필드 totalCredits,starships, primaryFunction 을 가질 수도 있다.

Netflix에서 GraphQL 활용법

GraphiQL

  • GraphiQL IDE 활용 1 - GraphQL 스키마 등 사용 문법을 UI로 제공함으로써 작업 능률 향상 (Swagger UI와 대조)

  • GraphiQL IDE 활용 2 - GraphQL의 다중 언어 프로토타입 코드 만들기 지원 (Swagger Codegen과 대조)

Graphile

Graphile은 PostgreSQL에서 제공하는 확장성이 뛰어난 고성능의 GraphQL API 구축 라이브러리이다.

  • Graphile은 PostgreSQL의 DB 테이블을 기반으로하여 GraphQL 스키마 자동 생성을 지원한다.
  • Graphile은 생성된 GraphQL 스키마의 보안, 성능 및 확장성을 보장한다.

Netflix에서는 "REST보다 더 나은 REST" 플랫폼을 제공할 수 있다는 가정하에 Docker를 사용하여 Netflix 내부 구성요소에 Graphile 라이브러리를 더해 추가 코딩 없이 매우 간단한 GraphQL API 구축 서버를 정의했고 그 결과는 성공적이었다고 평가한다.

  • 개발 단계에서 GraphQL 클라이언트가 자동 생성 및 Graphile에 의해 변형된 쿼리에 대한 전체 접근을 허용하여 유연성을 높인다.(모든 테이블 및 view에서 CRUD 작업 노출) 그리고 상용에 나가기 전 UI에서 노출되면 안되는 스키마 요소를 제거한다.

API로서의 DB View

Netflix에서는 DB Views를 API로서 사용하기로 하고 여러 스키마를 만든 다음 PostgreSQL user role에 의해 허용된 DB에 접근할 수 Graphile 웹 앱을 구성하여 다른 스키마의 테이블들에 접근할 수 있는 Views를 정의했다.


이러한 구성은 아래와 같은 목표를 달성할 수 있었다.

  • 기본 테이블에 대한 모든 권한은 PostgreSQL user에게 명시적으로 부여되어 예기치 않은 쓰기 접근을 방지한다.
  • 노출된 GraphQL 스키마에 대한 변경이 원자적으로 발생하도록 단일 트랜잭션 내에서 테이블과 View를 수정할 수 있다.

이를 가능하게 하는 것은 pgWatch가 활성화된 상태로 Graphile을 실행하므로 DB 테이블 구조가 업데이트 되는 즉시 GraphQL 스키마에 변경 사항을 반영한다.

많은 조직에서 GraphQL을 전사적 데이터 모델을 통합하고 구조화된 데이터 탐색을 위한 단일 진입점을 제공하는 방법으로 사용하고 있으며 CRUD 구축을 위한 풍부한 API로서 활용한다.
또한 사용 DB는 PostgreSQL DB로 PostgreSQL에서 제공하는 라이브러리를 적극 활용한다.

PostgreSQL Composite Types

지금까지의 작업으로 Graphile은 PostgerSQL DB 스키마를 읽고 테이블과 기본 View를 GraphQL 스키마로 변환하는 작업까지 모두 잘 수행했다.

하지만 PostgreSQL 집계 함수 또는 JSON 함수가 View 내에 존재할 때, Graphile이 해당 유형을 인식하지 못하는 한계를 발견했다.

이 때, 집계 함수 또는 JSON 함수를 정의할 때 Composite Types를 사용하게 되면 아래와 같이 Graphile이 해당 유형을 인식할 수 있다는 것을 알았다.

Smart Comments 을 사용하여 Graphile에서 보다 풍부하게 Views를 설명할 수 있다.

Graphile 생성 스키마에 대한 "전체 접근" 허용(개발중)

처음에 Graphile을 사용하자는 제안("모든 것을 아우르는 하나의 스키마")은 보안 (이것이 Netflix의 IAM(ID 관리 체계) 내에서 DB 내부의 row-level 접근 통제와 어떻게 통합 운영할 것인가?) 및 성능 (한번에 모든 rows를 조회하는 디도스를 피하기 위해 쿼리를 어떻게 제한할 것인가?)에 대한 우려 등을 비롯하여 격렬한 반대에 부딪혔다. (아직 완벽히 해결하지 못한 것으로 보여짐)

하지만 이를 도입했을 때 얻는 이점 중 가장 두드러지는 것은 UI팀과 백엔드팀의 강한 결합을 줄일 수 있다는 것이다.

이는 UI팀과 백엔드 팀이 초기 API 설계를 하고 백엔드 팀이 API를 구현하고, UI팀이 해당 API를 사용하고 UI팀의 또 다른 요구사항에 맞춰서 백엔드 팀이 재구현하는 기존의 일하는 방식과 확연한 차이를 보이며 결과적으로 UI팀이 백엔드 팀을 포함할 필요 없이 여러 새로운 기능을 빠르게 개발 및 적용할 수 있게 된다.

물론 도입 과정이 순탄치만은 않았다. UI가 원하는 데이터를 가져오기 위해 여러 쿼리가 필요한 경우가 많았고 전체 앱의 성능이 좋지 않았다. 그러나 앱의 동작이 구체화 되면 각 상호 작용에 단일 호출만 필요하도록 UI의 요구 사항을 충족하는 View를 만들 수 있었다. 이러한 요청은 DB에서 네이티브 코드로 실행되기 때문에 인덱스, 비정규화, 클러스터링 등의 적절한 사용을 통해 정교한 쿼리를 수행하고 성능을 끌어 올릴 수 있었다.

또한 개발이 완료되는 시점에서 UI와 백엔드 간의 공개 API가 확정되면 GraphQL 스키마를 강화하여 스마트 주석으로 테이블과 View를 표시하여 불필요한 쿼리를 모두 제거하였다.

결론

GraphQL API 개발에 있어서 스키마 우선(Schema-first) 접근 방식을 취하는 사람들에게 Graphile의 자동 GraphQL 스키마 생성 기능은 제한적일 것이다.

또한 세분화된 접근 제어가 필요한 경우 역시 Graphile을 기존 사내 서비스의 IAM 인프라에 통합하기 어려울 수 있다.

그리고 Graphile 생성 스키마에 추가 작업을 수행하는 것(UI에 필요한 gRPC 서비스 호출 등)은 현재 Docker 이미지로는 지원하지 않는다. 하지만 최근 Graphile의 makeExtendSchemaPlugin을 이용하면 가능하다는 것을 알게 되었다.

결과적으로 아직까지 위와 같은 어려움이 남아 있음에도 불구하고 초기 요구사항이 제한적이고 이전에 협업 이력이 없는 팀이 모여서 4~6주에 걸쳐 GraphQL을 사용하여 내부 앱을 구현한 것은 Netflix Studio 전체에 많은 관심을 불러 일으켰다.

이를 계기로 Netflix 내의 다른 팀은 다음과 같은 GQLMS 접근 방식을 찾고 있다.

1) 표준 GraphQL 구성 및 유틸리티를 사용하여 DB를 API로 노출
2) 사용자 정의 PostgreSQL 유형을 활용하여 GraphQL 스키마 작성
3) DB에서 대용량 API를 자동 생성하여 유연성 향상
4) Graphile에 의해 생성된 것과 함께 추가 사용자 정의 비즈니스 로직 및 데이터 유형 노출

그동안 CRUD 도구를 위한 솔루션으로 REST를 사용해왔었지만 Graphile을 더한 표준화된 컨테이너를 사용하면 Global Media Studio에 던져지는 끊임 없이 변화하는 요구 사항들을 신속하게 개발할 수 있는 인프라 환경을 구축할 수 있다.

참고 자료

profile
Java 백엔드 개발자입니다. 제가 생각하는 개발자로서 가져야하는 업무적인 기본 소양과 현업에서 가지는 고민들을 같이 공유하고 소통하려고 합니다.

0개의 댓글