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