株式会社ヘンリー エンジニアブログ

株式会社ヘンリーのエンジニアが技術情報を発信します

医療現場の未来を創る製品開発

こんにちは。ヘンリー CEOの逆瀬川です。

この記事は株式会社ヘンリー - Qiita Advent Calendar 2024の25日目の記事です。前回はあかつきんぐさん の「CSキャリアの描き方(カスタマーサクセス・カスタマーサポート)」でした。

私たちヘンリーは、病院向け基幹システムとして電子カルテ・レセコンを開発・提供している会社です。医療システムの開発は、単なるソフトウェア開発以上の意味を持っています。私たちの製品は、病院の業務効率化だけでなく、地域の患者さんへの貢献、そして日本の医療全体の発展に貢献できる可能性を秘めていますと考えていますので、それについて本日は説明したいと思います。

病院への貢献

医療機関が直面している最大の課題の一つが、膨大な業務負担です。先日訪問した地域の中心的な病院でも、大量の紙を使って業務を行なっていました。毎年、段ボール250箱分のカルテが生み出されて保存されています。(カルテは法令で5年保存する必要がある。)

紙を減らし、全体の業務フローを変革することが出来るのが、基幹システムを提供している会社の面白さです。

例えば、とある病院さんでは、今まで保管義務のある様々な書類を紙で保存していましたが、厚生局に許可を取り、法令上のルールを満たすかたちでHenry上に保存し、紙を大幅に減らすことに成功しました。
また、他の病院様の例ではリモートワークを一部取り入れ、医師が家から指示が出せるようになったり、スタッフも家で会計・請求業務をを行うことが出来るようになりました。

より詳しい導入事例は、以下の記事でご覧いただけます:

患者様への貢献

B2Bの事業だから患者様への貢献が感じにくいのかという印象をお持ちのあなた。そんなことはありません。 ヘンリーを導入する医療機関様は、もれなく地域の医療を支えている病院であり、そのことに誇りを持っています。電子カルテを入れていく過程でも素晴らしいエピソードを聞くことが出来ます。

例えば、とある緩和ケアの病院様の話です。緩和ケア病棟の病院では、その方々の感情に寄り添いながらケアしていくことが重要になります。以前は、紙で看護記録を取られていたのですが、「Henry」導入後は、その場で電子カルテに記入できるため、生の感情を即時に記載することが出来、患者様やご家族のお気持ちを忘れずに記録することが出来るようになりました。「お気持ちに寄り添ったケアを重要視するホスピスの病院にとって、とても大きな進歩だと言えます。」と看護師長の方に仰っていただきました。 また、他の病院様では、「Henryを導入後約1年で、入院のベッドの稼働率を100%に上げることが出来ました!」と嬉しい報告をもらいました。今まで、病院の業務が忙しく受け入れる事ができなかった患者さんが入院出来るようになるのは素晴らしいDXの成果ではないしょうか。

医療全体・国への貢献

病院の現場を理解している事業者として、病院DXの理想を発信して行くことは、私たちの重要な使命だと考えています。 ヘンリーは、唯一のクラウドネイティブの電子カルテ・レセコンを提供しており、業界内で最新の技術プレイヤーとして国や業界団体への声が届きやすい状況になってきています。今年は元厚生労働省のメンバーも参画し、政策提言や情報発信の機会が増えています。また、厚生労働省関係者にHenryを導入した医療機関の現場を視察いただく機会を設けたり、医療政策に関心の高い議員の方々へ直接提案する場をいただくこともできました。

note.com

こうした取り組みを通じて、現場での業務改革の成果を国の政策に反映させ、日本の医療全体に貢献する可能性を強く感じています。

製品開発チームがどのように貢献しているか

Henryでは、導入支援を担うメンバーが中心となり、病院現場の業務フロー改革を推進しています。その一方で、製品開発チームも現場で生じる課題やニーズを直接収集し、反映させる体制を整えています。ユーザーインタビューの実施や病院訪問を通じて、チーム全員が現場の声を共有し、理解を深めています。

最近では、導入支援チームと製品開発チームが連携し、「運用合意ミーティング」という取り組みを始めました。運用フローを双方で検討・提案し合うことで、製品開発チームも現場の業務改革に直接関与できる体制を作り上げています。まだ改善の余地はありますが、この取り組みを通じて、現場と開発が一体となって医療DXを推進する取り組みが整いつつあります。

今後は、病院のDXの先にある患者様や社会全体へのインパクトを見据え、日々の業務に取り組んでいきたいと考えています。

日本の医療現場の未来を作る仕事をしよう!

Henryを開発することは、日本の医療現場の未来を創造する挑戦です。確かに難しい課題も多いですが、それだけにやりがいのある仕事です。まだプロダクトも組織も発展途上ですが、チーム全員で力を合わせ、より良い未来を築いていきましょう。

あなたの検査がカルテに書かれてから結果が返ってくるまでに裏側のシステムでは何が起きているのか

株式会社ヘンリーで電子カルテと外部機器・ソフトウェアの連携部分の開発を担当している坂口(id:wakwak3125)です。この記事は株式会社ヘンリー Advent Calendar 2024の14日目の記事です。前回は「大きくて複雑なものを作る人達へ送る一冊|永田 健人」でした。

はじめに

電子カルテというものはたくさんの周辺分野の機器やソフトウェアと連携をして診療行為を記録していますがその中でも比較的身近な検査に焦点をあてて実際に裏側ではどのようなデータのやり取りが行われているのか、どういった課題があるのかについて解説してみようと思います。 ここでは院内や院外で実施される血液検査や尿検査などに代表される「検体検査」を対象とします。

検査システムと電子カルテの役割

まず検査システムと電子カルテのそれぞれの役割を簡単に解説します。

検査システム

血液検査や尿検査などの検体検査の結果の表示や患者情報の管理、検査会社・検査ラボへの検査情報の送信などを担っています。 また、検査結果の上での印刷もこのシステムで実施することができます。

電子カルテ

電子カルテは言わずもがなですが処方内容やどのような検査を行なったのか、どんな病状なのかということの詳細が記録されています。 今回解説する本題である検査システムとも連携をして電子カルテ上で入力した検査を検査システムに送信したり、結果を受け取り表示をすることもできます。

連携の際には各検査会社が用意している検査名称やコードと実際に電子カルテ上で選択する診療行為の紐付けがなされたマスタを登録する必要があります。

それぞれが独立しているとどうなるのか

診療時に複数の画面を行き来することが必要になったりと操作が煩雑かすることが予想されます。また検査会社を複数利用している場合はその分だけ管理するソフトウェアも増えるためより煩雑さが増します。

こうならないためにも電子カルテ上で一元して見られるようになっていることが便利なのはいうまでもないことだと思います。

連携の仕組み

では本題として実際にはどのようなデータのやりとりが実施されているのかを見ていきたいと思います。

連携の基本はファイルの交換

まず初めにWebAPIはほとんど存在しない世界(もし対応している検査会社さんがいらっしゃいましたらご一報ください!)なので、電子カルテと検査システムが共有しているPCのフォルダに検査の「依頼」と「結果」のファイルをそれぞれ出力し合うような連携方法が主流です。

依頼連携について

医師が電子カルテに「この患者さんについて、この数値を検査したい」といった指示が書かれると、検査システムにその内容が登録されます。また、医療機関ではその指示を元に採血や採尿を行います。このあたりの細かいオペレーションは医療機関ごとには変わってくると思うので一例としてお考えください。

指示をうけると検査システムは検体に貼り付けるためのラベルを出力します。このラベルには患者名や患者番号、採取日などの情報が記載されています。

依頼時に出力するファイル

このオペレーション中に電子カルテはMEDISと呼ばれる形式の固定長のファイルを検査システムへ登録します。その一例を見てみましょう。

"O1HRY   202105031234572664641                             2fad237a2-1          1234512345          テスト カンジャ           2Y028 9301012105030000010            ML                                                                                                  "
"O2HRY   2021050312345726646400001                    00002                    00001                    00002                    00001                    00002                    00001                    00002                    00001                       "
"O2HRY   2021050312345726646400002                                                                                                                                                                                                                               "

サンプルなので多少のミスがあるかもしれませんが多めにみてやってください。非常に見づらそうということがわかるかと思います。 ヘンリーの社員は特殊な訓練を受けている人が多いのでわかる人が多いです。僕は最初意味がわかりませんでした。

このMEDISファイルにはいくつか特徴があるのですが、特筆すべきものを挙げるとするならば検査会社・ラボごとに少しずつ仕様が異なるということです。 標準的な仕様があるにはあるんですが、各社歴史的な経緯もありそうも言えないのが現状です。 一例を以下に列挙すると

  • 性別を表す値が各社違うため、男が1のケースもあれば、男がMのケースもある
  • 依頼のユニークキーが20桁利用可能なのが標準仕様だが10桁しか利用できない
  • 固定長の右詰左詰が項目ごとに少し違う ...etc

などさまざまです。現在は連携する各社の仕様をベースに依頼ファイルの作成のコードを実装するような形になっています。

結果連携について

そうして依頼された検査を検査会社が実際に実施したあと、結果情報が検査システムへと送信されます。この辺も検査会社によりけりですが検査システムから結果出力する機能がない場合は、検査結果が入ったUSBメモリを利用して結果データを取得しているケースもあります。

取得方法はさておき、検査結果のファイルをまたもや電子カルテと検査システムが共有しているフォルダへと配置すると電子カルテはその情報を吸い上げて患者の検査結果データとして保存します。

検査結果には基準値や異常値などのフラグが入っている形式もあり、検査結果を表示する際にはそれらの情報を利用してわかりやすく表示することが求められます。

結果ファイル

それでは検査結果ファイルの一例を見ていきましょう。Henryが対応している結果フォーマットは3つあります。

MEDIS形式: 先ほどの依頼ファイルに対になる結果形式です。こちらも固定長のファイルになっています。

A1XXXXXX2021112599000000010016072759059         テスト タロウ テスト                   10100100              6.6       10100900               40       10101000               16       10101100              690       10101200              338                           
A1XXXXXX2021112599000000010016072759059         テスト タロウ テスト                   10109700                -       10109800                -       10109900                -       10130100             ベッシ       10130101             63.4

Wolf形式: CSVで表現されており比較的使いやすいファイル形式です。

"XXX","20220107","abcdefghij","0000012345","0000123456","テスト カンジャ1","","","","","","","","","5B106-00","LARGE マルチマ-","1","1234","5","","","","H","","12.3","mg","10.0","E","10.0","0.0","","","","","",""
"XXX","20220107","jihgfedcba","0000012345","0000123456","テスト カンジャ1","","","","","","","","","5B106-00","LARGE マルチマ-","1","1234","5","","","","H","L","12.3","mg","10.0","E","10.0","0.0","","","","","",""
"XXX","20220107","klmnopqrst","0000067890","0000789012","テスト カンジャ2","","","","","","","","","5B106-00","検査項目","2","1235","6","","","","L","","34.5","L","10.0","L","10.0","0.0","","","","","",""

HL7 FHIR v2.5: FHIRと呼ばれる医療情報共有の標準規格です。v2.5は2003年に発表された規格です。

MSH|^~\&|H100|XXXXX||999999999200|20221017152504||ORU^R01|99999999920020221017152504|P|2.5|||NE|NE|81|ASCII~ISO IR87~ISO IR159||ISO 2022-1994
PID|1||12345||サンプル^ホウコクショ^^^^^L^P||19631130^058|F||||||||||01
PV1|1|O|^内科^1病棟^1F||||Dr ハヤブサ
ORC|CN||2022101800999501||CM||||||||||20211203
OBR|1||2022101800999501|||20221018|202210181330||1000^ml||||149^40.5|20221018|01^全血|||||ビコウ999999999999||20221017142457|||F||PC^230^R^^術前||||||||||||3^^弱溶血
OBX|1|NM|3J010000002327101^総ビリルビン^JC10^00050000^総ビリルビン(T-Bil)^XXXXX^1|1|0.8|^mg/dL|0.2-1.2||4||F|||20211203|XXXXX|ヘンリー太郎^総合研究所
OBX|2|NM|3J015000002327101^直接ビリルビン^JC10^00070000^直接ビリルビン(D-Bil)^XXXXX^1|1|0.5|^mg/dL|0.0-0.4|H|5||F|||20211203|XXXXX|ヘンリー太郎^総合研究所
OBX|3|NM|3J020000002391901^間接ビリルビン^JC10^00090000^間接ビリルビン(I-Bil)^XXXXX^1|1|0.3|^mg/dL|0.2-0.8||4||F|||20211203|XXXXX|ヘンリー太郎^総合研究所

結果ファイルについては依頼ファイルほど各社の差異は少なくフォーマットの違い程度のため比較的扱いやすいというのが感想ですが、クセもあるため一概にどれがいいというものではありません。しかし個人的にはCSV形式が楽だなぁという気もします。

これらのファイルについても特殊な訓練を受けたヘンリー社員はぱっと見でどんな結果データが入っているかがわかるようです。すごい。

連携時のマスタデータについて

検査システムと連携をする際にはマスタ管理も重要なトピックにあがります。

例えば初めて電子カルテを利用される医療機関さまではこれまで慣れ親しんだ紙での依頼時に利用していた名称をそのまま利用したいということが考えられます。 そのため医療機関ごとにマスタを用意していることがほとんどです。

また、マスタのフォーマットも各検査会社で違い統一された規格というものは無いように感じます。(だいたい書かれている内容は同じなのですが...) そのため、連携時にはHenryの定めるマスタの形式に変換をして登録と管理を行なっています。

検査マスタについてはここでは書ききれないほどの課題があるのでまた機会があれば記事を書きたいと思います。

連携時の課題

検査システムとの連携にはいくつかの課題があります。

  • 依頼ファイルが正常に連携先システムに正常に取り込まれたどうかの結果を受け取ることが難しいこと
    • ファイル連携では取り込まれたかどうかの結果があくまでも「ファイルがフォルダから消えた」という以上のものがありません。
      • 取り込まれたが内容に問題がある場合などを検知することが難しい状態です。
  • 共有フォルダという仕組みを使っている以上、院内のネットワーク状態・PCの状態に強く依存するということ
    • 例えば検査依頼がシステムに飛ばない、というサポートリクエストが来た際に原因を調べると連携用のプログラムが動いていなかったり院内のネットワーク設定やセキュリティ設定が変わっていることに起因するケースが少なくありません。
  • 検査会社ごとに仕様が異なるため微妙な調整が入ることがあり、連携時の工数が低く無い
    • 「依頼はMEDIS、結果はWolfで」と合意をとっても中身が一味違う味付けになっていることが多いです。

それぞれの課題にどう向き合うか

ヘンリーではそれぞれに対して一発で解決できるような必殺技は持ち合わせていないの一つ一つ対処しています。

例えば、院内のネットワークやPC設定に起因する部分については問い合わせフローの効率化などを行うことで対応速度・工数を減らしたり、仕様合意では聞くべき内容の型化を行い抜けや漏れがないように務めるなどのいわゆる「運用でがんばる!」みたいなところでしか解決できていないのが正直なところです。

この辺りについては来年には解決し切っておきたい課題だなぁと思っています。興味がある人やこうしたらいいのでは?という案のある方は是非一緒にお話ししましょう!

明日は id:shenyu_cyan による記事です!お楽しみに。

GitHubのGraphQL APIを使ってPull Requestを分析するためのレシピ

株式会社ヘンリーでSREなどをしている id:eller です。この記事は株式会社ヘンリー Advent Calendar 2024の6日目の記事です。前回は id:Songmu の「社内ラジオをポッドキャスト配信する」でした。

先日GitHubのGraphQL APIでエンジニアのPRの使い方を分析したので、やり方を残しておきます。

材料

  • gh コマンド
  • jq コマンド
  • 適切な権限を持つGitHubアクセストークン
  • 質問をするためのGenerative AI(あれば)

概要

今回は指定日よりもあとに作成されたPRの一覧をCSVで出力することを目指します。中間データにJSONを採用して、 jq コマンドでいい感じに統計処理できるようにします。

手順

まずシェルスクリプトを書くのですが、今回はあらかじめ作成したものをご用意しております:

#!/bin/bash

# 過去3ヶ月の開始日を計算
START_DATE=$(date -v-3m +%Y-%m-%d)

gh api graphql --paginate -f query='
query($endCursor: String) {
  repository(owner: "example.com", name: "repository-name") {
    pullRequests(first: 100, after: $endCursor, orderBy: {field: UPDATED_AT, direction: DESC}) {
      pageInfo {
        hasNextPage
        endCursor
      }
      edges {
        node {
          number
          author { login }
          createdAt
          url
          merged
          baseRefName
          reviews(last: 100) {
            nodes {
              author {
                login
              }
              state
}}}}}}}' --jq ".data.repository.pullRequests.edges[].node | select(.createdAt > \"$START_DATE\" and .merged) | select(.baseRefName == \"develop\" or .baseRefName == \"master\")" > pr_data.json

# ユーザーごとの統計を計算
jq -r -s '
  group_by(.author.login) |
  map({
    author: .[0].author.login,
    total_prs: length,
  }) |
  sort_by(-.total_prs) | map([.author, .total_prs]) | .[] | @csv' pr_data.json > user_stats.csv

ここのポイントは gh api graphql --paginate です。ページング処理を gh api コマンドに任せることで、スクリプトそのものはシンプルにまとまっています。とても嬉しい機能です。 注意点としてはレビュアーが100名を越えないことを前提とした作りになっていますが、多くの場合は問題がないでしょう。

取得したデータに対するフィルタリングを --jq オプションで実施して元となるデータをJSONファイルに落としてから、改めて jq コマンドで統計を取る方法を採っています。 元データをまずJSONファイルに落とすことで、統計部分で試行錯誤をする場合にGitHub APIの利用からやり直すことを防ぎ、トライ・アンド・エラーのサイクルを早めています。 質を高めるには味見は大切ですから、スクリプト開発においても味見がしやすい作りを意識したいものです。

おそらく .createdAt での絞り込みはGraphQL APIでやった方が無駄な検索・通信が発生しないため望ましいはずで、次回改善の余地がありそうです。

GraphQL APIと仲良くすればGitHub上での活動の透明度がぐっと上がります

こんな感じでGitHubのGraphQL APIを使えば、量が多いデータもよしなに扱うことができます。GitHub上でのアクティビティを可視化する方法には色々と提唱されていますが、ちょっと思いついた仮説を検証したい場合にはGraphQL APIが特に便利にご利用いただけるのではないかと思います。

明日は @helene815 の「出社回帰のニュースを見て思うこと」の予定です。お楽しみに!

社内ラジオをポッドキャスト配信する

id:Songmu です。この記事は株式会社ヘンリー Advent Calendar 2024 5日目の記事です。

ヘンリーは北海道から沖縄まで、何なら上海から働いている社員さえいるフルリモートの会社です。フルリモート環境においては特に、社員の相互理解、そのためのそれぞれの自己開示が大事です。リモートに限らず、一部の人の声が大きくなりすぎて多様性が損なわれないよう、メンバーに自己開示を促し、支援することも必要です。

ヘンリーではその促進のために、Notion上に自己紹介やユーザーマニュアル、社内ブログ等を書いてもらう取り組みをしています。それに加えて Henry FMという社内ラジオがあり、広報メンバーを中心とした有志のギルドで運営されています。社員へのインタビューや雑談の録音を元に、30分程度の音声コンテンツが週に2本程度更新されています。これもNotion上で社内公開されており、Notion上で音声公開されています。

ポッドキャスト化の道のり

Notionに音声ファイルをアップロードするとHTMLのaudio要素として埋め込まれ、ブラウザのプレイヤー上で再生できます。ブラウザにもよりますが最大倍速再生できるのでそれなりに便利です。ただ、業務端末だったりスマートフォンでNotionを開いたままにしておく必要があるため、隙間時間で聞くには不便です。

普段から隙間時間でポッドキャストで多くの番組を聞いている私個人としては、同様に隙間時間で使い慣れたポッドキャストクライアントで聞きたいと思い、ポッドキャスト化することにしました。

プライベートコンテンツをポッドキャスト化する難しさ

ポッドキャストは、配信サービスを利用せずとも、独自のホスティング環境で始めることはそれほど難しくありません。ポッドキャストは単にブログで使われているRSSが拡張されたオープンな規格です。その反面、インターネットで公開されたオープンコンテンツを前提とした規格であるため、プライベートコンテンツを配信するには向いていません。RSSは音声ファイルのURLに認証をかけてしまうと、使い慣れたポッドキャストクライアントで聞けなくなってしまいます。

プライベートポッドキャストの現実解

今回は現実解として推測が困難なURLで配信することとしました。Basic認証をかけ、それに加えて多層的に防御することとした。完璧にはできませんが、コンテンツも最悪漏洩しても良い情報に限り、機密情報を扱わないようにすれば許容できます。

具体的には以下のような対応をしました。

  • サイトコンテンツ (ポッドキャストサイト及びRSSフィード)
    • 推測しづらいURLにする
    • Basic認証をかける
    • robots.txtやHTTPヘッダ、メタタグで不要なクローリングやインデックス登録を防ぐ
    • HTTPヘッダやメタタグでリファラを送らないように
  • 音声ファイル
    • 推測しづらいURLにする
    • robots.txtは置く
    • 認証はかけずURL直打ちでアクセスできてしまうことは許容する
  • RSSフィード
    • <itunes:block>yes</itunes:block> 要素を指定する

Basic認証を使うことにしたのは、ポッドキャストサイトはシンプルな静的サイトとして構築して、ホスティングサービスで配信しつつ、それにBasic認証をかぶせる方式が手軽だと考えたからです。Basic認証であれば、社員ごとに個別に認証情報を追加で発行することも容易です。

iPhone標準のポッドキャストアプリや、私が利用しているOvercastはBasic認証に対応しており、問題なくポッドキャストを購読できます。

この辺りの技術的な詳細は私の個人ブログの以下の記事に解説を譲ります。

社内プライべードポッドキャスト実現方法

Cloudflare Pagesで配信する

ポッドキャストサイトを配信する静的ホスティングサービスにはCloudflare Pagesを利用しています。最近のホスティングサービスはもはやBasic認証に対応しているものは少ないのですが、Cloudflare Pages Functionsを使うことでBasic認証のミドルウェアレイヤを追加できること、音声ファイルの配信にオブジェクトストレージのCloudflare R2を利用できる点、非常に廉価にお手軽に始められる点などが魅力的で選定に至りました。

Cloudflare PageでのBasic認証のかけ方はこれもまた私の個人ブログの以下の記事を参考にして下さい。

Cloudflare PagesにそれなりにちゃんとBasic認証をかける

ポッドキャストサイトを構築するPodbard

ポッドキャストサイト構築は何でもよいのですが、今回は個人の趣味活動もかねてOSSでPodbardと言うソフトウェアを開発して公開し、これを利用しています。

https://github.com/Songmu/podbard

GitHub上でコンテンツを管理し、GitHub Actions上でpodbardを動かしてサイトをビルドし、それをCloudflare PagesとR2にdeployするワークフローを組んでいます。詳細は省きますが、これもテンプレートリポジトリを公開しているため、興味があれば参考にしてみて下さい。

プライベートポッドキャスト用のテンプレートリポジトリは、本エントリで記述したプライベート化するための手法が一通り組み込まれており、すぐにポッドキャストを始められます。興味がある企業があれば是非利用してみて下さい。

まとめ

実際にポッドッキャスト化された社内ラジオを私が使っているポッドキャストクライアントのOvercastでは以下のように見られます。

OvercastはBasic認証がかかったポッドキャストに鍵マークを付けてくれる点が他のクライアントにあまりない機能で、そういった細やかさ含めて気に入っています。

ちなみに、AndroidにはどうもBasic認証に対応した有力なポッドキャストクライアントを見つけられていないので、情報をお持ちの方は教えて下さると嬉しいです。

また、Androidでメジャーなポッドキャストクライアントである、Pocket CastsはGitHub上でソースコードが公開されているため、こちらにBasic認証対応のpull requestを送っても良いかも知れないと考えています。ヘンリーはServer-side Kotlinの会社でもあるので。

株式会社ヘンリー エンジニアブログの今年1年の記事を振り返る

この記事は株式会社ヘンリー - Qiita Advent Calendar 2024 - Qiita の1日目の記事です。

ヘンリーで SRE をやっています id:nabeop です。会社では SRE の他に有志で集まっている技術広報ギルドに参加していて、社内と社外の両方の面で技術広報的なこともやっています。

技術広報ギルド内での活動の一環で毎週開催されている技術勉強会 (ギベン) [^giben]の月初の会では前月分のエンジニアのアウトプットを眺める会をやっています。その中の1コンテンツとして、この株式会社ヘンリー エンジニアブログの Google Analytics のデータを眺めて、前月はどのような記事が公開されて、どのような記事が読まれたのかということを振り返っています。

続きを読む

Gradleプロジェクトを分割するときに僕が考えたこと

株式会社ヘンリーでSREなどをやっている戸田(id:eller)です。

私は先日より複数のブログやプレゼンで「Gradleプロジェクトを分割したいけどできてない」と言っております。 Gradleプロジェクト分割にはコードの置き場所が明確になってプログラマの心理的な負担を下げ、Gradleプロジェクトという単位で依存を管理することで不正な依存が入る余地を減らす利点があります。またKotlinコンパイルひいてはGradleビルドの高速化にも繋がるため、プロダクト品質や開発体験を向上するにあたって重要な施策だと考えています。

そして……とうとうやりましたよ!プロジェクト分割を!!!🎉 ということで今回はGradleプロジェクトを分割する際に配慮したことをまとめておきます。

そもそもGradleプロジェクト分割は何が難しいのか

Gradleに限らずMavenでもAntでも良いのですが、プロジェクトをあとから分割することは至難の業です。それは多くの場合、クラスやパッケージの依存関係が複雑かつ循環していることが多いためです。

Javaのウェブアプリケーションは古くから3層アーキテクチャなどの構造が知られており、構造化が進んでいるはずなのですが、現実は厳しいものです。個人的にも ArchUnit を使ったりGradleのプロジェクト分割を使ったりして工夫してきましたが、新規プロジェクトはともかく既存プロジェクトは泥団子状態のことが多いです。

逆に言えば依存関係が整理されていれば、IDEやコンパイラを活用することでプロジェクト分割をスムーズに行えます。コイツが最初の山であり、ラスボスなのです。

import文ベースで粗く依存関係を見るところから着手

依存関係を見るのは、先のスライドで紹介した jdeps による解析が簡単です。これで循環を見つけられればしめたものです。ひとつひとつ見ていき、これをほどいていきましょう。

ほどきかたは本当にケース・バイ・ケースなので、勝ちパターンのようなものはありません。気合と根性!です。ですがリファクタリングの一種ではあるため、広く知られているリファクタリング手法が適用可能です。参考書籍として自分からは、だいぶ古いですが「レガシーソフトウェア改善ガイド」をおすすめしておきます。

とはいえケース・バイ・ケースと言われてもイメージが湧かないかと思いますので、弊社の事例から単純なものを紹介します。

クラスを別パッケージに移動した事例

たとえば gRPC API で使うデータ型のための extension が見た目の循環を生んでいることがありました。次の依存関係はパッケージのレベルは循環していますが、クラスのレベルでは循環していません:

graph LR
  subgraph service
    FooService --> BarService
    BarExtension
  end
  subgraph model
    FooModel --> BarModel
  end
  FooService --> FooModel
  BarService --> BarModel
  FooModel --> BarExtension

この場合は、 extension の置き場を調整して循環を断ち切れます:

graph LR
  subgraph service
    FooService --> BarService
  end
  subgraph model
    FooModel --> BarModel
    BarExtension
  end
  FooService --> FooModel
  BarService --> BarModel
  FooModel --> BarExtension

プロジェクトもきちんと切り分けられそうです:

graph LR
  subgraph foo
    subgraph foo.service
      FooService
    end
    subgraph foo.model
      FooModel
    end
  end
  subgraph bar
    subgraph bar.service
      BarService
    end
    subgraph bar.model
      BarModel
      BarExtension
    end
  end
  FooService --> BarService
  FooModel --> BarModel
  FooService --> FooModel
  BarService --> BarModel
  FooModel --> BarExtension

もちろん移動することに不都合がないかどうかは検討が必要ですが、多くの場合は問題になりにくいのではないかと思われます。またこの過程で internal 修飾子を削除した箇所がそこそこあり、その影響があるかどうかも確認していました。

ドメイン外の概念を見つけてプロジェクトから追い出す

もうひとつ弊社の事例を紹介します。今回は一枚岩のプロジェクトからひとつのドメインに属する概念を取り出すことを目的としていました。理想的には次のようになってほしかったわけです:

graph LR
  subgraph 元一枚岩
    FooService --> FooModel
  end
  subgraph 新プロジェクト
    BarService --> BarModel
  end
  FooService --> BarService
  FooModel --> BarModel

ところが実際には、新プロジェクトのクラスが依存する「新プロジェクトには属するべきではないクラス」が存在します。 これを新プロジェクト内に保持してしまうと、新プロジェクトがまた新たな一枚岩になってしまうリスクがあると感じました:

graph LR
  classDef hoge fill:orange;
  subgraph 元一枚岩
    FooService --> FooModel
  end
  subgraph 新プロジェクト
    BarService --> BarModel --> HogeEntity:::hoge --> HogeValueObject:::hoge
  end
  FooService --> BarService
  FooModel --> BarModel
  FooModel --> HogeEntity

よってこの「あるべきではないクラス」を切り出すための別のGradleプロジェクトを作成し、そこに該当するクラスを追い出しました:

graph LR
  classDef hoge fill:orange;
  subgraph 元一枚岩
    FooService --> FooModel
  end
  subgraph 新プロジェクト
    BarService --> BarModel
  end
  subgraph shared
    HogeEntity:::hoge --> HogeValueObject:::hoge
  end
  BarModel --> HogeEntity
  FooService --> BarService
  FooModel --> BarModel
  FooModel --> HogeEntity

この新しく作成した shared プロジェクトには、今回対象としなかったドメインに属するクラスが入ってきます。プロジェクト間で共有されるためのクラスやユーティリティであれば問題ないのですが、意図せず入ってきてしまったクラスもいくつか見つかりました。今後のリファクタリングで正しい居場所に動かす必要がありそうです。

Gradleプロジェクト分割による効果

当施策の定量的効果として、Gradleビルド高速化効果がどの程度あったのかを見てみました。1週間ほど観察しましたが、残念ながらGitHub Actionsにおけるビルド高速化効果は確認できませんでした。 理屈の上では、sharedプロジェクトないし今回切り出したプロジェクトに変更が入らなければ、それらに属するコードのコンパイルとテストを実施しなくて済むはずなんですけどね。

遠目に見ると20~30秒くらい早くなったかな?という気はしますが明らかな改善ではないですし、試行回数が少ないこともあるためしばらく様子を見たほうが良さそうです。

一方で定性的効果としては、プロジェクト分割による見通しの良さが評価されています。以前から package を使って整理を試みていましたが、package では依存関係の制約が緩く、意図しない依存が混ざることがありました。 プロジェクトを分割することで、こうした依存が明確に洗い出され、今後も意図しない依存の混入が起こりにくくなる効果が期待されています。

まとめ

以上で今回実施したGradleプロジェクト分割の概要を説明しました。依存の整理さえできれば問題ないこと、依存のほどきかたはケース・バイ・ケースだがリファクタリング手法が適用できること、プロジェクト分割によって新たなリファクタリング課題が見つかることについて触れました。

定量的効果であるビルド高速化効果はまだ確認できていませんが、定性的効果としての見通しの良さは package による整理よりも高い成果が得られたと考えています。 製品開発のスケールしやすさを確保するためにも、今後も分割を進めていこうと考えています。

ヘンリーでは各種エンジニア職を積極的に採用しています。医療ドメインに興味がある方も、GradleやSREに興味がある方も、ぜひカジュアル面談でお話させていただければと思います。よろしくお願いいたします!

jobs.henry-app.jp

Our DPE Journey Halved Pre-Merge Build Time

※ 英文記事です。同内容を日本語で掘り下げた記事を弊社有志で発行した同人誌に掲載していますので、よろしければご参照ください。

This report will share our Developer Productivity Engineering (DPE) Journey at Henry, Inc. Our server-side Kotlin project used to take around 20 mins to complete a pre-merge build. It discouraged engineers from adding more automated tests. We implemented several solutions including Develocity, and now pre-merge builds are completed within 8 mins.

About us

Henry, Inc. is developing products with the mission of "keep solving social issues to make a brighter world". As a first step, we are currently developing "Henry" a cloud-based electronic medical record and receipt system, which is a core system for small to medium-sized hospitals.

Our engineering team has ~20 engineers developing server-side Kotlin. We are in the phase to expand team size and quality of services, and it motivates us to improve developer productivity and shorten Lead Time to Change while adding more automated test cases.

DPE barriers in our project

Our Gradle project is a monolith which contains ~10 subprojects. We applied remote build cache with the help of tehlers/gradle-gcs-build-cache, and GitHub Actions cache with the help of setup-gradle action. Some of our engineers understood how to improve build performance with Gradle, so we believe that we did our best to improve DPE, but we still had many barriers:

  1. Kotlin compilation took much time. In our case, the K2 compiler does not shorten compilation time. Usually compilation took 3 to 5 mins to compile.
  2. The unit tests written in Kotest took 13 to 15 mins.

Deployment workflow has one more barrier, database migration processes (ridgepole and Algolia) which take 10~ mins. So our Lead Time to Change usually takes more than 35 mins.

The cause of our slow tests

By historical reasons, our test cases tightly depend on datastore (Postgres), and do not sufficiently account for conflicts between them. It means that one test case could fail due to changes made by other test cases, so we are forced to run test cases in serial. We could not run tests with maxParallelForks option, even though we used a large runner that had 8 cores and 16 GiB memory to launch on-memory Postgres instance for performance.

Why our compilation takes time

Our Gradle project is monolith, but it is not well separated yet. Last year we merged two Git repositories into one, and reorganization is still in progress. So each subproject is still huge and cannot use build caches efficiently. Here is a graph that explains dependencies among subprojects roughly:

graph BT
    master-data --> shared
    general-api --> master-data
    receipt-api --> master-data
    batches --> general-api

Considered solutions

Testcontainers

It is too costly to rewrite existing test cases to keep isolated from other test cases, so we tried to use process-local database to solve the problem. Testcontainers is a suitable solution for us, which lets us launch Postgres database for each test fork. It resolves conflicts between test forks by using an isolated Postgres database for each fork. Testcontainers also makes it possible to run test remotely; not only on GitHub Actions runner but also on Test Distribution Agents hosted by Develocity.

Parallelize data migrations

We run data migrations in parallel, with the help of build matrix. This is easy to maintain, and visualizes the progress of migration. However, it is costly and slow. In total, our workflow took 150 mins/build (cumulative CPU time) and a quarter of it comes from data migration.

We unify all data migrations into one Gradle task, and run it on one Runner. It enables us to run multiple data migrations in one process with the help of coroutines, so we could run migration in shorter time with less computing resource.

Subproject separation

To run Gradle builds in parallel, it is really effective to separate project into multiple subprojects. While we wanted to split the monolithic Gradle project into smaller subprojects to enhance build parallelism, our codebase is still in the process of reorganization. Thus, we had to defer this effort for now.

…and Develocity!

We expected that Develocity can shorten test execution time by Test Distribution Agents. After our verification, we found that it surely halved test execution time and whole pre-merge. You can find that builds with Develocity Test Distribution Agent (No.40 and later) finished in shorter time:

Figure 1: Develocity significantly reduced pre-merge build times by halving the test execution time.

In this POC we have only one Test Distribution Agent host, so sometimes Gradle build cannot run tests remotely due to conflict among build processes. We hope that auto-scaling feature will stabilize the performance. According to the Predictive Test Selection (PTS) simulator, PTS can skip about 20% of test cases, then it also contributes shortening pre-merge build time.

Lessons learned, and our next challenge

In this challenge, we have confirmed that we halved our pre-merge build time with help from Testcontainers and Test Distribution Agents of Develocity. This is really huge contribution to keep changes on our service frequent. It still has several areas for improvement, such as auto-scaling feature of Test Distribution Agents, but is already one of the important parts of our development workflow.

Legacy Builds (serial test exec) New Builds (parallel test exec) New Builds w/ Test Distribution Agents
whole pre-merge workflow 20 mins 15 mins 8 mins
compile tasks 3~5 mins 3~5 mins 3~5 mins
test tasks 13~15 mins 8~10 mins 7~8 mins

However, post-merge build including data migration still takes time. We'll keep working on subproject separation for better parallelism, and reduce time to run data migration.