티스토리 뷰

# 시작

저번 포스팅에서 Spring Boot에 GraphQL 연동을 마쳤습니다.

다만 GraphqlAPI.java에 DataFetcher를 등록하는 방식이 영 마음에 들지 않습니다.

쿼리 하나 추가하면 GraphqlAPI.java에도 DI 추가하고... DataFetcher 추가하고...

그래서 이번 포스팅에서는 어노테이션과 리플렉션을 이용하여 애플리케이션이 시작될 때 DataFetcher가 등록되도록 해보겠습니다.


# 어노테이션 생성

com/graphql/blog/util/annotation디렉터리를 생성하고 아래 파일들을 생성합니다.

GqlDataFetcher.java

package com.hello.graphql.util.graphql;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GqlDataFetcher {
  GqlType type() default GqlType.QUERY;
}

DateFetcher를 반환하는 메서드에 선언할 메서드 어노테이션입니다.

스프링 컨테이너가 구성될 때 이 어노테이션이 선언된 메서드를 GraphqlAPI에 등록할 겁니다.


Gql.java

package com.hello.graphql.util.graphql;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Gql {}

GqlDataFetcher 어노테이션이 선언된 메서드가 있는 클래스인지 식별하기 위한 어노테이션입니다.

이 어노테이션으로 DefaultListableBeanFactory를 이용하여 현재 등록된 Bean 중에서 DataFetcher 클래스를 식별합니다.


GqlType.java

package com.graphql.blog.util.annotation;

public enum GqlType {
  QUERY("Query"), MUTATION("Mutation");

  private String value;

  GqlType(String value) {
      this.value = value;
  }
  public String getValue() {
      return this.value;
  }

}

GqlDataFetcher 어노테이션을 선언할 때 쿼리의 타입을 명시하는 Enum 클래스입니다.

GraphQL에서 자원을 가져올 때는 query를, 자원을 변경할 때는 mutation을 명시적으로 작성하며,

query는 생략이 가능하지만 mutation은 생략할 수 없습니다.

// 아래 두 쿼리는 동일합니다.
query {
  city(id: 3) {
    id
    name
  }
}

{
  city(id: 3) {
    id
    name
  }
}

// mutation은 반드시 명시해주어야 합니다.
mutation {
  putCity(name: "NewCity") {
    id
    name
  }
}

# GraphqlAPI 수정

이제 GraphqlAPI.java에 어노테이션을 가진 메서드를 호출하여 DataFetcher를 등록하는 소스를 추가합니다.

package com.graphql.blog.config;

import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Map;

import javax.annotation.PostConstruct;

import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import com.graphql.blog.util.annotation.Gql; // +++++
import com.graphql.blog.util.annotation.GqlDataFetcher; // +++++

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.DefaultListableBeanFactory; // +++++
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import graphql.GraphQL;
import graphql.schema.DataFetcher;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.RuntimeWiring.Builder; // +++++
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import graphql.schema.idl.TypeRuntimeWiring;

@Component
public class GraphqlAPI {

  // +++++
  @Autowired DefaultListableBeanFactory beanFactory;

  private GraphQL graphQL;

  @Value("classpath:static/graphql/schema.graphqls") 
  Resource resource;

  @Bean 
  public GraphQL graphQL() {
    return graphQL;
  }

  @PostConstruct
  public void init() throws IOException {
    URL url = resource.getURL();
    String sdl = Resources.toString(url, Charsets.UTF_8);
    GraphQLSchema graphQLSchema = buildSchema(sdl);
    this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
  }

  private GraphQLSchema buildSchema(String sdl) {
    TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
    RuntimeWiring runtimeWiring = buildWiring();
    SchemaGenerator schemaGenerator = new SchemaGenerator();
    return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
  }

  // +++++
  private RuntimeWiring buildWiring() {
    return createBuilderByAnnotation().build();
  }
  
  // +++++
  public Builder createBuilderByAnnotation () {
    Builder builder = null;
    try{
      // (1)
      builder = RuntimeWiring.newRuntimeWiring();
      // (2)
      Map<String, Object> classes = beanFactory.getBeansWithAnnotation(Gql.class);
      Class<?> clz = null;
      GqlDataFetcher gdf = null;
      // (3)
      for( Object obj : classes.values() ){
        clz = obj.getClass();
        // (4)
        for( Method mtd : clz.getMethods() ){
          // (5)
          if( mtd.isAnnotationPresent(GqlDataFetcher.class) ){
            // (6)
            gdf = mtd.getAnnotation(GqlDataFetcher.class);
            builder.type(
              // (7)
              TypeRuntimeWiring
              .newTypeWiring( gdf.type().getValue() )
              .dataFetcher( mtd.getName(), (DataFetcher<?>) mtd.invoke(obj) )
            );
          }
        }
      }
    } catch(Exception e) {
      e.printStackTrace();
    }
    return builder;
  }

}

[+++++] 기존 소스에서 추가 및 변경된 부분입니다.

createBuilderByAnnotation 메서드로 RuntimeWiring 객체를 생성합니다.

(1) RuntimeWiring의 Builder 객체 생성

(2) DefaultListableBeanFactory에서 Gql 어노테이션이 선언된 Bean들을 식별하여 가져옵니다.

(3) 가져온 Bean들을 루프 돌립니다.

(4) 각 Bean들의 메서드들을 루프 돌립니다.

(5) GqlDataFetcher 어노테이션이 선언된 메서드가 있다면 

(6) 해당 메서드의 GqlDataFetcher 어노테이션 인스턴스를 가져옵니다.

(7) 어노테이션의 GqlType 정보로 쿼리 타입을 설정하고 메서드를 실행하여 반환된 DataFetcher를 등록합니다.

※어노테이션이 선언된 메서드의 이름이 쿼리명이 됩니다.


설정이 모두 끝났으니 마지막으로 com/graphql/blog/sample/CityDataFercher.java에 어노테이션을 적용할 차례입니다.

아래와 같이 수정합니다.

package com.graphql.blog.sample;

import com.graphql.blog.util.annotation.Gql; // +++++
import com.graphql.blog.util.annotation.GqlDataFetcher; // +++++
import com.graphql.blog.util.annotation.GqlType; // +++++

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import graphql.schema.DataFetcher;

@Gql // +++++
@Component
public class CityDataFetcher {
  
  @Autowired
  private CityRepository cityRepository;

  @GqlDataFetcher(type=GqlType.QUERY) // +++++
  public DataFetcher<?> allCities () {
    return environment -> {
      return cityRepository.findAll();
    };
  }

  @GqlDataFetcher(type=GqlType.QUERY) // +++++
  public DataFetcher<?> city () {
    return environment -> {
      int id = environment.getArgument("id");
      return cityRepository.findById(id);
    };
  }

}

[+++++] 기존 소스에서 추가 및 변경된 부분입니다.

앞으로는 DataFetcher 메서드를 만들고 나서 어노테이션을 지정해준 뒤 메서드 이름과 동일한 쿼리를 정의하면 

바로 데이터를 호출할 수 있습니다.

※어노테이션이 선언된 메서드의 이름이 호출하는 쿼리명이 됩니다.

    만일 따로 설정하고 싶다면 GqlDataFetcher.java에 쿼리명을 지정하는 변수를 선언하고

    GraphqlAPI.java에서 해당 변수에 지정된 값이 있을 때 쿼리명이 되도록 수정하면 됩니다.


# 실행

$ gradle bootRun

서버를 시작한 후 PlayGround를 실행해서 DataFetcher가 제대로 등록되었는지 확인합니다.

데모 사이트(https://www.graphqlbin.com/v2/6RQ6TM)

EndPoint : http://localhost:8000/graphql

테스트 쿼리를 날려봅니다.

정상적으로 데이터가 호출되네요. 그럼 이번엔 id가 5인 도시 정보를 가져와봅시다.

마찬가지로 정상적으로 호출되었습니다.


# 마치며

이것으로 [나만의 블로그] 백엔드 기본 세팅이 전부 끝났습니다.

다음 포스팅에서는 FrontEnd 세팅을 시작합니다.

이 프로젝트에서 쓰인 방식 외에도 GraphQLHttpServlet이나 GraphQLResolver 등 다양한 방법들이 있습니다.

궁금하다면 아래 링크를 참고하세요.

https://www.graphql-java-kickstart.com/tools/

 

GraphQL Java Kickstart

About GraphQL Java Tools This library allows you to use the GraphQL schema language to build your graphql-java schema. Inspired by graphql-tools, it parses the given GraphQL schema and allows you to BYOO (bring your own object) to fill in the implementatio

www.graphql-java-kickstart.com


# GitHub

https://github.com/eonnine/MyBlog

 

eonnine/MyBlog

Spring Boot, Graphql, PostgreSQL, React-Apollo, Parcel - eonnine/MyBlog

github.com

 



2019-09-02 추가

 

Annotation을 이용해서 Spring Boot에 GraphQL을 빠르고 쉽게 쓸 수 있는 라이브러리가 있었습니다.

다음에 또 Spring Boot에 GraphQL을 사용할 일이 있다면 이걸 쓸 것 같네요.

https://github.com/leangen/graphql-spqr

 

leangen/graphql-spqr

Java 8+ API for rapid development of GraphQL services - leangen/graphql-spqr

github.com


 

'프로젝트 > 나만의 블로그' 카테고리의 다른 글

React & Apollo [FE]  (0) 2019.08.24
React & Parcel 개발 환경 구성 [FE]  (0) 2019.08.24
Spring Boot & GraphQL (1) [BE]  (0) 2019.08.18
Spring Boot & PostgreSQL [BE]  (0) 2019.08.18
Spring Boot 개발 환경 구성 [BE]  (0) 2019.08.18
댓글