複雑性に立ち向かうためのサーバーサイドコード分割

初めに

kintoneチームの前田です。 kintoneはサーバーサイドがJavaで書かれていて、最近ではこれが結構な分量になっており開発上の障壁となっています。 その解消のため、kintoneチームではコード分割を進めています。 今回は、コード分割の目的や具体的方針、取り組みをチームでうまく進めていくための工夫、 これまでに得られたことや今後の見通しについてご紹介します。

コード分割とは

kintoneがリリースされてから10年経過し、その間に継ぎ足し継ぎ足しでコードが追加されてきました。 最近ではコード行数が35万行を超えてなお増えており、それに伴い複雑性も増している印象があります。 このまま何もしないと、コードの調査や影響範囲を確認するのがどんどん大変になっていったり、 新しくチームに参加した人がコードを把握するのにいままで以上に時間がかかってしまうことが懸念されます。 このような問題に対処するため、コード分割という取り組みを新たに始めました。

コード分割とは、

  • kintoneの機能に従ってコードをまとめ、
  • それぞれの機能がその機能の性質上関わらない場合は、対応するコードも関わらない (= 依存しない) ようにする

という状態にすることです。

kintoneの機能の例としてアプリとアプリ設定を取り上げてみます。 アプリ機能というのはアプリのレコード一覧やレコード詳細などを表示する部分で、 アプリ設定機能というのはアプリのフィールドやアクセス権などを確認・変更する部分です。 これらの二つの機能は、関わるところもあれば、関わらないところもあります。 例えば、アプリが保持するフィールドやカテゴリなどの情報は、 アプリ設定画面で設定するためアプリ設定側と関わりますし、 レコード一覧画面で設定どおりに表示されるのでアプリ側とも関わっています。 一方で、アプリのAPIトークンの生成に関しては、アプリ設定画面でしか生成しないので、 アプリ設定側とのみ関わり、アプリ側とは関わりません。 このように機能の特徴を捉え、それぞれを切り分けて配置することでコードの可読性が上がり、 コードの調査や理解、影響範囲の確認をしやすくなると期待できます。

kintoneではArchUnitを使ってコード分割の仕組みを整えました。 ArchUnitは単体テストで用いるライブラリで、パッケージやクラス間の依存関係をチェックすることができます。 分割後のコード構成では、アプリやアプリ設定それぞれにJavaのパッケージを作成してそれぞれの機能を実装するためだけのコードを置きます。 kintoneのサーバーサイド側のコードはレイヤードアーキテクチャで構成されており、 APIコントローラー、サービスクラス、ドメインレイヤー、リポジトリなどがあります。 各機能に属するこれらのコードすべてを対応するパッケージ内に配置し、 パッケージ外からの意図しない依存はArchUnitを使って検知、修正できるようにしています。

コード分割のイメージ
コード分割のイメージ

コード分割するといっても、現状は各実装を完全に独立させようとはしておらず、クラスを共有することを許容しています。 各機能でコードが完全に独立していればアーカイブ分割や差分ビルドなど開発の効率化を進めやすくなるのですが、 アプリとアプリ設定においてアプリが保持するフィールドやカテゴリのように、 機能間で共有しないといけなさそうなデータが依然として存在します。 コード分割は始まったばかりということもあってこれらをどのように扱っていいかまだ判断ついていません。 したがって、これらの共通コードは今後の課題として今のまま残しておき、 まずはArchUnitで明らかに非依存な部分だけ整理し、 その後に共有している部分をどのように整理していくかを考えていきたいと思っています。

コード分割の基準となる機能の特定はプログラマが独自に行うものではなく、 kintoneのプロダクトマネージャー (PM) がkintoneという製品について分析した結果に基づいています。 アプリとアプリ設定という上記のモデルもPMの分析から得られています。 アプリとアプリ設定にはいろいろな観点から違いがあります。 例えば、アプリ設定側を使うユーザーはアプリの管理権限を持っており、業務リーダーや情シスなどの役職の方になります。 一方で、アプリ側にあるレコード一覧やレコード詳細などは、現場でアプリを使って業務をするユーザーが利用します。 kintoneはもともと実際に現場で働いている人がシステムを作れるといいよねという思想で作られていて、 アプリの作る側と使う側を区別せず開発されてきました。 しかし、アプリを設定する瞬間は、アプリを作る人であって、業務リーダーや情シスなどの性格を持つ。 使う瞬間は、業務で使う。そこは同一人物であっても、違う属性である。このようなことが分かってきています。 コード分割は、PMの脳内にあるこのようなメンタルモデルに沿う形で進めようとしています。 PMのメンタルモデルとコードの境界をあえてずらす必要はなさそうですし、 逆に両者が合致することで機能追加がしやすくなったり、 プログラマからPMに機能の提案がしやすくなったりなど、様々なメリットがあるだろうと考えています。

コードの複雑化に対する取り組みとしてコード分割を選んだのはいくつかの理由があります。 kintoneのサーバーサイドでは、リポジトリのsrc/main/java/ディレクトリ下に全機能のソースコードが置かれています。 もちろんサブディレクトリを使ってある程度整理はされているものの、複雑性が抑制されている印象はありませんでした。 コード分割によりこの整理をさらに押し進め依存関係をチェックするということは、次にやるべきこととして自然なものに思えました。 さらに、コード分割の基準を一から考える必要ないということも分割を後押ししました。 リリースから10年経過し、ユーザー数が増え、様々な使われ方が生み出されてきたことを通じて、kintoneという製品そのものに関する学びや知見がPMに蓄積されていました。 こうして醸成されたPMのメンタルモデルが分割の基準にもそのまま適用できるものだったため、分割のハードルが下がりました。 また、コード分割の最中でも開発チームは滞りなく新規機能開発できるという点も挙げられます。 コード分割はチーム全員で行うわけではなく、むしろチームの大半は新規機能の開発をしています。 コード分割での主な作業はファイルを別のディレクトリに移動することなので、ファイルの内容は分割前後でほとんど変わりません。 したがって、知らない間に以前と全然違うコードになっている!?ということがないため、分割作業中も開発チームは違和感なく作業を継続することができます。

コード分割プロジェクト

ArchUnitを導入してコード分割のための仕組みを整えたのですが、コード分割はなかなか進みませんでした。 要因としては、サーバーサイドのコード分割をどのように進めていいか分からないことから手を出しづらいということでした。 そこで、コード分割の経験を積み、参加メンバ各々がコード分割をできるようになるという目的でプロジェクトを立ち上げ、 コード分割に興味があるメンバに参加してもらいました。 プロジェクトについてはJJUGでのスライドをご参照ください。

分割に選んだ機能はアプリ設定機能です。 アプリ設定を選んだのは、機能的な観点で他から依存されてないため分割がしやすそうであり、かつkintoneとしても重要な機能、という理由からでした。 しかし一口にアプリ設定といっても、その中にはフォームの設定やアクセス権など様々なサブ機能を持っています。 これらの中からコードの分量や依存の少ないものを選んで、一機能ずつアプリ設定用のパッケージにコードを移動していきました。 アプリ設定の中でサブ機能は約20あり、2022年11月から2023年1月のプロジェクト期間に5機能が分割されました。

プロジェクトはモブプログラミングで進めました。 アプリ設定機能でコード分割を行うというのは、kintoneチームにとっては初めての経験で、どのように分割していいのか手探りの状態でした。 また、コード分割をすると100~200ファイルに修正が入り、ほとんどの差分はJavaのパッケージが変わるだけの軽微なものでありつつ、 こういった変更をレビューするのは難しいです。 その点モブプログラミングでは、手順の確認を都度取ることができ、コードの認識をそろえながら作業を進められるので、やりやすかったかなと思います。

また、モブプログラミングは知見を共有する点でもうまく働いたと思います。 最初は私がコード分割の仕組みを整えてそれなりに知見を持っていたということで、 分割後のコード構成や分割のためのIDEの使い方などを手厚めにナビゲーションをしながら進めました。 そしてプロジェクト終盤になりある程度メンバが作業に慣れてくると、私はナビゲーションもドライバーもほとんどやらずずっと観察していました。 これにより各メンバの学習が促進されますし、それに加えてコード分割時にやりがちなミスも認識することができました。 やりがちなミスは、プロジェクトに参加していないメンバに今後コード分割の進め方を共有するときに有用になると思っています。

分割を通したコードと複雑性の理解

分割後のアプリ設定のコードは分割前に比べて可読性を上がっているのを実感しました。 それに加えてコードに対する理解が深まったり、コードを複雑にする要因が見えたように思います。 ここではkintoneのAPIトークンの分割の例を紹介します。

kintoneのアプリではAPIトークンを利用できます。 APIトークンはアプリ設定のAPIトークン設定画面で設定の確認、変更をすることができます。 設定されたAPIトークンは、REST APIを実行する際のリクエストヘッダに乗せて利用します。 例えば、レコード取得のREST APIに利用した場合、レコードを取得する処理の中での権限評価に使われます。

コード分割前のAPIトークン周辺のコンポーネント依存関係は下のようになります。

コード分割前のAPIトークン周辺のコンポーネント構成
コード分割前のAPIトークン周辺のコンポーネント構成

APIトークン設定を分割する前は、レコードを取得する場合もAPIトークンを変更する場合も、 どちらもApiTokenというデータクラスやApiTokenServiceというサービスクラスを使用していました。 レコードを取得する場合はApiTokenService#getTokenを実行することでApiTokenを取得し、ApiTokenが持つcanViewを参照してレコードの閲覧が可能かを確認します。 APIトークンを変更する場合は設定をApiTokenに変換してApiTokenService#updateを実行し、DBに永続化します。 その他、既存の設定を確認するためにはApiTokenService#listを使います。

APIトークン設定を分割するためには、ApiTokenServiceApiTokenなどのクラスを、 アプリ設定のAPIトークン設定専用のパッケージ (appsetting.apitokenパッケージ) に 配置することになります。 しかし、今のままではこれはできません。 もしこのまま分割すると、ApiTokenServiceApiTokenappsetting.apitokenパッケージに配置され、 ArchUnitによってappsetting.apitokenパッケージ外から依存できなくなります。 レコードを取得する処理はappsetting.apitokenパッケージ外にあるため、ApiTokenを取得して権限チェックをすることができなくなってしまいます。

依存を整理しないまま分割しようとした場合
依存を整理しないまま分割しようとした場合

そもそも、appsetting.apitokenパッケージはアプリ設定のAPIトークン設定画面のために存在するコードであり、 レコードの取得がこのパッケージに依存するのは役割の観点からおかしいです。 たしかに、レコードを取得する際にAPIトークンの権限をチェックする必要はありますが、 だからといってAPIトークン設定のための実装に依存する必要はないはずです。

そこで、レコード取得用のAPIトークンの権限を表現するApiTokenRightとそれを取得するためのApiTokenRightServiceを新たに定義し、 コード分割を行いました。 コード分割後のコンポーネント構成は以下のようになります:

コード分割後のAPIトークン周辺のコンポーネント構成
コード分割後のAPIトークン周辺のコンポーネント構成

これでレコード取得はappsetting.apitokenパッケージに依存しなくなり、コード分割が可能になりました。 ApiTokenRightServiceはレコード取得時にApiTokenRightを返却するためのインターフェイスでappsetting.apitokenパッケージ外に存在しますが、 その実装クラスであるApiTokenRightServiceImplappsetting.apitokenパッケージ内に存在します。 ApiTokenRightServiceImplはDBからAPIトークンの情報をロードし、ApiTokenRightに直して返却します。 このロードを実装するために、現状ではアプリ設定のAPIトークン設定用テーブルを参照する必要があるため、 ApiTokenRightServiceImplappsetting.apitokenパッケージに配置しています。 ここでSpringのDIを利用することでレコード取得側の処理が明示的にappsetting.apitoken下のApiTokenRightServiceImplに依存するのを防いでいます。

なお、appsetting.apitokenパッケージからApiTokenRightに依存がありますが、これは現状許容しています。 そもそも、ApiTokenRightへの依存以外にも、ApiTokenRightServiceImplはレコード取得のための処理であるためappsetting.apitokenにあるのは役割としておかしいはずです。 こういったことは、上述したアプリとアプリ設定の両者で共有しなくてはいけない情報 (今回の場合はAPIトークン) があることと関係しており、 今後の課題とするところであります。

ApiTokenに存在したdescriptionプロパティが、ApiTokenRightには存在しないことに気づかれると思います。 そもそもdescriptionとはアプリ管理者がAPIトークンの用途を記録するために使われるもので、 アプリ利用者がレコード取得する時に使われることは意図していません。 したがってレコードを取得する際にはdescriptionは不要であり、ApiTokenRightからは削られました。

このようにアプリ設定の内と外のそれぞれでデータクラスを作成したことで、コードの変更がしやすくなったと思います。 descriptionを追加するような、アプリ管理者がよりAPIトークンを管理しやすくする変更は、今後もアプリ設定側で起こりえます。 そのような変更はすべてApiTokenで吸収することで、 レコード取得側に影響を及ぼすことなくアプリ設定側にのみ意図した修正を加えることができると考えています。

また、データクラスが2つできるということは、PMのメンタルモデルとも逸脱していなさそうでした。 メンタルモデルでは、アプリ設定側には当然アプリの設定情報が存在し、アプリ管理者が確認、変更することができます。 一方でアプリ側にもアプリの設定に関する情報が存在します。 それはアプリを動かすためだけに必要となる情報であり、アプリ設定側のものとは違ってきます。 今回作成されたApiTokenRightdescriptionプロパティがないため、アプリを動かすためだけに必要なものであり、 アプリ設定側では使うことができないということで、上記の特徴と合致していそうです。

サービスクラスの可読性も上がっていると思います。 分割前はApiTokenServicegetTokenlistという似通った名前のメソッドが存在しました。 getTokenApiTokenを一つだけ、listApiTokenのリストを返却するメソッドで、 両者の違いは一見すると返り値が単一のクラスかリストかということに思えます。 しかし実際は役割や実装が異なっており、分割後では別々のサービスクラスに存在しています。 listはアプリ管理者がAPIトークンを確認するためのものなので、listを実行するための事前条件などはupdateと同じです。 しかしgetTokenの事前条件はlistとは異なります。 これは、getTokenの目的がレコード取得時のアクセス権評価であり、アプリ管理者は特に関わっていないためです。 事前条件が違うためそれに伴う実装はgetTokenlistとでは異なります。 分割前はこれらのメソッドが同じサービスクラスに定義されており、一見すると実装が不揃いに見えるだけになってしまっていました。 分割後のように各メソッドがそれぞれの目的に応じたサービスクラスにあるほうが、コードがより理解しやすくなると思います。 また、分割後にgetTokenが返却するデータクラスはdescriptionプロパティの削られたApiTokenRightになっており、 listとは違うクラスになっています。 これもgetTokenlistの性質の違いによるものであり、分けることは妥当ではないかと考えています。

APIトークンをはじめアプリ設定内のサブ機能を分割して気づいたことは、 アプリ側とアプリ設定側でデータクラスやサービスクラスを共有するとコードが複雑になりがちということでした。 分割前はアプリの作る側と使う側を区別しないという製品の特長だからなのか、 レコード取得とAPIトークン設定で同じデータクラス、サービスクラスが使われていました。 しかし、APIトークンのデータクラスではレコード取得時に必要ないdescriptionというプロパティまでロードされていましたし、 サービスクラスではgetTokenlistという似たようなメソッドが同一のクラスにありつつも、その役割や実装は違ったものでした。 このようなことがコードを複雑化させているのではないかと思っています。

逆に、何らかの機能を設定する側と設定された機能を利用する側でデータクラスやサービスクラスを分ける1と、実装が素直になりそうです。 ある機能があって、その機能の設定をする部分と、設定された機能を利用する部分に分けられるとします。 設定したり利用したりするのは同じ機能ではありますが、設定側にとって都合のよい情報構造が、利用側にとっても都合のよい情報構造になるとは限りません。 設定側にだけあると都合のよい情報があるかもしれません。 にもかかわらず、情報構造を表現するデータクラスを一緒にしてしまうと、利用側にとって最適でないデータ構造が設定側の都合で選択されるかもしれません。 サービスクラスを一緒にしてしまうと、設定側と利用側のそれぞれによる似たような見た目だけどやりたいことが違うメソッドが混在してしまいます。 こういったことでコードの可読性が下がり、複雑になっていきます。 そこで設定側と利用側のぞれぞれでデータクラスやサービスクラスを作成するというわけです。 この考え方はいろいろな場面で適用できるパターンになりそうなため、今後の分割作業でさらに検証していくつもりです。

今後

コード分割の仕組みを整え、プロジェクトを通してアプリ設定のサブ機能の分割を試みました。 3ヶ月のプロジェクトの間に約20あるサブ機能のうち5つ分の分割が終わりました。 私を含めてプロジェクトに参加したメンバはコードを分割することでコードの可読性が上がり、理解や変更が容易になりそうなことを実感することができました。 そしてプロジェクトが終わった後も、アプリ設定を分割し終えることを目標にチームはすでに動き出しています。 また、アプリ設定だけでなくアプリ側も分割しようと、プロジェクトに参加した別のメンバが取り組み始めています。 プロジェクト開始前ではまったくコード分割が進んでいなかったことを考えると、これは大きな進歩だと思います。

コード分割が進むとDB分割、サービス分割と道が開けていきます。 今後の課題としていたアプリとアプリ設定の共通部分のコードも、DB分割によって解消されるかもしれないということが最近分かってきました。 サービス分割までできると、kintoneチームがアプリ設定の機能をどれだけ変更してデプロイしても、 アプリ側では変わらずアプリ利用者が快適にレコードの確認や修正をできる、というようなことも可能かもしれません。 ユーザーやPMのメンタルモデルのことを気に留めつつ、これらのことを進めていこうと思っています。


  1. これはSRPの一例かもしれません。