XcodeGenによる新時代のiOSプロジェクト管理

こんにちは。モバイル基盤部の@giginetです。平成最後のエントリを担当させていただきます。

iOSアプリの開発では、Xcodeが生成するプロジェクトファイルである、*.xcodeprojをリポジトリで共有するのが一般的です。

しかし、この運用は大規模なプロジェクトになるほど、数多くの課題が発生します。

クックパッドiOSアプリは巨大なプロジェクトであり、通常の*.xcodeprojによる管理には限界が生じていました。

そこで、昨年秋にXcodeGenというユーティリティを導入し、プロジェクト管理を改善したので、その知見をお伝えします。

f:id:gigi-net:20190425150234g:plain

従来のプロジェクト管理の問題点

ファイル追加の度にコンフリクトが発生する

*.xcodeprojファイルはプロジェクトに含まれるソースファイルの管理を行っています。

開発者がプロジェクトにファイルを追加すると、このプロジェクトファイルが更新されることになります。

そのため、同時に数十人が開発するクックパッドiOSアプリの開発環境では、プロジェクトファイルのコンフリクトが日常茶飯事で、解消に多くの工数が発生していました。

レビューがしづらい

*.xcodeprojは特殊なテキストデータで表現されますが、とても人間が読める物ではなく、差分をレビューするのは困難です。

簡単なファイルの移動でも複数の差分が発生します。

大きな変更に向かない

クックパッドiOSアプリでは、巨大なビルドターゲットを分割し、ビルド時間を改善する『霞が関*1と呼ばれる取り組みを行っています。

マルチモジュール化を行っていくに当たって、ドラスティックな*.xcodeprojの変更に耐える必要がありました。

単なるファイル追加であれば、コンフリクトの解消や、レビューの難しさという問題はまだ解決可能でしたが、ターゲットやBuild Configurationの追加、大量のファイルの移動といったプロジェクトの変更をもはや人類が適切に扱うことは困難でした。

XcodeGenとは

そこで導入したのがXcodeGenです。

XcodeGenは、XcodeのプロジェクトデータをYAMLで記述し、定義から冪等に*.xcodeprojを生成できるユーティリティです。

このようなYAMLを定義し

targets:
  Cookpad:
    type: application
    platform: iOS
    sources:
      - path: Cookpad

XcodeGenを実行すると、*.xcodeprojを自動生成することができます。

$ xcodegen
Loaded project:
  Name: Cookpad
  Targets:
    Cookpad: iOS application
  Schemes:
    Cookpad
⚙️  Generating project...
⚙️  Writing project...
Created project at Cookpad.xcodeproj

f:id:gigi-net:20190425150303p:plain

このツールの導入により、数々の問題が解消できました。

導入して良かったこと

ファイルツリー構成が強制される

まず、*.xcodeprojの問題点として、ファイルシステム上のツリーと、プロジェクトの保持するツリーが一致しないという問題がありました。

追加されるファイルは、ファイルシステム上の階層と必ずしも一致しませんし、思い思いに追加されるため、プロジェクトが煩雑になります。

XcodeGenによる生成では、ファイルツリーからプロジェクト構造を生成するため、この不一致が解消されます。

targets:
  Cookpad:
    sources:
      - path: Cookpad

例えば、この指定では、Cookpad以下のファイル全てがCookpadターゲットに所属するため、ファイルシステム上の位置を強制することができます。

f:id:gigi-net:20190425150316p:plain

コンフリクト解消が不要に

上記の仕様による一番わかりやすい恩恵は、プロジェクト差分のコンフリクトからの解放です。

従来の*.xcodeprojでは、開発者がソースファイルを追加する度に更新が入り、差分が発生していました。

しかし、XcodeGenの仕様においては、ソースファイルの追加時にリポジトリへのファイル追加以外の操作が不要になり、一切のプロジェクトのコンフリクトがない世界が到来しました。

ターゲットの追加が容易に

上記の特性はビルドターゲットの追加にも役立ちます。XcodeGenでは、わずか数行のYAMLの定義のみでビルドターゲット追加を行うことができます。

targets:
  CookpadCore:
    type: framework
    platform: iOS
    sources:
      - CookpadCore

f:id:gigi-net:20190425150330p:plain

また、ターゲット間のソースファイルの移動も簡単です。 従来は、1ファイルごとにどのビルドターゲットでビルドされるか、という情報が保持されていたため、ファイルを移動する度にプロジェクトに大きな差分が発生していました。

しかし、XcodeGenでプロジェクトを生成することにより、特定のディレクトリ下のソースコードは、必ず特定のビルドターゲットに含まれることを保証することができるようになりました。

これにより、ビルドターゲット間の移動は単にgit mvするだけで済むようになりました。

この特性は、プロジェクトのマルチモジュール化に大きく役立ちました。

XcodeGenの導入

XcodeGenを導入したい場合、残念ながら既存の*.xcodeprojから簡単にプロジェクト定義ファイルを生成する方法はありません。

基本的には、ドキュメントを追いながら、生成結果を目で見て確認していきます。

GUIでの設定値をプロジェクト定義に忠実に移植する為には、既存の*.xcodeproj/project.pbxprojをテキストエディタで開き、設定値を探していくという地道な作業も発生しました。綺麗なプロジェクト定義を記述するには、Xcodeプロジェクトの構造をよく理解している必要があるでしょう。

最終的にクックパッドiOSアプリは、400行程度のYAMLファイルでほぼ元の挙動を再現することができました。

そこで、複雑なプロジェクトをXcodeGenの定義ファイルで記述するためのテクニックをいくつかご紹介します。

SettingGroup

まずはSettingGroupの機能です。 複数のターゲットで共通して利用したいビルドフラッグなどの設定をSettingGroupとして定義しておき、利用したいターゲットで読み込んで使用することができます。

settingGroups:
  SharedSettings:
    configs:
      OTHER_SWIFT_FLAGS: -DDEBUG
targets:
  Cookpad:
    type: application
    settings:
      groups: [Shared]
  OtherFramework:
    type: framework
    settings:
      groups: [Shared]

パッケージ管理

プロジェクトにCarthage*2でインストールしたライブラリを統合したい場合も簡単に記述できます。

targets:
  Cookpad:
    type: application
    platform: iOS
    dependencies:
      - carthage: RxSwift

このcarthage指定を用いるだけで、Embed Frameworkの設定や、Frameworkのコピーなど、Carthageの利用に必要な設定を自動で行ってくれます。

f:id:gigi-net:20190425150345p:plain

一方で、CocoaPodsを併用する場合、事態は複雑です。

CocoaPodsは、プロジェクトファイルにあとからビルド設定の注入を行う必要があるからです。現在のXcodeGenでは、プロジェクト定義だけでそれを管理することはできません。

例えば一連の処理をMakefileに記述するというアプローチが考えられるでしょう。

xcodegen
bundle exec pod install

ソースコード生成

Sourceryなどのコードジェネレーションと、XcodeGenを併用する場合には少し工夫が必要です。

XcodeGenによるプロジェクトツリーは、通常、存在しているソースファイルのファイルシステム上の構成により構築されます。

一方で、ソースジェネレーションを行うためには、他の定義からソースコードを生成するため、プロジェクトツリーが必要になります。 このように、鶏と卵問題が発生してしまうのです。

そこで、optionalオプションで、生成前のソースファイルの参照だけ持ち、先にプロジェクトを構築し、あとからソースジェネレーションを行うことでこの問題を解決しています。

targets:
  CookpadTests:
    type: unit-test
    platform: iOS
    sources:
      - path: "CookpadTests/AutoGenerated/AutoGenerated.swift"
        optional: true
        type: file

f:id:gigi-net:20190425150358p:plain

今後の課題

XcodeGenを大規模に運用している中で以下のような問題が発生しました。

主にプロジェクトの生成時間に関する課題で、現在解決している最中です。

CocoaPodsを利用するときの生成速度

CocoaPodsの利用時に、プロジェクト生成後、毎回pod installが必要なことは、先ほど触れました。

XcodeGenは冪等に実行されますが、それ故に、プロジェクト生成ごとにCocoaPodsによるビルド設定の注入を毎回行う必要が出てくるのです。

この仕組みでは、XcodeGenの生成ごとに毎回パッケージインストールが走り、数十秒の待ち時間が発生しています。

この問題を解決するアイディアはいくつかあります。

まずは、CocoaPodsによるプロジェクトの設定変更を無効化し、自分で依存関係を記述する方式です。*3

もう一つの方法は、CocoaPods 1.7で利用可能になったincremental_installを有効にすることです。 このオプションを有効にすることで、差分がある依存関係のみが生成されるため、プロジェクト生成速度が改善すると踏んでいます。

いずれの方式も構想段階でまだ実用できていません。

プロジェクトキャッシュの問題

毎回プロジェクトファイルを上書きしていると、希にXcodeのビルドキャッシュが無効になり、フルビルドが発生してしまう問題にも遭遇しています。

この問題は、生成されたプロジェクトの差分が発生しないようにしても再現しており、解決していく必要があります。

まとめ

ご覧いただいたように、XcodeGenを使ったプロジェクト運用は、クックパッドiOSアプリほどの規模であっても十分に実用できていると言えます。

*.xcodeprojで苦しむのは平成までです。皆さんもプロジェクトを破壊して新しい時代を迎えませんか。

クックパッドではXcodeプロジェクトのコンフリクト解消で消耗したくないエンジニアを募集しています。

*1:詳しくは2月に行われたCookpad Tech Confの資料をご覧ください https://techconf.cookpad.com/2019/kohki_miki.html

*2:ちなみに筆者は最近Carthageのコミッターになりました 💪

*3:この手法は integrate_targets というオプションを有効にすることで実現できますが、難しいのでここでは解説しません

RubyKaigi 2019 Cookpad Daily Ruby Puzzles の正解と解説

Ruby 開発チームの遠藤です。RubyKaigi 2019 が無事に終わりました。すばらしい会議に関わったすべてのみなさんに感謝します。

開催前に記事を書いたとおり、クックパッドからはのべ 7 件くらいの発表を行い、一部メンバは会議運営にもオーガナイザとして貢献しました。クックパッドブースでは、様々な展示に加え、エンジニアリングマネージャとトークをする権利の配布やクックパッドからの発表者と質疑をする "Ask the speaker" など、いろいろな企画をやりました。

クックパッドブースの企画の 1 つとして、今年は、"Cookpad Daily Ruby Puzzles" というのをやってみました。Ruby で書かれた不完全な Hello world プログラムを 1 日 3 つ(合計 9 問)配布するので、なるべく少ない文字を追加して完成させてください、というものでした。作問担当はクックパッドのフルタイム Ruby コミッタである ko1 と mame です。

RubyKaigi の休憩時間を利用して正解発表してました↓

問題と解答を公開します。今からでも自力で挑戦したい人のために、まず問題だけ掲載します。(会議中に gist で公開したもの と同じです)

問題

Problem 1-1

# Hint: Use Ruby 2.6.
puts "#{"Goodbye" .. "Hello"} world"

Problem 1-2

puts&.then {
  # Hint: &. is a safe
  # navigation operator.
  "Hello world"
}

Problem 1-3

include Math
# Hint: the most beautiful equation
Out, *,
     Count = $>,
             $<, E ** (2 * PI)
Out.puts("Hello world" *
         Count.abs.round)

Problem 2-1

def say
  -> {
    "Hello world"
  }
  # Hint: You should call the Proc.
  yield
end

puts say { "Goodbye world" }

Problem 2-2

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  yield "Hello world"
end

puts e.next

Problem 2-3

$s = 0
def say(n = 0)
  $s = $s * 4 + n
end

i, j, k = 1, 2, 3

say i
say j
say k

# Hint: Binary representation.
$s != 35 or puts("Hello world")

Problem 3-1

def say s="Hello", t:'world'
  "#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.

puts say :p

Problem 3-2

def say s, t="Goodbye "
  # Hint: You can ignore a warning.
  s = "#{ s } #{ t }"
  t + "world"
end

puts say :Hello

Problem 3-3

def say
  "Hello world" if
    false && false
  # Hint: No hint!
end

puts say

以下、ネタバレになるので空白です

自力で解いてみたい人は挑戦してみてください。











解答

では、解答です。重要なネタバレですが、すべての問題は 1 文字追加するだけで解けるようになってます。

Answer 1-1

作問担当は ko1 でした。問題再掲↓

# Hint: Use Ruby 2.6.
puts "#{"Goodbye" .. "Hello"} world"

解答↓

# Hint: Use Ruby 2.6.
puts "#{"Goodbye" ..; "Hello"} world"

"Goodbye" .. の後に ; を入れています。これにより、Ruby 2.6 で導入された終端なし Range (Feature #12912) になります。この Range は使われずに捨てられ、"Hello" が返り値になって文字列に式展開されるので、Hello world が出力されるようになります。

この問題の勝者は tompng さんでした。なお、tompng さんは 1-2 と 1-3 も最初に 1 文字解答を発見しましたが、勝者になれるのは 1 人 1 問だけ、としました。

Answer 1-2

作問担当は ko1 でした。問題再掲↓

puts&.then {
  # Hint: &. is a safe
  # navigation operator.
  "Hello world"
}

解答↓

puts$&.then {
  # Hint: &. is a safe
  # navigation operator.
  "Hello world"
}

&. の前に $ を入れて $&. にしています。$& は正規表現にマッチした部分文字列を表す特殊変数です。ここでは正規表現マッチは使われていないのでこの変数は nil になりますが、重要なのはこの書換によって puts メソッドに $&.then { "Hello world" } を引数として渡す、というようにパースされるようになることです。then メソッドはブロックの返り値を返すので、この引数は文字列 "Hello world" になり、めでたく Hello world プログラムになります。

この問題の勝者は Seiei Miyagi さんでした。

Answer 1-3

作問担当は mame でした。問題再掲↓

include Math
# Hint: the most beautiful equation
Out, *,
     Count = $>,
             $<, E ** (2 * PI)
Out.puts("Hello world" *
         Count.abs.round)

解答↓

include Math
# Hint: the most beautiful equation
Out, *,
     Count = $>,
             $<, E ** (2i * PI)
Out.puts("Hello world" *
         Count.abs.round)

E ** (2i * PI) というように i を入れました。

これはちょっと知識問題で、e^{i\pi} = -1 という公式を使います。この公式は「オイラーの公式」と呼ばれ、ヒントにあるように「最も美しい等式」などと言われることもあります。Ruby で書くと Math::E ** (1i * Math::PI) #=> -1 です。E ** (2i * PI) はそれの二乗になので、浮動小数点数演算の誤差もあるのでおよそ 1 になります。Count,abs,round によって正確に 1 になって、Hello world プログラムとなります。

この問題には別の意図もありました。これらの問題は 1 文字で解けると知っていたら、ブルートフォース(いろんな箇所にいろんな文字を挿入して実行してみるのを網羅的に試す)によって頭を使わずに解けてしまうのですが、この問題はそれをじゃまするために用意しました。というのは、$< の前に * を挿入して *$< とすると、標準入力を配列化する演算となり、標準入力を待ち受けて動かなくなるようになります。よって、下手にブルートフォースをするとここで実行が止まります。ただ、このトラップにひっかかった人はいたかどうかはわかりません。

この問題の勝者は pocke さんでした。

Answer 2-1

作問担当は ko1 でした。問題再掲↓

def say
  -> {
    "Hello world"
  }
  # Hint: You should call the Proc.
  yield
end

puts say { "Goodbye world" }

解答↓

def say
  -> {
    "Hello world"
  }.
  # Hint: You should call the Proc.
  yield
end

puts say { "Goodbye world" }

}.. を追加してあります。これにより、yield はブロック呼び出しではなく、上の Proc 式に対して yield メソッドを呼び出すようになります。Proc#yieldProc#call の別名なので、このラムダ式が実行され、"Hello world" を返すようになります。

この問題の勝者は Shyouhei さんでした。

Answer 2-2

作問担当は mame でした。問題再掲↓

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  yield "Hello world"
end

puts e.next

普通に考えたら、次の 2 文字の解答になります。

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  g.yield "Hello world"
end

puts e.next

Enumerator の最初の要素として "Hello world"yield メソッドで渡し、Enumerator#next によってそれを取り出し、それを表示します。Enumerator についてはドキュメントの class Enumerator を参照ください。

ヒントに従って考えると、次の 6 文字の解答にたどり着きます。

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially Fiber.
  Fiber.yield "Hello world"
end

puts e.next

Enumerator は Fiber のラッパのようなものなので、実はブロックの中で Fiber.yield を呼ぶことでも要素を渡すことができ、上のプログラムと同じように動きます。

ただしこれは 6 文字も追加しているのでまったく最短ではありません。どうすればよいかというと、次が 1 文字解答です。

解答↓

e = Enumerator.new do |g|
  # Hint: Enumerator is
  # essentially
 Fiber.
  yield "Hello world"
end

puts e.next

コメントの中の essentiallyFiber. の間に改行文字を追加しました。コメントの中にある Fiber. という文字列を利用するのがミソでした。すべての問題に適当なヒントコメントが書いてあるのは、この問題にだけヒントコメントをもたせることで不自然になってしまわないようにするためでした。

余談ですが、より面白い想定回答は↓でした。

e = Enumerator.new do |g|
  # Hint: Enumerator is
  #
 essentially Fiber.
  yield "Hello world"
end

puts e.next

essentially の前に改行を入れています。essentially は関数呼び出しとみなされますが、引数が Fiber.yield "Hello world" なのでこちらが先に評価され、essentially が実際に呼び出されることはなく、正しく動きます。この解答にたどり着いた人はいなかったようです。

この問題の勝者は youchan さんでした。

Answer 2-3

作問担当は mame でした。問題再掲↓

$s = 0
def say(n = 0)
  $s = $s * 4 + n
end

i, j, k = 1, 2, 3

say i
say j
say k

# Hint: Binary representation.
$s != 35 or puts("Hello world")

2 文字解答はたくさんあります。3535-8 に変えたり、say jsay j*2 に変えたり、or putsor 0;puts と変えたり、いろいろなやり方が発見されていました。

1 文字解答は、意外と理詰めでたどり着けるようになっています。say メソッドは「$s を右に 2 ビットシフトし、引数 n を足す演算」です。ヒントにあるとおり 35 の二進数表現を考えると 10 00 11 になります。それぞれ二進数で 2, 0, 3 なので、say(2); say(0); say(3) という順序で say を呼び出せばいいことがわかります。say i; say j; say ksay(1); say(2); say(3) なので、say k はいじらなくて良さそうです。また、say の引数を省略したら 0 になるので、say i; say j をうまくいじって say j; say という意味にする方法はないか、と考えます。ということで答えです。

解答↓

$s = 0
def say(n = 0)
  $s = $s * 4 + n
end

i, j, k = 1, 2, 3

say if
say j
say k

# Hint: Binary representation.
$s != 35 or puts("Hello world")

say i のあとに f を足して、後置 if 文にします。条件式は次行の say j です。これにより、先に say j が評価されて、say j は真の値を返すので、if の中の say が無引数で呼び出されます。それから say k が呼ばれることで、所望の挙動になります。

この問題の勝者は k. hanazuki さんでした。

Answer 3-1

作問担当は ko1 でした。問題再掲↓

def say s="Hello", t:'world'
  "#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.

puts say :p

解答↓

def say s="Hello", t:'world'
  "#{ s }#{ t } world"
end
# Hint: Arguments in Ruby are
# difficult.

puts say t:p

say :psay t:p に書き換えています。これにより、シンボルの :p を渡していたところから、キーワード t のキーワード引数として p を渡すように変わります。pKernel#p の呼び出しで、無引数の場合は単に nil を返します。よって、s = "Hello" かつ t = nil になり、"#{ s }#{ t } world""Hello world" になります。

この問題の勝者は Akinori Musha さんでした。

Answer 3-2

作問担当は mame でした。問題再掲↓

def say s, t="Goodbye "
  # Hint: You can ignore a warning.
  s = "#{ s } #{ t }"
  t + "world"
end

puts say :Hello

解答↓

def say s, t=#"Goodbye "
  # Hint: You can ignore a warning.
  s = "#{ s } #{ t }"
  t + "world"
end

puts say :Hello

t=#"Goodbye " というように、オプショナル引数のデフォルト式をコメントアウトしています。これにより、次の行にある式がデフォルト式になります。この場合、s = "#{ s } #{ t }" がデフォルト式です。s はすでに受け取った引数で :Hello が入っています。引数 t は未初期化の状態で参照され、これは nil になります(コメントにあるとおり、それは問題ないです)。よってこのデフォルト式は "Hello " という文字列になります。あとはそのまま。

この問題の勝者は DEGICA さんでした。

Answer 3-3

作問担当は mame でした。問題再掲↓

def say
  "Hello world" if
    false && false
  # Hint: No hint!
end

puts say

解答↓

def say
  "Hello world" if%
    false && false
  # Hint: No hint!
end

puts say

if の後に % を書き足します。答えを見ても意味がわからない人のほうが多いのではないでしょうか。

Ruby には % 記法というリテラルがあります。%!foo! と書くと、文字列リテラル "foo" と同じです。デリミタ(先の例では !)には、数字とアルファベット以外の任意の文字を使うことができます。上の例は、このデリミタとして改行文字を使っています。わかりやすく、デリミタを改行文字から ! に書き換えると、こうなります。

def say
  "Hello world" if%!    false && false!
  # Hint: No hint!
end

puts say

後置 if の条件式に文字列リテラル(常に真)を書いたことになるので、このメソッド say は常に "Hello world" を返します。

なお、% 記法のデリミタに改行文字や空白文字を使える仕様は、matz が「やめたい」と言っていたので、将来廃止されるのかもしれません。

この問題の勝者は cuzic さんでした。

まとめ

Cookpad Daily Ruby Puzzles の問題と解答と解説でした。今回はわりと手加減せずに Ruby の仕様の重箱の隅をつつくような問題ばかりでしたが、「クックパッドのパズルがおもしろかった」という声も結構いただきました。まだやっていないかたは、今からでも(上の解説を見ずに)楽しんでいただければ幸いです。

こういうパズルが入社試験として出るわけではありませんが、このパズルをきっかけにクックパッドに興味を持ってくれた人は、↓からぜひ応募してください。

cookpad.jobs

Special thanks

  • hogelog:クックパッドのブースに「超絶技巧パズル(ってなに?)置いておこう」と発案した人
  • sorah:シュッとチラシをデザインした人
  • ブースにいた全員:パズルの配布や運営をした人たち
  • 参加してくれた全員:解けた人も解けなかった人も

おまけ

もっと遊びたい人のためにエクストラステージを用意しておきました。答えはないので考えてみてください。

Extra 1

作問担当:mame

Hello = "Hello"

# Hint: Stop the recursion.
def Hello
  Hello() +
    " world"
end

puts Hello()

Extra 2

作問担当:mame

s = ""
# Hint: https://techlife.cookpad.com/entry/2018/12/25/110240
s == s.upcase or
  s == s.downcase or puts "Hello world"

Extra 3

作問担当:ko1

(1 文字解答が 2 つあります)

def say
  s = 'Small'
  t = 'world'
  puts "#{s} #{t}"
end

TracePoint.new(:line){|tp|
  tp.binding.local_variable_set(:s, 'Hello')
  tp.binding.local_variable_set(:t, 'Ruby')
  tp.disable
}.enable(target: method(:say))

say

GraphQL Asia 2019 で登壇しました

こんにちは。メディアプロダクト開発部の我妻謙樹(@itiskj)です。 ソフトウェアエンジニアとして、広告配信システムの開発・運用を担当しています。好きな言語は Go と TypeScript です。

先日、GraphQL に関してアジア圏初の大型カンファレンス、GraphQL Asia がバンガロールで開催されました。元 Facebook で GraphQL の策定者の一人である Lee Byron を始めとし、Twitter, PayPal, Airbnb, Atlassian などのエンジニアが登壇し、GraphQL の導入事例やベストプラクティスの紹介が行なわれました。私も CFP が通過し発表してきたので、他の登壇者の内容と合わせて紹介いたします。

GraphQL Asia 2019 での発表

全発表資料は後ほど https://www.graphql-asia.org/ にて公開されるとのことですが、先にいくつかピックアップしてご紹介します。

BrikL - A GraphQL native

まず、GraphQL Asia 2019 の主催者の一社である BrikL から、GraphQL を用いた自社製品の開発の変遷について紹介がありました。

個人的には、S3/DynamoDB/Elasticsearch などの外部サービスを Directives を用いて宣言している点がユニークで学びでした。

f:id:itiskj:20190422134337p:plain
graphql asia brikl slide capture

APIS.GURU - GraphQL Tools are easy or how to write one in less than 100 lines

https://github.com/graphql/graphql-js のコミッタや https://github.com/APIs-guru/graphql-voyager のメンテナをしている OSS 開発者からの発表でした。

GraphQL tool を書くことは思ったより簡単であることを、実際に demo で GraphQL coverage tool を書きながら伝える内容でした。

GraphQL 界隈は、基本的なものに関しては出揃ってきた感がありますが、エコシステム全体的にはツールも production ready でないものが多かったり、作りかけのものが多かったりと、成熟期には達していません。GraphQL community に対して、ツールを書くことに敷居を下げ、ヒントや手法を与えた、という意味でこの発表はとても有意義なものでした。

Airbnb - GraphQL @ Airbnb

Airbnb における GraphQL 活用事例の紹介でした。

Airbnb では、monolithic な Rails アプリ、いわゆる "Monorail" が 10 年以上稼働していました。2 年前から Airbnb SOA というアーキテクチャを導入し、徐々に service oriented な構成へと移行しているとのこと。その中でも、presentation layer に GraphQL Gateway を導入しているとのことでした。

この発表の最大の価値は、RPC フレームワークであるFacebook's branch of Apache Thrift と GraphQL を併用した場合のアンチパターン及びエッジケースについて紹介されていることでした。おそらくこの組み合わせを大規模なサービスで用いているのは Airbnb が初めてとのことで、彼らならではのチャレンジから生まれる知見が世に出たことは、今後の GraphQL community の資産となることでしょう。

発表資料

メディアプロダクト開発部のうち、動画領域を担当するプロダクト開発グループでは AWS AppSync を利用しており、GraphQL および AppSync の活用事例として、以下のような資料が公開されています。

一方、私が所属する広告領域を担当するマーケティングサービス開発グループでも、社内の広告管理システムにおいて GraphQL を導入しています。今回は、こちらのシステムに GraphQL を入れるにあたって行った技術選定の過程や、実際に GraphQL を利用して得た知見を共有する、という内容で発表を行いました。

GraphQL の導入事例は他にも Tokopedia, Intuit や Phillips, Adobe からもいくつかありましたが、どの会社も違った課題を抱えており、それぞれのユースケースの講演も参考になりました。

感想

国際カンファレンスに参加したのは初めてだったのですが、Speaker として参加したからこそ得られた知見や経験が非常に貴重でした。というのも、Speaker 同士の交流会や市内観光などもカンファレンススタッフによって予定されていたのですが、それらに参加する中で、リアリティに富んだ新鮮度の高い情報(GraphQL の欧米圏における浸透具合や、今必要とされているツールや技術)のみならず、決して SNS では得られることのできないような情報についても聞くことができたのは、非常に有意義な点でした。また、彼らと接点を持てたことも大きいです。

発表資料は後からオンラインに公開されるので世界中どこに住んでいても同じ情報にアクセスできますが、やはり直接会うからこその経験を得ることができるのも、カンファレンス(国内・国外問わず)に参加することの意義だということを再認識しました。

また、国外ではカンファレンス渡航費を会社が出してくれないという Speaker もそれなりにいました。今回、渡航費をサポートしてくれたり、出張中に業務をカバーしてくれたりした同僚や上司の方々には感謝しかありません。ありがとうございました。

まとめ

今までは、"Header Bidding 導入によるネットワーク広告改善の開発事情""cookpad storeTV の広告配信を支えるリアルタイムログ集計基盤" など、まさに広告領域を代表するような技術およびシステムについて紹介してきました。

しかし、広告領域ではネットワーク広告や配信サーバーのみならず、社内入稿システムも開発しています。そして、その入稿システムを利用する業務推進チームや制作チームなどが、会社の広告事業の売上を支えてくださっています。そのメンバーの業務効率を上げるために、社内入稿システムの開発及び改善・保守にも力を注いでいます。今回の記事をきっかけに、また違った観点からの技術的チャレンジもお伝えできれば嬉しいです。

広告領域は、技術的にチャレンジングな課題も多く、かつ事業の売上貢献に直結することが多いだけでなく、入稿から配信まで様々なシステムが複雑に絡み合う、非常にエキサイティングな領域です。ぜひ、興味を持っていただけたら、Twitter からご連絡ください。

また、メディアプロダクト開発部では、一緒に働いてくれるメンバーを募集しています。少しでも興味を持っていただけたら、以下をご覧ください。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/