サービス開発を加速させる技術選定 in Kibela

2017/07/18 Service Dev Meetup #1 の資料です。

会場は Speee さんに提供していただきました。ありがとうございました。

自己紹介

f:id:gfx:20170720100419p:plain

今日の話

  • Kibelaにおける技術選定とは
  • やらないこと
  • やったこと
  • これからやること

Kibelaにおける技術選定とは

  • 自分(@gfx)にとっては技術的挑戦は精神衛生上必要なこと
    • 興味のある分野に限るが…
  • スタートアップにとってはサービスの成長が最も重要
  • 技術的挑戦によって機能を磨いて差別化にも繋げられればそれがベスト

しかし、コスパの悪い挑戦はしない

  • リスクが未知数
  • 新しすぎて時期尚早
  • 作業量が膨大

挑戦しない例

❌ ウェブエディタのフルスクラッチ開発

  • エディタで気持ちよく書けるのはサービスとして非常に重要
  • では、 :star2: 最高のエディタ:star2: をフルスクラッチ開発するか?
    • ❌ No!
    • 自由度はあがる
    • しかしKibelaにおける「理想のエディタ」がどんなものかもまだ見えていない
      • WYSIWIG editor vs plain text editor?
    • 現時点だとリスクもコストもベネフィットも未知数すぎる
    • いまはまだやるべきじゃない
  • フルスクラッチでのエディタ開発をやっているところはある
  • KibelaではFacebookのDraftJSを採用
    • しかしDraftJSはいろいろ問題があった
      • カーソルが飛んで行方不明になることがある
      • 入力できなくなることがある
      • スマホでの日本語を入力できないことがある
    • DraftJSは開発も停滞しておりFacebookも本番導入予定はなさそう
    • つまりフルスクラッチ開発に踏み切るは「Facebookにできなかったことが我々ならできる」という確信が必要
  • KibelaのエディタはいまCodeMirrorに置き換え作業中
    • 設計は古いが実装は枯れている
    • GitHubが採用しているのもCodeMirrorなので安心

❌ SPA: Single Page Application

  • UX向上の見込みはあるが、今はまだ開発コストに見合わないという判断
  • 「UX向上 > コスト」となる画面遷移はありそうなのでそこはいずれSPAにする
    • たとえばホーム画面からエントリ画面への遷移はSPAにすると高速になると期待できる

挑戦したこと

TypeScript

  • Microsoftが開発している静的型をもったaltJS
  • 構文はほぼJavaScriptと同じ

TypeScriptのメリット

  • TS導入により 生産性があがり、バグが少なくなる ことを期待できる
  • 静的型は「制約によりコードの整合性を保つためのLint」として振る舞う
  • TypeScriptコンパイラが提供する language-service が優秀で、メジャーなエディタだと補完やリファクタが効く

f:id:gfx:20170720100454p:plain

  • RubyMineでブラウザの File objectのメソッドやプロパティをを補完している様子

TypeScriptに挑戦した理由

  • TypeScriptはリリース当初(2012年)から目を付けていて、情報を追っていたのでリスクについてはある程度予想できた
    • 初期はハマりどころがものたくさんあってオススメできる感じではなかった
  • ES 2015の策定とそれを踏まえた TypeScript 2.0 (2016年)によって不満点がほぼなくなったので、導入のきっかけ探していた
  • KibelaにReact Reduxの導入するタイミングで静的型がほしくなり、導入を決意した

TypeScriptとの付き合い方

  • TypeScriptについては過度に期待しないのがよい
    • ランタイムとしては良くも悪くもただのJavaScript
    • 実行時に影響をおよぼす特別な機能はない
  • 型付けは がんばらない方針。よくわからなければanyでいい
  • 型定義ファイル (@types/*) にもこだわらない。別に使わなくていいし、使っていてもおかしければすぐ捨ててよい
  • TSとRailsとの統合でいくつか足りない機能があったのでそのへんは作った

Railsのルーティングヘルパーと仲良くする

  • Railsのルーティングヘルパー: wiki_path(blog)"/wikis/42"
    • 簡単なクエリビルダつき: wikis_path(page: 1, per: 20)"/wikis?page=10&per=20"
  • 当然ながらJavaScriptからは利用できない
  • 作った: https://github.com/bitjourney/ts_routes-rails
import { wikisPath, wikiPath } as Routes from './generated/routes';

console.log(wikiPath(42)); // "/wikis/42"
console.log(wikisPath({ page: 1, per: 20 })); // "/wikis?page=1&per=20"
  • ある程度静的型が付いて、補完も効きやすいので最高

Railsの画像アセットと仲良くする

import * as React from 'react';
import { ImageRubyIcon } from './generated/assets';

class MyComponent extends React.Component<any, any> {
  render() {
    return (
      <ImageRubyIcon // for app/assets/images/ruby-icon.svg
        className='rubyLogo'
      />
    );
  }
}

これから挑戦したいこと

  • GraphQL
    • これから話す
  • React Native
    • 今日は話しません

なぜGraphQLに注目したか

  • モバイルアプリを開発するにあたってWeb APIがほしい
  • 将来的にWeb APIを公開したい
  • なるべくコスパよく!

GraphQL

  • RESTの次の地位を狙う Web APIのリクエストフォーマット を定めるもの
  • インターフェイスの定義はエンジニアが記述する
    • DBアクセスはActiveRecordを通じて(Railsの場合)
    • 検索はElasticsearchへのアクセスを実装したモデルを叩き
    • モデルの関連(association)も表現できる
  • 実サービスの例としては GitHub API v4 のAPI consoleを触るとよい
  • まだKibelaには導入していないものの、仕様にグッときたので導入を決意
    • RESTの延長にある雰囲気が気に入った
    • GitHubやFacebookという大手が使っているのも導入の決め手

例: GitHub API v4

request:

query {  # トップレベルは必ず "query" または "mutation"
  viewer {  # ログインユーザから見れるデータで…

    # ログインユーザが管理するリポジトリの最初の3個をudpated_atでソートして
    repositories(first: 3, orderBy: { field: UPDATED_AT, direction: DESC }) {
      nodes { # data nodeのリスト
        name # リポジトリの名前をリクエストする
        description # リポジトリの説明をリクエストする
      }
    }
  }
}

response:

{
  "data": {
    "viewer": {
      "repositories": {
        "nodes": [
          {
            "name": "ts_routes-rails",
            "description": "Exports Rails URL helpers to TypeScript"
          },
          {
            "name": "rouge",
            "description": "A pure-ruby code highlighter"
          },
          {
            "name": "pygments.rb",
            "description": "pygments syntax highlighting in ruby"
          }
        ]
      }
    }
  }
}
  • クエリの基本はシンプル
  • コレクションの操作が独特なので習熟が必要
  • API consoleの実装であるGraphiQL (グラフィクル)が優秀

説明付きでフィールドを補完してくれたり:

f:id:gfx:20170720100517p:plain

f:id:gfx:20170720100532p:plain

内蔵されているドキュメントビューアも優秀で、クエリをいじりながらすぐに検索できる。とはいえ、慣れると補完だけでほぼいけるようになる。

GraphQLに関するよくある誤解

  • レイヤ的にはあくまでも「アプリケーションのインターフェイス」を定めるもの
    • RDBMSやNoSQLなどのストレージのレイヤではない
    • というのは「 RESTでアクセスできるRDBMS」というのと同じ水準の話
    • MongoDBはHTTP interfaceがあるのでありえない話ではないが… :innocent:
  • Graph DB とは関係ない
  • 「 N+1が心配」⇢ RESTに置き換えて考える
    • 1 resource : 1 endpoint厳守のRESTだと簡単にクライアントサイドで N+1 になる
    • GraphQLの場合、サーバーサイドで完結する部分が多く工夫の余地があるぶんマシ
  • 「複雑なクエリで過負荷にならないか心配」⇢ RESTに置き換えて考える
    • RESTだとRate-Limitで制限するが、GraphQLは別の方法で制限する必要がある
  • gRPCなどのRPC frameworkと何が違う?
    • GraphQLは「必要最小限なデータを1リクエストで取得する」ことに集中している
      • 取得するフィールドを最低限に
      • 幾つかの型のリソースをまとめて
    • GraphQLの比較対象はRESTで、RPC frameworkではなさそう

コード例

KibelaのGraphQL pull-request(開発中)より抜粋:

f:id:gfx:20170720100600p:plain

たとえば単一のリソースの取得:

# トップレベルの "query" 型 (トップレベルはquery or mutation)
Types::QueryType = GraphQL::ObjectType.define do
  name "Query"

  # GraphQLは仕様としてdescriptionを書ける
  # descriptionはドキュメントビューアで使われる
  description "The query root of the schema"

  # 「blog」というリソースを宣言する
  field :blog do
    type Types::BlogType

    # blog リソースを取得するにはただひとつの必須引数 id が必要
    argument :id, !types.ID

    resolve ->(_object, args, context) {
      # current_userがアクセス可能なblogをidで検索する
      # 戻り値はtypeで宣言したTypes::BlogTypeで宣言されたフィールドを持つ
      Blog.accessible_for(context[:current_user]).find_by!(id: args["id"])
    }
  end
end
  • 結局、パラメータがあってそれを元にActiveRecord経由でデータを検索するという点においてRESTと違いはない
  • 全てのパラメータとレスポンスのフィールドを明示的に宣言しなければならないので、作業量としてはそれなりにある
    • とはいえ新規開発するなら作業内容が変わるだけといえる
  • 最終的には routes.rb からJSON APIを一掃できるのでスッキリ
  • 内部API / モバイルアプリ用API / 外部APIを実装上区別する必要がないのは大きい
    • 認証方法(それに伴うrate-limit的な制限)の違いはあるのでエンドポイントは変えるかも

懇親会で受けた質問

Q: GraphQLはRESTより制限しにくく想定外のクエリによる過負荷が心配。やはり外部公開APIには向かないのでは?

「GraphQLがどうか」だけを考えるとたしかにその通りだと思います。これは結局RESTと比較しないと答えが見えてこないテーマです。

たとえばRESTが単位時間あたりのリクエスト数を制限する(= Rate Limit)ように、GraphQLの場合は、リクエストをパースしたあとの構造を解析してリソース取得のネスト数やリソースの参照数などをRate Limitのような形で制限することで、RESTと同水準の制約は掛けられるのではないかと考えています。

N+1 問題も同じで、たとえばREST APIを大量に発行するN+1リクエストは、REST APIでは簡単に起きてしまいます。そしてRESTの場合、そういう大量にリクエストを送るパターンを、サーバーサイドの工夫で改善するのは難しいと思います。GraphQLの場合、リクエスト自体は1つなので、サーバーサイドで工夫できる余地があります。

総じてみると、GraphQLは「REST同様に問題が起きる可能性はあるが、RESTよりサーバーサイドで対応できる余地があるだけマシ」と言えるのではないかと思います。