백엔드기술/스프링프레임워크

Netflix DGS (Domain Graph Service) 시작하기

RevFactory 2021. 2. 7. 16:49

 

 

최근 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에 응답한다. 

Federated GraphQL Architecture with Shows and Reviews DGSs (출처 Netflix Tech Blog)

 

Netflix는 DGS 개발 초기부터 좋은 모듈화에 집중했는데, 이는 프레임워크의 대부분을 오픈소스로 사용할 수 있게 해주는 효과가 있었다고 한다. Netflix의 많은 애플리케이션은 Java8을 사용하고 있었기 때문에 Java9 에 도입된 모듈 시스템을 사용할 수 없었고, 대신 Graph api  및 구현 모듈을 사용하여 좋은 모듈 구조를 만들 수 있었다.

 

DGS Framework 는 Spring Boot를 기반으로 하며, 전체적인 구성은 아래와 같다.

DGS Framework with Netflix and OSS modules (출처 Netflix Tech Blog)

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 을 추천해준다. 설치하자.

JS GraphQL

도메인과 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
    }
}

http://localhost:8080/graphiql

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에 대한 기본적인 내용들을 살펴보았다.

 

예제들과 문서들이 잘 나와 있으니 학습하는데는 어려움은 없을 것 같다.

- dgs-examples-java

- dgs-examples-kotlin

- dgs-federation-example

 

v3.0.0 공개 이후, 현재 2021년 2월 3일 v3.3.0 버전이 릴리즈 되었다.

Netflix에서 어느정도 검증을 하고서 3.0.0 으로 공개 릴리즈 되지 않았을까 추측해본다.

아마도 이번 DGS의 공개로 인해 GraphQL을 사용하게 되는 프로젝트들이 더 많아지지 않을까 예상이 되는데 좀 더 복잡한 프로젝트들에 적용 했을때 얼마나 더 유용할지 기대가 된다.

 

 

Reference

- netflixtechblog.com/open-sourcing-the-netflix-domain-graph-service-framework-graphql-for-spring-boot-92b9dcecda18

- netflix.github.io/dgs/getting-started

- github.com/Netflix/dgs-examples-java

- www.baeldung.com/spring-graphql