#jjug_ccc #ccc_f1 広告システム刷新の舞台裏 - phpからjavaに変えてみました
TRANSCRIPT
広告システム刷新の舞台裏PHPからJavaに変えてみました
ヤフー株式会社
森下大介
自己紹介
YJ入社前
Sierな世界の住人でした。
受託案件をこなす業務システム開発のエンジニア。
YJ入社後
BtoBなWebサービス開発へ。
Web系と言いつつエンタープライズ系っぽいシステムの開発に携わる。
• 名前:森下大介
• 年齢:41
• 職歴:2011年にYJに中途入社
• 言語:C/C++/Java/PHP/Node.js
3P今回のお話の前提その1
自分の所属部署における取り組み事例です。
• 対象は、広告主向けの入稿やレポーティング機能を提供する「業務系」と呼ばれるシステム。
• データ登録と集計系の比重が高いところがエンタープライズシステムっぽい。YJの中ではマイノリティ。
4P今回のお話の前提その2
刷新前のシステムをディスるつもりは無いです。
・・・ホントですよ?
ビジネスをしっかり支えてくれて、後から個別に刷新もできる優れた構成でした。
5P本日の内容
• バックグラウンド• 取り組んだこと
• やったことその0:体制づくり• やったことその1:プログラミング言語を変える• やったことその2:テストができるアーキテクチャに• やったことその3:テスト向けDSL (Spock)• やったことその4:CI/CD, 静的解析• やったことその5:インタフェース定義言語
• 振り返っての所感
バックグラウンド
7P入社当時のシステムの特徴:利用言語、道具
• 言語:– PHPメイン– たまにバッチ系にPerl– 一部のライブラリはC + PHP Extension
• 道具:– ほとんどの人がvim, emacs等を使用– IDEはほとんど使われていない– テストはほぼブラックボックステストのみ
8P入社当時のシステムの特徴:システム構成
• システム構成– FE/BEは明確に分離、BEはWebAPI化
– マイクロサービスほどでは無いが分散システム化
• このおかげで・・・– 機能ごとにスケールができた
– 機能ごとにアップデートができた
9Pこんな感じの構成
A Service
B Service
C Service
XService
YService
Z Service
バックエンドサーバー群1 バックエンドサーバー群2
FEサーバー群バックエンド機能をWebAPIで構成。
複数Serviceの集まりを1アプリケーションにしてサーバーにデプロイ。
10P当時、思っていたこと
何をやっても
「なんか妙につらい・・・」
11P例1:小改修なのにつらい
typo修正したいだけなのに・・・
- ユニットテストがない
- コンパイラ、型がない
- IDE使ってない
12P例2:結合がつらい
なかなかつながらない・・・
特にAPIIFドキュメントが
- 手書きだし、書き方が統一されてない- 書き手と読み手でお互いに認識がズレてる- ドキュメント通りに実装されない
13P例3:なんとなく動くのがつらい
LLの柔軟さが裏目に・・・
- 存在しないメソッドコールしても動く。
- 間違った型で引数受け取っても動く。
14P何が起きていたのか?
考察1:「大規模」で「複雑」に。
サービスが育った結果・・・
- 道具の助け無しで
- 人手と記憶と努力と根性で
- がんばれる規模をこえている
15P何が起きていたのか?
考察2:道具とやり方が合ってない
ちゃんとデータを扱いたいのに
- 型宣言が無い
- コンパイラに頼れない
- 単体テストを仕掛けられない
16P何が起きていたのか?
考察3:積み重なる技術負債
やり方を変えないままで
- 既存コンポーネントは拡張され
- 新規コンポーネントが追加され
- そしてそこにテストは無い
17P何が起きていたのか?
考察4:チーム同士が良くも悪くも独立
分散システム化によって・・・
–担当機能毎にチームの独立性が強い
–技術/知識/事例の共有がされない
–助け合う、融通しあう意識が弱い
18P当時のエンジニアの心中
このままだとヤバイのは薄々感じてる。
感じてるけど・・・
– 目の前の巨大システムは売上を現実にあげている– ビジネス拡張のための案件はひっきりなし– 変えるには根本的な手当が必要・・・– どーすりゃいいのこれ?
19Pそんなときに
組織変更がありました。新部長が言いました。
「イチから全部変えてよし」
このおかげで動き出すことが出来ました。
これくらいの上位レイヤから言い出してくれると格段に動きやすくなると実感してます。
20P目指したこと
システム刷新は「結果」
• 自分の変化がアウトプットを変える。• でも結果を出すことも重要。• 結果で証明する。
変わるべきは「自分達」
• 3倍早く• やりやすく• 確実に
開発できる強いエンジニアになること。
「3倍早い開発スピード」
取り組んだこと
やったことその0
体制をつくる
23P体制づくり
リード役の配置
– 自分が当時の役職を離れて、部長直下の「部付」ぼっちとなり、刷新活動に集中。
– できるかぎりディスカッションしてコンセンサス取ることを努力するが迷ったら最後は決断の責任を全て負う。
24P体制づくり
仲間を集める
–同じような問題意識を持つ人に声をかけて、バーチャルなチームを構築。
25P体制づくり
部門長による宣言
–刷新していくことを部の内外でステークホルダー/部内メンバーに宣言。
やったことその1
プログラミング言語を変える
27Pプログラミング言語を変える
これを決めた時点で、
学習コストが極大化する事が確定。
大抵はそうしないで済む方向を考えるものですが・・・
28Pプログラミング言語を変える
「大規模で、
データをきっちり扱うシステム」
をちゃんと開発していくために必要な変化だと判断。
29Pプログラミング言語を変える
とにかく欲しかったのは
–コンパイル
–型宣言
30Pプログラミング言語を変える
特に求めた効果は、しょうもないレベルの間違いを動かすまでもなく潰せること。
これができないと、typo修正すらおそろしくなる。
– 動かして全確認が必要
– 漏れが無いか確証が持てない。
31Pプログラミング言語を変える
この条件でいくと、現実的な選択肢はだいぶ絞られる。
考えたのは以下あたり。
– C++– Java– Scala
32Pプログラミング言語を変える
選んだのは
Java
33Pプログラミング言語を変える
これで得られたもの
• コンパイルできる• 厳密な型定義ができる• 優れたメモリ管理(GC)• 実行時最適化(JIT)• デバッガ、解析ツール
副次的なものとして
• 豊富なOSS• 統合開発環境の活用• 優れた静的解析ツール
やったことその2
テストできるアーキテクチャに
35Pテストできるアーキテクチャに
「テストできない」とはどういう状況?
• 動かす準備が大変
• CI/CDの中で実行できない
• ブラックボックステストになる
• etc…
36Pテストできるアーキテクチャに
根本的な原因として、アプリケーション内部が「モノリシック」なカタマリになっているためと仮定。
システム全体で見ると分散システム構成となっていたが、個々のアプリケーション内部ではユニットテストしたいクラス単位で単独で動かせない状態。
37Pモノリシックなコードの例
これらは一見するとA, B, Cというクラスに分割されているように見えるが・・・
public class A {public void x() {
B b = new B();b.y();
}}
public class B {public void y() {
C c = new C();c.z();
}}
public class C {public void z() {
DB操作とか}
}
38Pこれにテストケースを仕掛けてみると・・・
public class A {public void x() {
B b = new B();b.y();
}}
public class B {public void y() {
C c = new C();c.z();
}}
public class C {public void z() {
DB操作とか}
}TestCaseA(A, B, Cの複合テスト)
Class AにテストケースつくるとB, Cも必ずくっついて来て単独テストにならない。
TestCaseB(BとCの複合テスト) TestCaseC
(これはまだ単独)
39Pテストできるアーキテクチャに
ポイントは、
「呼び出し先の実装クラスを自分でnewしていること」
これだとクラス同士が密結合する。この状態で無理にテストケースを仕掛けたとしても、
• 実行条件が増える• バリエーションが掛け算で増加• メンテ、問題箇所の特定が困難
40Pテストできるアーキテクチャに
このようなことから、
「テストできるような構造じゃないからやらない」
という結論となり、放置される。
これを解決するために、アプリケーション内部の基本アーキテクチャとして「Dependency Injection」を導入。
41PDI(JavaでSpringFramworkの場合)
public class Aimplimplements A {
@Autowiredprivate B b;
public void x() {b.y();
}}
public class Bimplimplments B {
@Autowiredprivate C c;
public void y() {c.z();
}}
public class Cimplimplements C {
public void z() {DB操作とか
}}
実装クラスとインタフェースを分離、コール先のインタフェースのみ認識し、実装インスタンス(Dependency)は外部から注入(Injection)。
42Pこれにテストケースを仕掛けてみると・・・
public class Aimplimplements A {
@Autowiredprivate B b;
public void x() {b.y();
}}
今まではA単独のテストができなかったが、依存するBをモック化することでテストしたい処理だけに対して確認を行えるようになる。
class TestCaseA {def testA() {
def a = new Aimpl()//モック注入a.b = Mock(B.class)//テスト実行、asserta.x()
}}
43Pテストできるアーキテクチャに
これにより、依存する他モジュールをnewしなくなる。
そうなると、テスト時にモックを自由に差し込めるようになるので単独テストが可能となる。
ただし、絶対にあらゆるクラスのnewが禁止というわけではなく、
「テストの都合で分離しててほしい単位」
でこれが適用されていればよい。
44Pテストできるアーキテクチャに
ということで、テストが出来るようになる事を目的としてDIコンテナを使用。
利用してるDIコンテナは
「SpringFramework」
やったことその3
テスト向けDSLの採用
46Pテスト向けDSLの採用
当初使おうとしていたのは定番のJUnit, JMock。
でもJUnitはJava言語でテストを書くことになるが、Java言語はテストを表現する文法を持たない。
またJMockはかなり変態コードになるためキツイ。
47Pテスト向けDSLの採用
final SampleDao dao = context.mock(SampleDao.class);final List<Sample> expected = Arrays.asList(new Sample());
Expectations mockConfig = new Expectations() { {oneOf(dao).findBySelector(with(new TypeSafeMatcherSimple<SampleSelector>() {@Overrideprotected boolean matchesSafely(SampleSelector item) {assertEquals(ids[0], item.getIds().get(0));assertEquals(ids[1], item.getIds().get(1));assertEquals(userStatuses[0], item.getUserStatuses().get(0));return true;
}}));will(returnValue(expected));
}};context.checking(mockConfig);
JMockを利用したテストコードの一部。
つらくないですか?
48Pテスト向けDSLの採用
テストケースの作成に労力が掛かり過ぎるようだと、
「テスト書くのがキツすぎるのでやらない」
ということになる。
49Pテスト向けDSLの採用
選んだのは
Spock
50Pテスト向けDSLの採用
Junitの上に構築されたものだが、記述言語は、JVM言語の「Groovy」その上にテストを記述するためのDSL(ドメイン固有言語)が構築されている。
主な機能としては
• テスト実行• BDD的なテスト記述文法• 柔軟なモック/スタブ生成• テストパターンデータの記述
特に良いのが、Groovy自体がJavaよりも色々省略して書けること。
テストコードはJava言語では書きたくない。
51PSpockによるテストコード例
class SampleSTest extends Specification {def “データ更新テスト(#testname)”() {given:
def service= new SampleServiceImpl()def dao = Mock(SampleDao.class)1 * dao.update(_) >> responseserivce.dao = dao
when:def result = service.update(request)
then:assert result == response
where:testname | request | response“パターンA” | “foo1” | “bar1”“パターンB” | “foo2” | “bar2”
}}
テストケースで以下のようなブロックに区切ってコードを書ける。
• givenがテスト対象のセットアップ• whenがテスト対象の実行• thenがテスト結果の確認• whereがテストデータ
Mockの生成とその挙動も全てテストコードの中で記述できるのが良いところ。
テストデータを複数件かけばその件数分で全体をループして実行してくれる。
Groovyの型推論や省略記法も楽で助かる。
やったことその4
CI/CD、静的解析
53PCI/CD, 静的解析
アーキテクチャとテストケースそれぞれのアプローチからテストができない理由を取り除いた。
これでやっとCI/CDの中であたりまえにテストを行うようになった。
54PCI/CD, 静的解析
CI/CDの中では以下の様なテストや解析を実施
• SpockによるSテストケース実行• Cloverによる詳細なカバレッジ計測• Coverity QualityAdvisorによるコード解析• Coverity TestAdvisorによるテスト解析• Frisbyを使ったWebAPIのSmokeテスト
やったことその5
インタフェース定義言語
56Pインタフェース定義言語
分散システムの形でシステム全体を構成しているので、バックエンドの各機能は
「Webサービス(API)」
としてFEや他BEに提供する形にしている。
57Pインタフェース定義言語
その実装とテストのためには以下を行うことになる。
• 外部仕様(APIIF)の定義と公開• 外部仕様どおりの実装• 結合試験
58Pインタフェース定義言語
APIを「提供」する側では、
• 人がAPIIFを考えて• それを頑張ってドキュメントとして書く。• それを元に実装する。
APIを「利用」する側では、
• 人がAPIIFドキュメントを読んで理解して、• そのAPIを利用する処理を実装する。
59Pインタフェース定義言語
「仕様/ドキュメント/実装」を人の脳が頑張って変換しながら何種類も成果物つくってるが・・・。
この変換時に認識違いによるズレがあった場合、それは結合試験まで発見できない。
60Pインタフェース定義言語
でも・・・
IFって静的なものなんだから、宣言的に記述できるはず。
宣言的な記述ならそこからコードも文書も生成できる。
61Pインタフェース定義言語
導入したのが
インタフェース定義言語
62Pインタフェース定義言語
当初はOSSの以下あたりを使えないかと検証したが、• Googole ProtocolBuffer• Apache Thrift
以下の理由から断念• 入力値バリデーションが表現できない• ドキュメントが生成できない
63Pインタフェース定義言語
一から以下を行いました。
• 定義言語自体の文法設計• コンパイラ開発• Javaコード・ドキュメントジェネレータ開発• ドキュメント表示サーバー開発• 入力値バリデーションエンジン開発
64Pインタフェース定義言語
IDLを使ってAPIIF設計をすると・・・
65Pインタフェース定義言語
1.APIIFをIDL文法で記述
記述した内容はIDLコンパイラを通すことで整合性チェックを行うことが出来る。
66Pインタフェース定義言語
2.ドキュメント生成して公開
ドキュメントジェネレータを通してJSONデータを生成し、それをドキュメント表示サーバーにアップロードして公開する。
67Pインタフェース定義言語
3.Javaコード生成して利用
Javaコードジェネレータを通してコード生成して、実装で利用する。生成コードは編集は一切せず利用のみとして、常に上書き更新可能にしている。
68Pインタフェース定義言語
IDLから生成されるJavaコードは以下。
• Context (データ操作のコンテキスト情報)• Entity (Pojoデータオブジェクト)• Enum (Enum系項目の値定義)• Error (エラーコード一覧)• JAX-RSインタフェース(APIエンドポイント)
69Pインタフェース定義言語
//IDLファイルの例namespace entity sample.entity;
entity Sample {field Long id {valid min 1;valid max 100;
}field String name {valid max 100;valid pattern ”^[a-zA-Z0-9¥¥-_]+$”;
}}
//生成Javaコードの例package sample.entity;
public class Sample implements Entity {@CheckNumber(min=1, max=100)private Long id;public void setId(Long id) { …. }public Long getId() { …. }
@CheckString(max=100, pattern=“^[a-zA-Z0-9¥¥-_]+$”)
public String name;public void setName(String name) { …. }public String getName() { …. }
}
IDLコンパイラでJavaコード生成
IDLで記述したデータ構造(entity)のJavaコード生成例。この場合はバリデーション用アノテーション付きのPojoが生成される。
70Pインタフェース定義言語
//IDLファイルの例namespace service sample.jaxrs;
service SampleService {path /SampleService;operation Response add(Sample sample);operation Response set(Sample sample);
}
//生成Javaコードの例package sample.jaxrs;
@Path(“/SampleService)public interface SampleService {@Path(“/add”)@POSTpublic Response add(Sample sample);
@Path(“/set”)@POSTpublic Response set(Sample sample);
}
IDLコンパイラでJavaコード生成
IDLで記述したWebAPIエンドポイントのJavaコード生成例。この場合はJAX-RSのアノテーション付きインタフェースが生成される。
71Pインタフェース定義言語
72Pインタフェース定義言語
73Pインタフェース定義言語
システム刷新を振り返っての所感
問題検出はできるだけ前工程に。
76P問題検出はできるだけ前工程に
システム開発は先の工程(結合試験とか総合試験とか)に進めば進むほどソースコードが開発者の手元を離れる。
そこで見つかった問題を修正して環境に届けるには相応の時間がかかる。
77P問題検出はできるだけ前工程に
開発者の手元を離れる直前まで、やれる事をやる。
• IFを結合前に安定させられるIDL
• コンパイルと型宣言
• テストを書いて手元でもCI/CDでも実行
• Coverityの静的解析。
テストを阻む要因を潰す
79Pテストを阻む要因を潰す
テストに向かないアーキテクチャや道具を使うと対応コストを理由にやらないことが正当化されやすい。
80Pテストを阻む要因を潰す
テストできない理由が消えると、エンジニアは
わりとちゃんとテスト書くようになる。
書いたほうがいいのは皆わかってるし、
書いたものは皆動かしたい。
81Pテストを阻む要因を潰す
アプリケーションアーキテクチャまで踏み込んでテストを考慮することができればベスト。
ただこれが出来るのはかなり幸運なこと。
未熟でも早めに適用、フィードバックを受けて磨く
83P早めに適用・フィードバックを受ける
やり方を大幅に変えた時は最初からいろいろ頑張りたくなる。
でもそこは一旦最低限に押さえて早く実戦投入することが大事。
84P早めに適用・フィードバックを受ける
机上で考えるよりも
実践の場で揉まれるほうが一番早い。
85P早めに適用・フィードバックを受ける
ただし最初の適用プロダクトでは途中で色々と方針変更が入りがち。
それを承知してもらうのと、できればしがらみの無い新規プロダクトがベスト。
部門長サポートと仲間が大事
87P部門長サポートと仲間が大事
現場で「こうしたい」という思いがあっても、仕事で開発をしている以上はビジネス要件への対応は一番力を割くべきところ。
88P部門長サポートと仲間が大事
部門長や組織が
• 攻めの開発(ビジネス要件対応)
• 守りの開発(保守/刷新)
の両方を理解してくれて初めて効果的に取り組める
89P部門長サポートと仲間が大事
個人の能力とアウトプットはかなり限られる。
仲間がいれば、個の範囲を越えた成果が必ず出る。