エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

フロントエンド向けの API サーバリニューアルに GraphQL を検討している話

エムスリーでマルチデバイスチームのチームリーダーをしている松原@ma2geです。 マルチデバイスチームはこちらのテックブログでは初出なので簡単に紹介すると、iOSAndroid 等のデバイス対応を主導する開発するチームで、主に iOS, Android のネイティブアプリ開発から、アプリから叩く API サーバ(いわゆる Backends For Frontends(BFF))、プッシュ通知基盤システムのバックエンドサービスも開発しております。 私自身は3月までは別チームで Rails/Java/Elixir などを触っていましたが、4月から現チームに移動しこちらでもまた新たな挑戦をさせてもらっています。 💪

前提

今回はネイティブアプリ向けの RESTful な API サーバがレガシーとなっており、このサーバのリニューアルを検討している話を書きます。 対象のサーバはフレームワークが古く、諸事情により刷新することを前提としています。 せっかくなので技術的に新しいことも取り入れたく、リニューアルにあたって解決したい課題、選定する技術をチームで議論したところ、RESTful で行くか GraphQL で行くかについて決めかねる状況になっています。

※gRPC も検討時に候補として上がっていますが、今回の記事では触れません。

ちなみに RubyKaigi で「Ask Me Anything about GraphQL」Tシャツを来た方に、2018 年にもなって GraphQL 使っていないとかどういうこと?って煽られたことはしばらく忘れないと思います。

f:id:ma2gedev:20180703211931p:plain
Ask Me Anything About GraphQL Tシャツのイメージ

GraphQL については森が入門記事を書いているため、よければご覧ください。

RESTful vs GraphQL

今回のリニューアルに当たって特に改善したい課題は以下の通りです。

  • クライアントの自動生成(今までは手作り)
  • ドキュメントの管理を同一レポジトリで実装と差分なく行いたい(今までは別となっていた)

これらは GraphQL を使うことでどちらも解決できそうですが、今まで通り RESTful で行く場合も OpenAPI(Swagger) を使うことでほぼ同様の解決を行えます。

GraphQL にすることでさらに以下のメリットがあると理解しつつも、これらは今回は必須ではありません。

  • スキーマファーストな開発
  • 複数のリクエストをまとめる
  • 今後 Web のフロントエンドから叩くバックエンドサーバとしても機能させるのが容易そう

また GraphQL は今まで社内での採用事例がなく、RESTful で行くか GraphQL で行くか決定するための情報が不足しており、以下のような点が不明瞭でした。

  • ネイティブアプリなどから呼び出すクライアントは本当に自動生成できるのか、それの使い勝手はどうか
  • 生成されるドキュメントは要望を満たせるのか
  • GraphQL で作ることでバックエンドの作りが複雑になるのではないか(開発のイメージがついていない)

そこで GraphQL についての判断材料を増やすために今回はプロトタイピングを行うことにしました。

GraphQL でのプロトタイピング

プロトタイピングなので手軽な技術を使ってみることにします。 今回対象のサーバは Java で出来ており、弊社の各種 Microservice の API を叩くいわゆる BFF となっています。 そのため Java の資産が使えて、チームの技術スタックとしてもマッチする、Kotlin と Spring Boot をベースとして、GraphQL 部分には graphql-java の各種ライブラリを用いて検証しました。 以下に記載したサンプルコードはこちらへ上げましたので合わせてご覧ください。

GraphQL 実装の下準備

Spring Boot の雛形を https://start.spring.io/ からダウンロードし、下記 GraphQL 関連のライブラリを依存関係に追加することで簡単なサンプルの準備はすぐに出来ます。

# build.gradle から抜粋

dependencies {
  # 省略...
  compile "com.graphql-java:graphiql-spring-boot-starter:4.2.0"
  compile "com.graphql-java:graphql-spring-boot-starter:4.2.0"
  compile "com.graphql-java:graphql-java-tools:5.1.0"
}

GraphQL の簡単なサンプル実装

開発の流れを掴むために簡単な例をあげます。

GraphQL のスキーマ定義は src/main/resources/graphql/ に .graphpqls 拡張子でおきます。 例えば中身はこのような感じです。AuthorBook の型があり、クエリとして Author を取得するための getAuthorById フィールドを定義しています。

type Author {
    id: ID!
    name: String!
    books: [Book]!
}

type Book {
    id: ID!
    name: String!
}

extend type Query {
    getAuthorById(id: ID!) : Author
}

上記スキーマに対応する処理を書きます。 まずは AuthorBook に対応するデータクラスを作ります。

data class Author(
    val id: Int,
    val name: String
)

data class Book(
    val id: Int,
    val name: String,
    val authorId: Int
)

そしてクエリのフィールド getAuthorById に対応するリゾルバを書きます。GraphQLQueryResolver を継承したクラスにクエリのフィールド名に対応する関数を書くことで、graphql-java-tools が対応する処理を紐づけてくれます。

@Component
class AuthorQueryResolver(private val authorDao : AuthorDao) : GraphQLQueryResolver {
    fun getAuthorById(id: Int) = authorDao.getAuthorById(id)
}

AuthorDao が出てきましたが、ここでは感覚を掴むために実際のデータへ繋ぐのではなく簡易的なサンプルとして以下の実装をしています。

@Component
class AuthorDao {
    private val data = mutableListOf(
        Author(id = 1, name = "taka"),
        Author(id = 2, name = "susan")
    )

    fun getAuthorById(id: Int) = data.firstOrNull { author -> author.id == id }
}

最後にスキーマAuthor にある books フィールドについてですが、データクラスの Author には books プロパティがありません。 そのため対応するリゾルバを定義しますが、こちらはクエリではないので GraphQLQueryResolver ではなく、GraphQLResolver を使います。 GraphQLResolver に対応する型として Author を指定し、そのフィールド名である books に対応する関数を定義することで紐づけることができます。 また books にはそのフィールドを持つ Author のオブジェクトが渡されてきます。これによって以下の通り N+1 の問題を回避することが可能になっています。

@Component
class BookResolver(private val bookDao : BookDao) : GraphQLResolver<Author> {
    fun books(author: Author) = bookDao.getBooksByAuthorId(author.id)
}

BookDao はこのように実装しています。

@Component
class BookDao {
    private val data = mutableListOf(
        Book(id = 1, name = "Elixir入門", authorId = 1),
        Book(id = 2, name = "Erlang入門", authorId = 2),
        Book(id = 3, name = "Kotlin入門", authorId = 2),
        Book(id = 4, name = "Rust入門", authorId = 2)
    )

    fun getBooksByAuthorId(authorId: Int) = data.filter { book -> book.authorId == authorId }
}

ここまできたら $ ./gradlew bootRun でアプリケーションを起動をして、http://localhost:8080/graphiql へアクセスします。 左上のペインで以下のクエリを入力して実行すると、右のペインへ結果が表示されるはずです。

{
  getAuthorById(id: 2){
    id
    name
    books{
      id
      name
    }
  }
}

一連の開発の流れとしてはスキーマ定義し、型に対するモデルを定義、対応するリゾルバを作りモデルを返す、ということが確認できました。

これを元に既存 API を実際に実装してみましたが、大まかな開発のイメージとしては RESTful で今までやっていたものと大きく異なるものではないということが確認できました。 graphql-java-tools のようなライブラリを使うことで、GraphQL だからと言って開発が大変になるようなことはなさそうです。ただしシンプルではない複雑なクエリについては、さらに検証が必要と考えています。

ドキュメンテーション

ドキュメントについては、GraphiQL がとても便利なツールとなっております。 スキーマが定義されていれば、http://localhost:8080/graphiql アクセスした際の右ペインで定義されているスキーマ情報を確認することができます。 ただ、スキーマ情報だけではクエリや型に対する説明が足りておらずドキュメントとして情報が不足しています。 こちらについては以下のように .graphqls ファイルへコメントを加えてあげることで説明文を追加することができるようになっています。ちなみにこちらも graphql-java-tools が頑張ってくれています。そのおかげで Swagger UI のような便利さも手に入れることができていると感じます。

# 著者
type Author {
    id: ID!
    # 著者名
    name: String!
    # 著者の本
    books: [Book]!
}

# 本
type Book {
    id: ID!
    # 名前
    name: String!
}

extend type Query {
    # 著者を取得する
    getAuthorById(id: ID!) : Author
}

クライアント側

クライアントの自動生成については Android のプロジェクトで試しました。使用したライブラリは Apollo GraphQL Client for Android を使いました。 今時のライブラリっぽく RxJava2 にも対応しているので、既存のアプリから使うのにもちょうど良いです。

こちらについてアプリへの導入は ApolloREADMEサンプルアプリを見ていただくのが良いと思います。

流れとしては .graphql のファイルにアプリで必要とするクエリを定義してやると、それを元にクライアントが自動生成されます。 この辺りは GraphQL の柔軟さをうまく取り込んでいるなと感じます。

例えば上で例として出した getAuthorById クエリについて例を出すと、src/main/graphql/com/example/sample/api/API.graphql というファイルを作って以下のように中身を記載します。

query Author($id: ID!){
  getAuthorById(id: $id){
    id
    name
    books{
      id
      name
    }
  }
}

すると、AuthorQuery という名前でクエリ用のビルダークラスが生成され、レスポンスについてもクラスが作られ Response<AuthorQuery.Data> として受け取れるようになります。 今までは各 RESTful API についてシリアライズするようなコードを書いて手作りしていたので、それと比べるとかなり楽をできそうだと想像できました。

プロトタイピングしてみて

プロトタイピングを通して、一通りの開発の流れを追ってみることで、不明瞭だった以下の点について情報量を増やすことができました。

  • ネイティブアプリなどから呼び出すクライアントは本当に自動生成できるのか、それの使い勝手はどうか
  • 生成されるドキュメントは要望を満たせるのか
  • GraphQL で作ることでバックエンドの作りが複雑になるのではないか(開発のイメージがついていない)

ちなみにこの他にも認証をどうするか、並列処理はできるのかなど、あまりクリアでなかった点も合わせて確認したり、メンバーから受けた指摘をさらに検証したりといったことをしています。

まとめ

プロトタイピングした本人というのもありますが、個人的には今のところ GraphQL に傾いています。 ただそのまま1人の意見で決めてしまうのは好ましくないので、チームメンバーにもフィードバックをもらいつつ進めていってます。 エムスリーでは自分たちの環境は、自分たちで良くしていくという文化があると思っています。 その一部を見せられたらなと思い、今回検討途中ではありましたが検討の過程を記事にしてみました。

マルチデバイスチームではエンジニアを募集中です

マルチデバイスチームでは AndroidiOS、サーバサイドエンジニアを募集しています。 私も3ヶ月ほど現チームで仕事をしてみて思ったのですが、サーバサイドやりつつ機会があれば AndroidiOS にも手を出せる(あるいはその逆も)という環境のあるチームなので、キャリアを広げたい人にも向いている環境ではないかと感じています。 もし興味がわいた方は、カジュアルにお話しましょう。ぜひエンジニア向けフォームよりご連絡お願いします。 もちろんtwitterで私に直接声をかけていただいても構いません。

参考リンク