Netflix DGS (Domain Graph Service) 시작하기
최근 Netflix 에서 Spring Boot에서 GraphQL 사용을 위한 DGS(Domain Graph Service) Framework 를 공개했다.
GraphQL은 페이스북에서 공개한 API Query Language이다. 기존 Rest API 의 단점들을 보완하고 새로운 패러다임을 제시하여 현재 GitHub API 등 널리 쓰이고 있다. GraphQL은 단일 접점(End Point) 제공을 하며, 클라이언트가 원하는 데이터를 정확하게 응답해주는 방식이다. 예를 들어, 게시글 정보를 조회하려고 할 때 아래와 같은 Query를 요청하면 API서버는 요청에 맞는 정보만 내려주는 방식이다.
query {
articles(count: 20, offset: 0) { // 가장 최신의 20개 게시글 요청
id // 게시글의 id, title, category 요청
title
category
author { // 작성자의 id, name 요청
id
name
}
}
}
Spring Boot 환경에서의 GraphQL
Spring Boot에서 GraphQL을 적용하려면 아래와 같은 일련의 과정으로 개발이 필요했다.
참고 : www.baeldung.com/spring-graphql
1. graphql-java-tools 의존성 추가
2. Schme 파일 관리
3. GraphQLQueryResolver 인터페이스를 구현한 스프링 Bean 을 등록
4. Response를 위한 DTO클래스를 선언
5. Field Resolver 구현
6. 그 밖에 다양하고 복잡한 케이스 처리 로직 추가
GraphQL 사용시, 클라이언트는 편해지고 유리한 점이 많이 있었지만 프로젝트가 복잡해질 수록 API Server에서는 처리해야 할 일들이 많아졌다. 그래서 다양한 솔루션들이 제시되고 있는 상황이었는데 이번에 Netflix에서 그러한 솔루션 중 하나로 DGS Framework를 공개하게 되었다. 필자는 Netflix의 DGS 공개가 매우 반가웠다.
DGS Framework 기본
DGS의 주요 기능
- Annotation 기반의 Spring Boot 프로그래밍 모델
- Query 테스트를 단위테스트로 작성하기 위한 테스트 프레임워크
- GraphQL 스키마에서 Java, Kotlin 클래스를 생성하는 Gralde 플러그인
- GraphQL Federation과의 손쉬운 통합
- Spring Security와 통합
- GraphQL 구독 (WebSocket 및 SSE)
- File Upload
- Error 처리
- Interface/Union 타입에 대한 자동 지원
- Java를 위한 GraphQL 클라이언트
- Pluggable 수단
DGS의 GraphQL 아키텍처
DGS는 기존의 GraphQL 구현 방식을 최대한 맞추었으며, graphql-kotlin, graphql-java 라이브러리 사용을 유지했다. Apollio의 스펙에 정의된 @extends Annotation을 사용하여 통합 스키마에 정의된 유형을 공유하고 확장했다. 이는 대규모 단일 GraphQL 스키마의 소유권을 마이크로서비스로 분할하는 효과적인 방법이다.
Federated Gateway에서는 해당 Query를 처리하는데 필요한 서비스를 호출하는 Query Plan 을 구성한다. 그리고 각 서비스는 소유한 데이터에 대한 Query를 부분적으로 처리하기 위해 _entities query에 응답한다.
Netflix는 DGS 개발 초기부터 좋은 모듈화에 집중했는데, 이는 프레임워크의 대부분을 오픈소스로 사용할 수 있게 해주는 효과가 있었다고 한다. Netflix의 많은 애플리케이션은 Java8을 사용하고 있었기 때문에 Java9 에 도입된 모듈 시스템을 사용할 수 없었고, 대신 Graph api 및 구현 모듈을 사용하여 좋은 모듈 구조를 만들 수 있었다.
DGS Framework 는 Spring Boot를 기반으로 하며, 전체적인 구성은 아래와 같다.
Netflix에는 인프라와 통합할 수 있는 Spring Boot용 확장 기능들을 많이 가지고 있다. DGS는 이러한 Tracing, Metrics, 분산 Logging, 인증/인가와 같은 인프라들과 통합되어 즉시 사용 가능한 좋은 경험을 제공할 것이다.
DGS Framework 시작해보기
샘플 프로젝트 생성
- 간단한 Spring Web 을 생성하고 아래와 같이 DGS 의존성을 추가한다.
- Schema 로 Class 자동 생성 Gradle Plugin 을 위해 Gradle 프로젝트를 권장한다.
- jcenter에서만 사용할 수 있는 JVM 라이브러리인 Apollo Federation 을 사용하기 위해 jcenter repository를 추가했다.
repositories {
mavenCentral()
jcenter()
}
dependencies {
implementation "com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:latest.release"
}
스키마 생성
- DSG Framework 는 기존 방식에 따라 Schema 우선 개발로 설계되었다.
- DSG는 디폴트로 스키마 파일을 src/main/resources/schema/*.graphqls 을 탐색한다.
- /src/main/resources/schema/schema.graphqls 파일을 생성한다.
type Query {
shows(titleFilter: String): [Show]
}
type Show {
title: String
releaseYear: Int
}
- schema.graphqls 파일을 생성하니 JS GraphQL IntelliJ plugin 을 추천해준다. 설치하자.
도메인과 Fetcher Class 추가
- Show Class를 작성한다. 필드는 title 과 releaseYear 를 추가했다.
package kr.revfactory.dgssample;
public class Show {
private final String title;
private final Integer releaseYear;
public Show(String title, Integer releaseYear) {
this.title = title;
this.releaseYear = releaseYear;
}
public String getTitle() {
return title;
}
public Integer getReleaseYear() {
return releaseYear;
}
}
- Show 도메인 Query 처리를 위한 Show Fetcher 클래스를 추가한다.
- 예제에서는 임시 데이터로 shows 리스트를 담았다.
- Fetcher 클래스에 @DgsComponent 을 추가하고, 처리 메소드에는 @DgsData 를 추가한다.
package kr.revfactory.dgssample;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsData;
import com.netflix.graphql.dgs.InputArgument;
import java.util.List;
import java.util.stream.Collectors;
@DgsComponent
public class ShowsDataFetcher {
private final List<Show> shows = List.of(
new Show("Stranger Things", 2016),
new Show("Ozark", 2017),
new Show("The Crown", 2016),
new Show("Dead to Me", 2019),
new Show("Orange is the New Black", 2013)
);
@DgsData(parentType = "Query", field = "shows")
public List<Show> shows(@InputArgument("titleFilter") String titleFilter) {
if(titleFilter == null) {
return shows;
}
return shows.stream().filter(s -> s.getTitle().contains(titleFilter)).collect(Collectors.toList());
}
}
The End
- 이것만으로 예제 구현은 끝이다.
Test
- DGS 프레임워크에는 GraphQL Query 수행을 위한 GraphiQL 를 함께 제공한다.
- 애플리케이션을 실행하고 http://localhost:8080/graphiql 로 접속한다. (철자 주의 : 중간에 i 포함)
- GitHub API V4 에서 보던 익숙한 Graphiql Explorer 가 나타난다.
- GraphQL Query를 수행해보자.
{
shows {
title
releaseYear
}
}
Gradle Plugin 으로 Class 자동 생성
위에서는 Schma 파일을 작성하고 Show Class도 직접 작성했다. 위에서 언급한 것처럼 DGS 에서는 Schema 파일을 이용하여, Class도 자동으로 생성할 수 있는 기능을 제공한다. QueryDSL 을 사용해보면 Q** Class 를 생성하는 것과 유사한 방식으로 Build 시 Schema파일에 정의된 내용을 기준으로 자동으로 Class가 생성된다.
build.gradle 설정
- Gradle plugin 추가
// Using plugins DSL
plugins {
id "com.netflix.dgs.codegen" version "4.0.10"
}
- 빌드 스크립트에서 Class Path를 설정하고, 생성될 패키지를 지정한다.
- packageName 은 자신의 프로젝트에 맞게 수정한다.
buildscript {
dependencies{
classpath 'com.netflix.graphql.dgs.codegen:graphql-dgs-codegen-gradle:latest.release'
}
}
apply plugin: 'com.netflix.dgs.codegen'
generateJava{
schemaPaths = ["${projectDir}/src/main/resources/schema"] // List of directories containing schema files
packageName = 'com.example.packagename' // The package name to use to generate sources
generateClient = true // Enable generating the type safe query API
}
- build를 수행하고 나면 프로젝트의 build/generated 디렉토리에 Class가 자동으로 생성된다.
기존 Type 맵핑
- Codegen 은 몇가지 예외 케이스를 제외하고 Schema에서 찾은 각 Type을 아래와 같이 규칙으로 생성한다.
1. 기본 Scala Type 은 Java/Kotlin 에 상응하는 타입(String, Interger etc.)으로 맵핑
2. Data와 Time Type은 java.time 패키지의 클래스로 맵핑
3. PageInfo 와 RelayPageInfo 는 graphql.relay 클래스로 맵핑
4. typeMapping 구성을 통해 맵핑
typeMapping 구성은 아래와 같이 gradle 설정으로 추가 가능하다.
generateJava{
typeMapping = ["MyGraphQLType": "com.mypackage.MyJavaType"]
}
Client API 생성
- Codegen 을 이용하여 Class를 생성시, Client 에서 사용가능한 Builder 가 포함된다.
- 이 Builder를 이용하면 Client도 손쉽게 GraphQL Query 작성이 가능해진다. (유레카!)
- 아래는 그 사용 예 이다.
schema.graphqls
type Query @extends {
ticks(first: Int, after: Int, allowCached: Boolean): TicksConnection
}
type Tick {
id: ID
route: Route
date: LocalDate
userStars: Int
userRating: String
leadStyle: LeadStyle
comments: String
}
type Votes {
starRating: Float
nrOfVotes: Int
}
type Route {
routeId: ID
name: String
grade: String
style: Style
pitches: Int
votes: Votes
location: [String]
}
type TicksConnection {
edges: [TickEdge]
}
type TickEdge {
cursor: String!
node: Tick
}
Client - GraphQLQueryRequest 작성
GraphQLQueryRequest graphQLQueryRequest =
new GraphQLQueryRequest(
new TicksGraphQLQuery.Builder()
.first(first)
.after(after)
.build(),
new TicksConnectionProjection()
.edges()
.node()
.date()
.route()
.name()
.votes()
.starRating()
.parent()
.grade());
Query 수행 로직
@Metatron("spinnaker-app-name-goes-here")
private RestTemplate dgsRestTemplate;
private ObjectMapper mapper = new ObjectMapper();
private static HttpEntity<String> httpEntity(String request) {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<>(request, headers);
}
Map<String, String> request = Collections.singletonMap("query", graphQLQueryRequest.serialize());
// Invoke REST call, and get the "ticks" from data.
JsonNode node = dgsRestTemplate.exchange(URL, HttpMethod.POST, httpEntity(mapper.writeValueAsString(request)),
new ParameterizedTypeReference<JsonNode>() {
}).getBody().get("data").get("ticks");
//Convert to the response type
TicksConnection ticks = mapper.convertValue(node, TicksConnection.class);
이상 여기까지 Netflix 에서 공개한 DGS에 대한 기본적인 내용들을 살펴보았다.
예제들과 문서들이 잘 나와 있으니 학습하는데는 어려움은 없을 것 같다.
v3.0.0 공개 이후, 현재 2021년 2월 3일 v3.3.0 버전이 릴리즈 되었다.
Netflix에서 어느정도 검증을 하고서 3.0.0 으로 공개 릴리즈 되지 않았을까 추측해본다.
아마도 이번 DGS의 공개로 인해 GraphQL을 사용하게 되는 프로젝트들이 더 많아지지 않을까 예상이 되는데 좀 더 복잡한 프로젝트들에 적용 했을때 얼마나 더 유용할지 기대가 된다.
Reference
- netflix.github.io/dgs/getting-started
- github.com/Netflix/dgs-examples-java
- www.baeldung.com/spring-graphql