複雑なドメインにベイビーステップで立ち向かう

 高齢社会に適した情報インフラを構築・提供する株式会社エス・エム・エスで、エンジニアをしている前田隼輔です。2018年7月に入社し、介護事業者向け経営支援サービス『カイポケ』の開発を担当しています。今回は、介護業務という複雑なドメインに対して、既存のモノリシックなシステムに対してどのようにアプローチして改善しようとしているのかについて紹介します。

 カイポケは、介護業務に加えて勤怠管理や給与管理などの様々な機能を備えている介護事業者向けの経営支援サービスです。通常、介護業務は介護保険制度に則って行うので、介護事業者向けのサービスでは介護保険制度(介護保険法)に追従していく必要があります。介護保険制度は3年ごとに法改正があり、情勢などに合わせて変化し続けています。カイポケでは長らくオフショア開発をしていましたが、ドメイン知識やノウハウを社内で貯め、サービスを拡大・安定化して長く提供していくために内製化を進めており、積極的にエンジニア採用を行っています。

複雑なドメイン

 介護保険で受けることができるサービス(介護サービス)は、自宅を生活の拠点としたまま受けられるものや生活の拠点を移して利用するもの、さらに車椅子のレンタルや、手すりの取付といった自宅改修など多岐に渡ります。これら提供する介護サービスの種類によって業務内容は全く異なり、加えて、訪問看護サービスなどの医師や看護師を介したサービスを提供する場合には医療保険制度、サービス付き高齢者向け住宅の運営には国土交通省および厚生労働省が定める高齢者住まい法などが関わりより複雑なものにしています。

 また、日本では高齢化社会と言われて久しいですが、社会の構造は日々変わっています。その変化に対応するために制度自身も変わっていきます。介護保険制度では3年毎(医療保険制度は2年毎)に法改正が行われ、介護事業を続けるにはこれに追従しなければなりません。

 もちろん、我々が提供しているシステムとしても対応していく必要があります。しかしながら、介護保険の法改正は4月に施行されるのに対してシステムに落とし込むための情報が出揃うのが当年の1月以降順次となっており、より柔軟に開発できるようにしておく必要があります。

コンテキストの境界線をみつける

 上述のように、介護保険制度は複雑に変化していきます。複雑に変化し続けるドメインと向き合っていくためには、コンテキストの境界を意識し、システムを小さく作っていくことが望ましいと考えました。

 介護業務の主な内容はもちろん「介護サービス利用者に対するケア」にありますが、介護事業所の運営には「従業員や利用者の情報管理」、「ケア内容の記録」や「介護報酬の計算・請求」などを行う必要があります。管理や記録、金額計算などの運営に必要なこれらの業務はシステムの得意とするところであり、我々はこの部分をサービスとして提供しています。従業員のシフトを組み、実際にケアを行った場合はその内容を記録し、記録した内容は介護報酬の計算に利用するといったように、いずれの業務もそれぞれ関連してはいます。しかし、これらをシステムとしても密に結合すると、ロジック変更時の影響範囲が思わず大きくなったり、新しくシステムに携わる人が理解しにくい状態となってしまいます。今後も変化する介護報酬制度に対応し続けるため、できるだけ小さくてかつ意味のあるコンテキストを探した結果、今は特に法改正の影響を受けやすい「介護報酬の計算」にフォーカスし、ロジックを別アプリケーションとして切り出すアプローチに挑戦しています。

 「介護報酬の計算」とは、介護事業所が利用者に対して1ヶ月のうちに提供した介護サービスの費用を請求するための明細書を作る業務となります。この計算ロジック一つとっても介護サービスの種類や介護事業所および利用者の状態によって大きく異なり、深いドメイン知識が必要となります。別のアプリケーションとして切り出すアプローチは、開発チームとしても分離しやすくなり特定のドメインにフォーカスできることに繋がりました。

境界づけたコンテキストを分離する

 「介護報酬の計算」のコンテキストを分離し、そのロジックを別アプリケーションとして切り出しますが、計算の元となるデータは既存システムのものを利用する必要があります。計算ロジックを提供するアプリケーションを疎にするために、既存システムのデータベースからデータを読み込むアプリケーションを別に用意しました。計算ロジックとデータ読み込みのそれぞれのアプリケーションはマイクロサービスとして提供し、既存システムとはAPIゲートウェイを介して連携するようにしました(下図)。

 APIゲートウェイを介したマイクロサービスにしておくことで、リリースを新アプリケーションの開発チームだけでハンドリングしやすくなり、また、今後既存システムから「介護報酬の計算の元になるデータを作成するドメイン」を分離したときにも連携しやすくしておく狙いがあります。

f:id:shin1rosei:20210330113822p:plain

ロジックを切り替える戦術

 コンテキストで分けられたドメインを別のアプリケーションとして切り出すことで、そのコンテキストに集中することができるようになります。しかしながら、既存システムの特定のロジックを別のアプリケーションに切り替えるには戦術が必要になります。すべての機能を作りきってから切り替え方法を考えるのではなく、段階的に切り替えて行くことにしました。

第1段階: 検証

 既存のシステムからそのままソースコードを移植するのではなく、堅牢で改修しやすいシステムにするには、ドメインを理解してロジックを作り直すことが重要になります。しかし、開発メンバーはドメイン知識が十分に蓄積されているわけではありません。そこで、早い段階で本番環境のデータを使って計算することにしました。

 既存アプリケーションでのイベントをフックとして新規アプリケーションでも計算し、それぞれアプリケーションでの計算結果を比較検証しています。このアプローチにより、実業務でのエッジケースを発見することができ、ドメイン知識の拡充にも役立ちます。また、既存のイベントをフックにしているのでパフォーマンスの課題も気づきやすくなります。現在はこの検証段階であり、差分をみながら開発の優先度決めなどを行っています。

第2段階: ダブルライト

 十分に検証を行ったら、続いては新規アプリケーションの計算結果を利用する段階になります。既存データベースが複数のアプリケーションから参照されていることもあり影響範囲が計り知れません。また、既存データベースに書き込まれた計算結果に対してさらなる操作を行っている部分もあり、それらの機能を実装するとスコープが広がってしまいます。まずはスコープを小さくしてリリースするために、すべての参照を一度に切り替えるのではなく、新規アプリケーションの計算結果を既存データベースに書き込むことを考えています。

 この段階では、介護報酬の計算ドメインとしてはアプリケーション(ロジック)は分離されているがデータベースは分離されていないという状況になります。しかし、ロジックが他のコンテキストから分離されているだけでもドメインの変化への対応はとても素早くなると考えています。

第3段階: 切り替え

 計算結果の参照先を新規アプリケーションに切り替えていく段階になります。これによって計算結果のリソースは共有データベースから開放され、介護報酬計算ドメインとしてアプリケーションおよびデータベースともに分離することが望めます。

 この段階的に切り替えていくアプローチでは、作りきってから組み込むのではなく既存システムの裏で新規アプリケーションを動かしており、新規アプリケーションではAPIを変更したいタイミングも出てきます。しかし、既存システムはリリース時にシステムダウンが必要となってしまうので、直接呼び出す場合新規アプリケーションでのAPIの変更の度に既存システムもシステムダウンを伴うリリースが必要となってしまいます。これを柔軟に変更できようにするために、既存システムからのリクエストは計算に利用するキーのみにとどめて、小さいアプリケーションを挟んで新規アプリケーションを呼び出すようにしました。

技術選定

 一部機能を新規アプリケーションとして切り出すアプローチのため、既存のアプリケーションにとらわれずに技術選定を行うことができます。ドメインロジックを表現するメインのアプリケーションを Kotlin x Spring Boot、現行システムとの受け渡しなどの周辺のツールを Go で構築しています。また、複数のアプリケーションから構成されているので、開発を容易にするために各アプリケーションをコンテナ化し、ローカル環境では docker-compose、本番環境ではAmazon ECSを用いることにしました。

型で計算式を表現する

 Kotlinを選択した理由としては、現行システムが Java で記述されていることを踏まえて学習コストが小さく Java よりも表現力が高いためです。 例えば介護報酬において、報酬額は介護サービスごとに定められた単位数に介護サービスを提供する地域ごとに定められた単価を乗算することで算出します。点数と単価をかける計算は、病院などでうける診療報酬でも同じであるため馴染みがあるのではないでしょうか。

介護報酬額 = 単位数 x 単価

 Kotlinでは演算子オーバーロードといった機能があり、これにより作成したクラスを使って計算式を表現することができます。

class 単位数(val value: Int) {
  operator fun times(tanka: 単価): 報酬額 = 報酬額((this.value * tanka.value).toInt())
}
class 単価(val value: Double)
class 報酬額(val value: Int) {
  override fun toString(): String = "報酬額は${value}円です"
}

fun main() {
    val amount: 報酬額 = 単位数(200) * 単価(10.9)
    println(amount) // 報酬額は2180円です
}

 例えば単位数と報酬額をどちらもInt型で定義してしまうと、改修を繰り返すうちに思わぬところで変数が使い回されたりしてしまうことがあります。単位数と報酬額をクラスとして分けておくことで、報酬額に単価をかけるといったドメインでは存在し得ない計算をコンパイルレベルで防ぐことができます。

プロジェクトを進めるために

 正しさを求めるのではなく、そのタイミングでのベストな選択肢を選ぶことを心がけています。正しさを求めてしまうとその裏付けが必要になり、足が止まってしまいます。実際に、上述のスコープについても何度もスコープを小さく切り直しています。これはドメイン知識が身についてコンテキストの境界線が見えてきたからであり、最初の選択肢が間違っていたことを意味するものではありません。ただし、ベストな選択肢を常に取れるようにできるだけ疎でシンプルにしておくことは重要です。

 また、全て自分たちだけで完結しようとせず他チームとの関係構築も意識しました。既存システムに関わるチームとは別チームとして編成されているので、現チームからジョインしたメンバーなどは課題感などを感じにくくなってしまいます。ドメイン知識においても、非エンジニアや既存システムに携わっている別チームのほうが圧倒的に豊富な場合があります。特にフルリプレイスから現行システムの一部分にフォーカスするように方向性をシフトしたため、初期にはプロジェクトとしての期待値が擦り合ってないことを感じていました。どのように実現しようとしているかを適宜説明し、期待値の調整などを行って他チームとも協力を仰ぎやすい関係性が築けていると思います。

エス・エム・エスにおけるアーキテクトの役割

 システムアーキテクチャを考えることや技術選定は役割の一つです。しかし、技術やアーキテクチャは課題を解決するための手段に過ぎません。置かれたコンテキストにおいて何が課題かを見極め、意思決定を繰り返して、システムやそれ以外の方法で解決に導くことが重要な役割となります。アーキテクトの仕事については、こちらの記事で詳しく紹介しています。

tech.bm-sms.co.jp

 また、エンジニアが貸与されたPCに様々なツールをいれて開発環境を構築し育てていくように、チームや自分が動きやすい環境を作ることも必要です。課題を適切に把握するために、また課題を解決するために必要であればチーム外にも働きかけていく必要があります。それに対して協力を惜しむ人は少ないので、自ら動いていける方には働きやすい環境だと思います。

おわりに

 介護業界は複雑なドメインを有していますが、それを紐解き、システムに落としていく過程は非常にやりがいがあります。そんな複雑なドメインに一緒に立ち向かってくれる仲間を募集しています。エス・エム・エスの仕事に関心がある方は、ぜひこちらのスライドも見てみてください。