Spring + Railsによるサービス分割の取り組み
寺下 翔太
はじめまして、ビューティ開発Tの寺下です。
現在ホットペッパービューティーでは、一部システムのリプレイスに取り組んでいます。 リプレイスはiOS・Android・Webと、各プラットフォームでそれぞれ並行して進めている状況です。
今回は私のチームが担当している、Web開発に焦点をしぼって書いていきたいと思います。 なお、iOS・Androidのリプレイスの取り組みにつきましては、 Wantedly にて紹介していますので、ネイティブアプリにご興味のある方はそちらをご参照下さい。
リプレイスに至った背景
ホットペッパービューティーは、サービス全体で年間5000万件以上のネット予約が行われるなど、 多くのユーザの方にご愛用していただいています。 サービス内容・品質の改善のため、日々機能開発を行っているのですが、 長年の運用・改修により、少なからず技術的負債が蓄積しており、 機能追加開発速度とビジネスの速度にズレが生じてきているという課題がありました。
このような背景で、今回のリプレイスプロジェクトがスタートしました。
リプレイスの概要
現在のシステムの大部分は、Javaを用いた内製フレームワークのモノリシックなアプリケーションになっています。 前段で述べたように、内部アーキテクチャの課題は大小多岐に渡ります。
一度に全ての課題を解消するのはほぼ不可能な状況の中、 「ビューとロジックの分離」を現状の課題解消の一つの打ち手として、 リプレイスを進めていくこととしました。
ユーザへの影響が比較的少ないページから、新アーキテクチャを導入・評価し、 リプレイスの適用範囲をシステム全体へと少しずつ広げていきます。
技術スタック
現行のJavaベースのアプリケーションを、
- Java8 (Spring Boot)
- Ruby on Rails
の2つのコンポーネント( Backend Server , Frontend Server )に分割します。
各コンポーネントの責務を以下にまとめます。
Backend Server - Java8 (Spring Boot)
- DBアクセス含むビジネスロジック
- APIエンドポイントの提供
Frontend Server - Ruby on Rails
- APIコールの集約・キャッシング
- Viewのレンダリング
- Session管理 (Session storeはRedis)
※なお、私自身がFrontend Server側をメインに開発していたこともあり、 以降の説明ではFrontend Server側の話が多くなります。
技術選定基準
Backend Server
安定性・パフォーマンス・既存ロジックの移植コスト等を考慮し、 Backend Serverは従来と同じくJavaを採用します。
フレームワークを従来の社内製のものからSpring Bootに変えることで、 保守性・開発効率の向上を目指しています。
Frontend Server
Frontend ServerはRailsを採用しています。 パフォーマンスや開発効率等の観点から、 他にNode.js, Golang等も候補となっていました。 しかし、今回は選定基準として以下の観点を重視し、Railsを選択するに至りました。
- 社内事例が多くあること
- コードの品質がブレにくいこと
- 人材・ノウハウが豊富であること
今回のFrontend Serverの言語選定は一部検証も兼ねているので、 別のサブシステムでは別言語/フレームワークを採用することも視野にいれています。
チーム文化
リプレイスでのチーム文化についても簡単に紹介します。
開発体制
1週間1スプリントと、短めのサイクルでのスクラムで進めています。
エンジニアの人数は10人程度で、Frontend/Backend共に同じチームで開発しています。
開発環境
ローカルの実行環境は、Dockerで構築できるようにしています。
リプレイス以前の環境構築は、数時間 - 数日かかっていたらしいのですが、
リプレイス後はdocker-compose build
等を実行するShell scriptを叩くだけで、
5分程度で開発が開始できるように整備されています。
ソース編集自体はローカルで行うので、 各人が好きな環境(OS・IDE・エディタ)で開発を行うことができます。
コード品質の担保
- 各種Linter(SonarLint, RuboCop等)の警告は常に0にする
- テストを壊さない
- coverageは90%以上をキープ
といったルールをCIツール(Jenkins, TeamCity)・コードレビューで担保しています。
コードレビュー
各人の作業状況をオープンにするため、早い段階でPull Requestを出すことにしています。
その際、Pull Requestが作業中なのかレビュー中なのか分かりやすくするため、
作成者が review
, in progress
のラベルを切り替える運用にしています。
GitHubへのcommit, commentをSlackに全て流して可視化することで、 大きな手戻りの防止や、相談等をしやすくしています。
なお、commit時のコメントや粒度にはルールは設けていませんが、 Pull RequestはGitHubのテンプレート機能を利用して、チケット情報・変更概要を明確にし、 merge時にsquashすることで、mainlineのcommit logの可読性を保っています。
設計・実装面でのポイント
APIの設計
Backend Server側のAPIは、実際のViewのコンポーネントのユースケースに合った粒度で設計しています。 具体的には、RESTのリソースの形をある程度非正規化し、 意味のある単位でエンドポイントを作成しています。 実際に、一画面を構築するために呼び出すAPI数は1 - 5回程度になっています。
当初はAPIの粒度を小さく保ち、綺麗なRESTful APIに設計しようとしていました。 しかし、現行のサービスでは表出すべきリソースが複雑に関連づいていたため、 APIのコール回数が非常に多くなってしまうという課題がありました。
APIにEager loadingの機構を実装したり、GraphQLにする、といったような解決策も考えられましたが、 可能な限りシンプルなアーキテクチャにするため、先述の実装に止めています。
またAPI仕様の提供にはSwaggerを用いることで、ドキュメントの二重化等を防止しています。
Railsの設計
Railsはレールに沿って開発することで最大限の恩恵を受けられるフレームワークです。 しかし、今回のようにBFF(Backend For Frontend)に近い形で使う場合、 一部レールを踏み外さざるを得ない箇所があります。
以下、標準外の設計・実装上のポイントをいくつかまとめます。
Model層の責務
DBアクセスはFrontend Serverで行わないため、基本的にActive Recordは使いません。
取り扱うAPIがRESTfulの場合、大きな変更を加えず Active Resource
gemで代替できるのですが、
今回のようにAPIが非正規化されている場合、Active Recordパターンはあまり適していません。
そのため、通常のRailsでModel層で担っているデータアクセス(永続化)の責務は後述のRepository層に委譲します。
Model層ではRepositoryの操作・その他のビジネスロジックを処理します。
以下が実装のイメージです。
1
2
3
4
5
6
7
8
9
10
11
12
class SomeModel
include ActiveModel::Model
attr_accessor :foo, :stores
def self.fetch(params = {})
foo = FooRepository.find_by!(params)
stores = StoresRepository.find_by!(size: 99)
new(....)
end
# ... 以下省略
end
Repository Layerの導入
Model層から取り扱うためのデータアクセスの抽象層としてRepository層を導入しています。 Repository層の責務はデータアクセスインタフェースの提供です。 各RepositoryはAPIのエンドポイントに対応しており、APIコールやCacheアクセスを抽象化しています。 各HTTPメソッド等はmoduleとして切り出してmix-inすることで、実装を分離することができます。
APIレスポンスの取扱い
APIレスポンスのJSONを標準ライブラリ等で適当にparseするとHashになります。 そのままHashの形で上位レイヤまで引きずり回すと、 ViewがHashで汚れてしまい、開発が非常につらくなります。
典型的な問題としては、ネストされたHashのnilハンドリングや、typoとnilの区別が付かないことが挙げられます。 このあたりは、Active Resource等のGemでもRubyのplainなclassにmappingしていたため、 同様の機能を自前で実装し、問題を回避しています。
アーキテクチャの評価
よかったところ
ビューとロジックの独立
リプレイスの目的そのものなので当然ですが、やはりこれが一番大きいです。 既存のDBスキーマ自体かなり複雑化していたので、 DBアクセス処理をがBackend Server側で全てwrapできるメリットは大きいです。
また、APIのインタフェースを変えない限り、 各層が独立してコード/言語/フレームワークを変更できるようになることもメリットとして挙げられます。
コーディングスタイルの統一
Spring, Railsともに標準があるので、コーディングスタイルがブレにくいです。 また、特にRails標準から外れてしまう実装についてはGitHubのWikiにルールをまとめることで、 比較的低コストで全体を把握できるようになっています。
アセットが多い
Spring, Railsともに開発者、ライブラリが多いので開発が楽です。 また、社内外にノウハウが豊富なので実装面で詰まることも比較的少ないです。
悪いところ
パフォーマンス面で不利
Backend Server, Frontend Server間でHTTP通信を挟む以上、 パフォーマンスの劣化は避けられません。 また、Rails単体で見てもNode.jsやGolangと比較するとやはりパフォーマンスが悪いです。
今回のサブシステムでの性能要件では問題なかったのですが、 高い性能要件を求められるような状況下では、Railsで書くのは苦しいです。
RESTから逸脱する難しさ
アーキテクチャ上の問題ではなく、プロダクトの性質上の問題ですが、 リソースが綺麗に作れないような仕様が多く、Rails側でCoCの理念をあまり活かせませんでした。 残念ながら、今回のケースではActive Resourceがうまく刺さらなかったので、 設定を書いて力押ししたシーンが多かったです。
Backend - Frontendの分離により、以前見えなかったサブシステム内の影響度等も見えるようになってきたので、 今後のリファクタリングで綺麗にしていきたいところです。
まとめ
Backend ServerとFrontend Serverを分割するリプレイスを行うことで、 ビューとロジックを独立して開発できるようになりました。 また、リプレイスを通して、暗黙の仕様が明確化(テストコード化)できたり、デッドロジックを排除できたりと、 いろいろ嬉しい副産物もありました。
また、内部設計の変化に伴い、 今まで手を出せなかった組織内課題も改善の方向へ向かっています。例えば、従来はデザインチームがHTML+JS+CSSとして納品したものを、 エンジニアがJSPに実装し直す、といったフローでした。 しかし、リプレイスでビューを独立させたことにより、 デザインチームが直接Slimを触れるようになり、チーム間連携が楽になる等、 業務フローにも変化を与えつつあります。
今回はアプリケーションの話がメインでしたが、リプレイスでは当然インフラ・運用面でも様々な取り組みを行っています。 そのあたりの話はSRE(Site Reliability Engineering)チームの皆さんが面白いブログを書いてくれると信じています。
最後になりますが、リクルートライフスタイルでは一緒に働ける仲間を募集しています(๑و•̀ω•́)و