こんにちは、@binarytaです。
私の加入しているプロジェクトでは社内システムの運用を行なっています。
近々、その社内システムのリニューアルを行うことになり柔軟で依存の少ないWeb構成について模索しています。
社内システムというのはそもそも、社内での大きな需要が発生したことにより、ある程度恒久的に使用されることを視野に入れて開発されるものだと思います。
しかし、社内で求められる需要や要望、そしてその仕様は社内に特化するため非常に複雑なプログラムになりがちで、どうしても恒久的なプログラムからは遠のいてしまいがちです。
リニューアルするにあたって、再構築を行うための検証期間を設けていただいたので、報告として本記事でその内容を紹介させていただきます。
本連載は以下のような構成で話を進めます。
前編
- 現状のWeb構成/アーキテクチャ
- リニューアル時のWeb構成/アーキテクチャ
- プロジェクトのディレクトリ構成概要
- プロトコル定義の実装
後編
- Spring bootでgRPCサーバの実装
- Ruby on RailsでgRPCによるリクエスト処理の実装
1. 現状のWeb構成/アーキテクチャ
現状のWeb構成はサーバサイドにRails、フロントエンドにVue.js & TypeScriptを採用しています。
フロントエンドにVue.jsを導入する以前は、jQueryによるDOM操作を行なっており、現在もその断片コードは残っています。
サーバサイドでは約1年前までは単純なMVCアーキテクチャを採用していましたが現在はDDDを採用し、ビジネスロジック責務の分離を図りました。
現状足枷として大きな問題となっているのは、サーバサイドのビジネスロジックの肥大化(ファットモデル化)と単体テストがされていないことです。
これらが相まって、新機能開発や既存機能の改修をする際に、既存箇所へ影響する可能性が出てしまいます。
そしてフロントエンドの現状の問題点はVue.jsのtemplate(DOM部分)がTypeScriptによる型チェックがされないことです。
せっかくTypeScriptを導入しているのに型チェックがされないのなら旨味も半減です。
現状のWeb構成を簡易的に示すと下図のようになっています。
前述した問題点を考慮して次はリニューアル時のWeb構成を見ていきます。
2. リニューアル時のWeb構成/アーキテクチャ
リニューアル時に採用しようとしている構成はやや大掛かりに思えますが、問題点は改善されるかと思います。
まず、サーバサイドには2つのサーバが存在します。
ビジネスロジックやDB操作, gRPCによるAPI配信を行うことを責務としたコアサーバ、そしてマークアップ(HTML, CSS)とJavaScriptのレンダリングとルーティング処理、gRPCによるAPI取得を責務としたViewレンダリングサーバの2つです。
これは図に表すと下図のような構成です。
Kotlin側のコアサーバではApache Spring FrameworkによるWebサーバの構築とExposedというORMライブラリを用いたDB操作の役割を担います。
そしてRuby側ではRails5を採用しましたが、MVCのMをKotlin(Spring)側に委ねているため、役割はルーティングとViewのレンダリングのみと、かなりシンプルになっています。
フロントエンド環境はRailsからは独立した形で構築していますが、RailsのVIewテンプレート内からReactが展開されるようになっています。
この構成により、現状抱えているファットモデル化はKotlin(Spring)側のみに集約されます。
また、gRPCによるデータ送受信を採用したことにより、Kotlin(Spring)側とRuby(Rails)側の両サーバ、または片方で言語を変えたいというケースが発生した場合に共通のデータ源(プロトコル定義)が存在するため、言語移行の柔軟性も非常に増すことが期待されます。
そしてRuby(Rails)側とフロントエンド環境を切り離すことにも同様のメリットが存在します。
react-rails等のRubyのgemを採用することは当然ながらレンダリングサーバとフロントエンド環境間での依存を生みます。
両者を独立した環境で構築することで、両者共に技術の再検討をしやすいはずです。
そしてVue.jsからReactへ移行した理由は前述したVue.jsのtemplate内での型チェックがされないことを考慮してです。
Reactで記述するDOM(JSX)はTypeScriptによる型チェックがされます。
長々と背景を説明しましたが、早速実装に関する内容に移ります。
今回は簡素化のため、Todoアプリケーションの構築を目標に実装していきます。
完成後のソースは私のgithubに載せています。
3. プロジェクトのディレクトリ構成概要
gRPCのプロトコル定義ファイルはあらゆる言語間で共通で使用されるため、依存性のないディレクトリ構造にします。
今回のWeb構成であれば、Spring用ディレクトリ、Rails用ディレクトリ、プロトコル定義用ディレクトリをトップ階層に置きます。
各サーバ側ではプロトコル定義から生成されたProtocolBufferのコードが配置されることになります。
Spring側は spring-server/src/main/generated_proto/
配下に、Rails側では rails-server/lib/
配下に配置するように今回は設定していきます。
. ├── proto/ ----------------------- プロトコル定義用ディレクトリ │ └── todo.proto ├── rails-server/ ----------------- Railsプロジェクトディレクトリ │ ├── app/ │ ├── bin/ │ ├── config.ru │ ├── db/ │ ├── Gemfile │ ├── Gemfile.lock │ ├── lib/ ---------------------- この中にProtocolBufferコードが生成されるようにする │ ├── log/ │ ├── package.json │ ├── public/ │ ├── Rakefile │ ├── README.md │ ├── test/ │ ├── tmp/ │ └── vendor/ ├── README.md └── spring-server/ ----------------- Springプロジェクトディレクトリ ├── build/ ├── build.gradle ├── gradle/ ├── gradlew ├── gradlew.bat └── src/ ├── main/ │ ├── generated-proto/ --- この中にProtocolBufferコードが生成されるようにする │ ├── kotlin/ │ └── resources/ └── test/ └── kotlin/
4. プロトコル定義の実装
gRPCの詳しい定義方法については以下の公式ガイドを参照してください。
grpc.io
今回の実装ではTodoに関する 一覧取得 / 追加 / 更新 / 削除 処理をプロトコル定義に実装していきます。
まず、完成後のソースをご覧ください。
syntax = "proto3"; package todostore; option java_multiple_files = true; option java_outer_classname = "TodoStore"; option java_package = "com.example.demo.todostore"; service Todostore { rpc GetTodos(GetTodosRequest) returns (GetTodosResponse) {} rpc AddTodo(AddTodoRequest) returns (AddTodoResponse) {} rpc UpdateTodo(UpdateTodoRequest) returns (UpdateTodoResponse) {} rpc DeleteTodo(DeleteTodoRequest) returns (DeleteTodoResponse) {} } message GetTodosRequest { } message GetTodosResponse { repeated Todo todo = 1; } message AddTodoRequest { int64 id = 1; string content = 2; } message AddTodoResponse { Todo todo = 1; } message UpdateTodoRequest { int64 id = 1; string content = 2; } message UpdateTodoResponse { Todo todo = 1; } message DeleteTodoRequest { int64 id = 1; } message DeleteTodoResponse { bool status = 1; } message Todo { int64 id = 1; string content = 2; }
概略に説明をすると、Todoに関する一連のサービス(API)をservice定義内に実装し、各rpcメソッドのリクエスト、レスポンスのスキーマをmessageで定義します。
それではまず、service定義を見ていきます。
/* -- 略 --*/ service Todostore { rpc GetTodos(GetTodosRequest) returns (GetTodosResponse) {} rpc AddTodo(AddTodoRequest) returns (AddTodoResponse) {} rpc UpdateTodo(UpdateTodoRequest) returns (UpdateTodoResponse) {} rpc DeleteTodo(DeleteTodoRequest) returns (DeleteTodoResponse) {} } /* -- 略 --*/
service定義内には複数のrpcメソッドが存在しています。
これらは、待受サーバ側ではメソッドとして実装し、要求サーバ側では生成されたProtocolBufferが用意してくれるリクエスト用のメソッドです。
(後編で両サーバ言語での実装します)
次にAddTodoというrcpメソッドに焦点を当てて message
定義の内容を見ていきましょう。
messageには各rpcメソッドのリクエスト/レスポンスに対する型定義を実装してます。
/* -- 略 --*/ message AddTodoRequest { int64 id = 1; string content = 2; } message AddTodoResponse { Todo todo = 1; } /* -- 略 --*/ message Todo { int64 id = 1; string content = 2; }
rpcメソッドのAddTodoはリクエストとしてAddTodoRequestメッセージ、レスポンスとしてAddTodoResponseメッセージを返却します。
これらは今回の構成におけるRails側(要求サーバ側)からのリクエストの定義と、Rails側(要求サーバ側)へのレスポンスの定義となります。
そのため、Rails側からSpring側へ要求を送るパラメータの形式をAddTodoRequest, 返ってくるレスポンスをAddTodoResponseというmessageで定義しています。
それぞれAddTodoRequestはint64
でid
、string
でcontent
をパラメータとして要求されることを期待し、AddTodoResponseはTodo
型でtodo
を返却します。
Todo型は別途定義していて、int64
のid
、string
のcontent
が型指定していた箇所はここを参照します。
まだ説明していないものが一つだけあるので、それを最後に説明して今回は終わりにします。
/* -- 略 --*/ service Todostore { rpc GetTodos(GetTodosRequest) returns (GetTodosResponse) {} /* -- 略 --*/ message GetTodosResponse { repeated Todo todo = 1; } /* -- 略 --*/
GetTodosというrpcメソッドは全てのTodoの一覧を返却するメソッドで、レスポンスの型はGetTodosResponseという型を指定しています。
GetTodosResponseの内部にはtodo
というフィールドがTodo
型でrepeated
という構文と共に実装されています。
これはTodoの配列を表す構文です。
他にも定義に関する構文はいくつか存在するので公式ガイドをしっかり読むことをお勧めします。
後半へ続く
次回(後編)はSpring側, Rails側のgRPCに関する実装を共有します。
是非またご閲読ください。