リクルートテクノロジーズ メンバーズブログ  Go言語を使って1年間が経った

Go言語を使って1年間が経った

はじめに

これはRecruit Engineers Advent Calendar2日目の記事です。

昨年書いた記事より、1年が経ちました。
Go言語を用いて開発を行ったプロジェクトも無事にリリースを迎えることができました。ほぼ問題は発生しておらず、安定的な稼働を実現できています。

今回の記事では、1年前の記事を踏まえ、Goを用いてWebシステムのバックエンドを開発する上での振り返りを行いたいと思います。

全体アーキテクチャについて

リクルートテクノロジーズではバックエンドとフロントエンドの疎結合を促進させ、より柔軟な設計と開発効率の向上を意図して、 Backends For Frontends(BFF) と呼ばれる層を設けています。
BFFについてはこの資料この連載が詳しいのですが、代表的なユースケースだと、

  • APIのAggregation(いわゆるAPI Gateway)
  • Viewのレンダリング
  • Session管理

などの責務を担っています。このレイヤを挟むことで、フロントエンドの開発に対して有利になるだけでなく、バックエンドはSessionなどのステートフルな情報を管理せずに、シンプルな実装が可能になります。

今回の案件でもBFFを採用し、バックエンドはステートレスなJSONのAPIとして開発を行いました。従って、Goで開発する範囲は以下の通りです。

  • ビジネスロジックの実装
  • データ永続化(DBアクセス等)
  • 外部システムとのAPI連携
  • バッチ処理

全体のアーキテクチャを図示します。

ProxyやSession Storeなどの構成物は記載していませんが、大まかには上記のような構成です。今回はライフサイクルが違う複数のWebシステムを提供する必要があったので、BFFは複数設置しています。

APIのアーキテクチャ

今回の開発ではAPIは3層のレイヤーで構成しました。このレイヤー構成は、リクルートのJava版の標準開発のガイドラインに沿ったものになっており、Goでもそれを継承した設計を行いました。
以下にその実装と責務をまとめます。

  • Controller
    • echoを使用。
    • Serviceに依存。
    • 責務
      • http requestをvalidationし、structにmappingする。
      • DIコンテナからServiceを取り出し、呼び出しを行う。
      • Serviceからの返り値をhttp responseにmappingして返却する。
  • Service
    • 使用ライブラリは特になし。interfaceとして定義し、実装は公開しない。
    • Repositoryに依存。
    • 責務
      • ビジネスロジックの実装。
      • repositoryの呼び出し。
  • Repository
    • gorpを(主に)使用。interfaceとして定義し、実装は公開しない。
    • 依存レイヤはなし。
    • 責務
      • 永続化層(主にDB)への読み書き。

これ以外にも、実際には数々のutilityをまとめた support というパッケージや、レイヤ間のデータ受け渡し用の構造体をまとめたパッケージもあります。
また、これは余談ですが、バッチを開発する際にも似たような構成はとられ、特にservicerepositoryのレイヤのコードはバッチの開発でも再利用されています。

この構成自体は上位レイヤが一つ下の下位レイヤのinterfaceに依存するという典型的なパターンです。前回の記事で紹介したように、DIの力を借りることにより、各レイヤは依存の解決を気にすることなく実装を行うことができます。

テスト方針について

この構成のメリットとして、testabilityの担保が挙げられます。

具体的に疑似コードで話します。ユーザのパスワード認証の擬似コードとなっています。

レポジトリのコードです。User DBから、emailで検索してレコードを返却するという擬似コードになっています。

次にserviceのコードです。repositoryからemail経由でユーザを取得し、パスワードを比較します。

最後にcontrollerのコードです。このcontrollerはclosureを使って定義されており、予め依存となるserviceをDIコンテナから取得するように実装してあります。

さて、ここで、serviceのレイヤのテストを考えてみます。
servicerepositoryに依存しているので、repositoryのモックを作り、serviceに渡すパターンを作ってみます。
repositoryのモック(正確にはスタブですが)は以下のように実装できます。

これで固定のユーザを返すrepositoryが実装できました。

test codeは以下の通りになります。

実行してみます。(PATH等はよしなに読み替えてください)

TestがPassしました。少なくとも正常系については正しく実装できていることが確認できました。
このように、依存があるモジュールでもmock等を簡単に差し込むことができ、容易にtestを実装することが可能になっています。

上記のようなパターンを用いて、各レイヤのテストは以下のように行いました。

  • Controller
    • Serviceのmockを差し込んでテストを行う
  • Service
    • Repositoryのmockを差し込んでテストを行う
  • Repository
    • mockは用いずに本物のDBを使う

これらの方針を開発前にドキュメント化し、チーム内で確認をするという作業を行ったため、特にテストに関しては非常にスムーズに実装を進めることができました。

Test Double自動生成

上記の基本方針に加え、(昨年の記事の末尾でも少し触れましたが)Testに用いるmock / stubを自動生成する機能を開発しました。
具体的な使用方法はGithubの方を参照していただきたいのですが、ここではどういったコードが自動生成されるのかを紹介したいと思います。
dicongenerate-mockの機能を使うと、DIで解決される依存のコンポーネントすべてのmockが自動で生成されます。今回の場合だと、repositoryserviceのmockが以下のように生成されます。

interface名+Mockのstructが生成されています。このstructinterface関数名+Mockのメンバ変数の関数を持っており、それぞれの実装メソッドではこの関数を呼んでいます。
このmockを使うときは、メンバ変数(としての関数)を差し替え、任意の挙動を実装します。例えば、上で実装したmockと同じ挙動をさせる場合には以下のようになります。

mockの挙動を変えるたびにいちいち構造体を宣言し直さないと行けない上の方式より、かなりeasyになったかと思います。
また、以下のようにmock関数のスコープ内で*testing.Tを使うことができ、特別なライブラリなどを使わなくても、関数の入力値のチェックなどができるようになります。

goのmockingではgolang/mockなどの有名なライブラリがありますが、私達はdiconのDIと組み合わせの問題もあり、こういった方式を採用しています。

ちなみに、diconに依存しない形で、このmockingの機能だけを取り出したツールimpast/mockerがあります。興味がある方はこちらも参照してみてください。

DI + test doubleの支援ツールの組み合わせにより、高いソースコードの品質を保ちつつも効率よく開発を行うことができました。

まとめと今後について

この記事では1年前に開発したgoのDIコンテナのライブラリ、diconが実際の開発でどう使われたのかを紹介しました。
また、リクルートテクノロジーズにおけるGoを用いたwebシステム開発で、テストがどのように行われてるのかも合わせて紹介いたしました。

今後もリクルートテクノロジーズではGoによる開発を勧めていきます。現在行おうとしている取り組みを簡単にだけ紹介すると、

  1. DBやSQLから自動でコードを生成してくれるツール、xoの導入
  2. Swagger(OpenAPI2.0)からのコード自動生成

1に関しては、リクルートテクノロジーズの開発ではそれなりに複雑なSQLを記述することが多く、そのmappingの簡略化を意図して導入しようとしています。
2については、おそらくアドベントカレンダー2の方で紹介できると思いますが、リクルートテクノロジーズで開発しているConsumer Driven Contractのツール、agreedからOpenAPI準拠のjson/yamlを生成するツールを開発しています。
このjson/yamlからサーバサイドのGoのコードを自動生成するという取り組みも行っています。

以上となります、ありがとうございました!

  • アイキャッチ画像はこちらからお借りしました。