SmartHR Tech Blog

SmartHR 開発者ブログ

入社してわかったSmartHR本体の難しさ

どうも2022年9月にSmartHRに入社したエンジニアの大澤(@qwyng)と申します。SmartHRの本体を開発しています。
SmartHRというサービスは、従業員情報を集約したアプリケーションをコアとし、そのコアと連携する複数のアプリケーションを配置した構成になっています。 そのコアというのがSmartHR本体です。
SmartHR本体は歴史が長いプロダクトです。カジュアル面談でも「キャッチアップはどうされました?」、「SmartHRの開発って技術的に何が大変ですか?」といった質問をよく頂きます。 本記事はそういったSmartHRの開発の大変さを知りたい方に向けて自分が感じたことを言語化したいと思います。

2022年初頭に弊社の@sugamasaoさんがSaaS.techで発表した. 「アプリケーションが大きくてつらい・・・ってこと!?」*1 というスライドを見たことがある方もいると思います。
歴史のあるプロダクトが現在どのような状況なのか気になる方に向けて、入社半年の自分が開発して見てきたSmartHR本体の難しさを紹介したいと思います。

SmarHR本体の難しさ

BiTemporal Data Model

SmartHR本体が採用している履歴を表すデータモデルです。
SmartHR本体は人事情報を集める重要な窓口です。ユーザーは履歴を参照して組織変更、研修計画、給与計算等意思決定を行っています。

SmartHR本体のコードではActiveRecordと履歴の管理が密接に結びついており、普通のActiveRecordとは異なる振る舞いをします。

company = nil
Timecop.freeze('2023-05-01') do
  company = Company.create!(name: 'A')
end

Timecop.freeze('2023-05-5') do
  company.update!(name: 'C')
end

# この時点でレコードが2つ存在する。
pp Company.ignore_valid_datetime.all
# =>
# [#<Company:0x00000001243592e0
#   id: 1,
#   bitemporal_id: 1,
#   name: "A",
#   valid_from: 2023-05-01 00:00:00 UTC,
#   valid_to: 2023-05-05 00:00:00 UTC,
#   transaction_from: 2023-05-05 00:00:00 UTC,
#   transaction_to: 9999-12-31 00:00:00 UTC>,
#  #<Company:0x0000000124358c28
#   id: 1,
#   bitemporal_id: 1,
#   name: "C",
#   valid_from: 2023-05-05 00:00:00 UTC,
#   valid_to: 9999-12-31 00:00:00 UTC,
#   transaction_from: 2023-05-05 00:00:00 UTC,
#   transaction_to: 9999-12-31 00:00:00 UTC>]

Timecop.freeze('2023-05-10') do
  # 現時点で有効なレコードのみを参照できる。
  pp Company.find_by(name: 'A')
  # => nil

  # default_scopeとは別の独自の仕組みでスコープをかけている。
  pp Company.all
  # =>
  # [#<Company:0x0000000124388428
  # id: 1,
  # bitemporal_id: 1,
  # name: "C",
  # valid_from: 2023-05-05 00:00:00 UTC,
  # valid_to: 9999-12-31 00:00:00 UTC,
  # deleted_at: nil,
  # transaction_from: 2023-05-05 00:00:00 UTC,
  # transaction_to: 9999-12-31 00:00:00 UTC>]  
end

詳しい動作はOSSとして公開しているActiveRecord::Bitemporal GemのREADMEをご確認ください。
この様にActiveRecordの基本的なメソッドであるsaveupdate内に履歴ロジックが隠蔽されています。履歴をもっているモデルを扱うときは常にこの動作を考慮して実装する必要があります。

派生して様々な問題も出てきます。

  • 履歴と履歴が関連を持ったら?
  • 単純なtypoの修正では履歴を残したくないと言われたら?
  • 過去の履歴を今から挿入したいと言われたら?
  • データ構造の変更をしたくなったら?

履歴はSmartHRの根幹を成すデータ構造であり、挑むべき課題が多いデータ構造でもあります。

従業員と権限

履歴と双璧をなして複雑な概念です。

従業員の基本的な情報だけでも12個のモデルがあり、それぞれが別のテーブルとの関連を持っています。従業員情報を持つモデルは前述した履歴データモデルがほとんどなので複雑性は更に増加します。

権限についても同様に複雑です。「操作者がどこまで情報を見て良いのか」「操作者はこの操作をして良いのか」を判断するのですが、情報、操作、共に種類が多く組み合わせによって考慮するべき条件も膨大です。
PunditというGemを利用して権限を実装しているのですが、app/policies/配下には単純なAbc Metric計測でも50を超えるものが複数存在します。

コードの規模が大きい

rails statsの結果を貼ります。(一部省略しています)

+----------------------+--------+--------+---------+
| Name                 |  Lines |    LOC | Classes | 
+----------------------+--------+--------+---------+
| Controllers          |  32045 |  24939 |     652 |
| Models               |  59977 |  47910 |     785 | 
+----------------------+--------+--------+---------+
| Total                | 713225 | 557457 |    1709 | 
+----------------------+--------+--------+---------+

先程記載した@sugamasaoさんのスライドには2022年1月時点のrails statsが載っていますが、当時と比較して20万行以上増えています。
コードの規模が大きいということはプロダクトの関心事も広いということです。
現状のSmartHR本体の存在する機能を全て把握するだけでも難しく、どの機能がどの機能に影響するかのマインドマップを持つのはかなり大変です。

プロダクトの歴史が長い

SmartHR本体機能の最初のコミットは2015年です。

> git log --reverse --format=%cd                                                                       
Fri Feb 13 02:48:54 2015 +0000
Fri Feb 13 02:49:47 2015 +0000
Fri Feb 13 16:53:28 2015 +0900

8年間でユーザーの要求も日本の法律もどんどん変化していきました。
この変化に対応するためにその時々ごとの特例に対応した分岐が数多く存在します。勿論テストコードはありますが、そもそもの法律の変遷やユーザーの要求の変化の過程がわかっていないと理解が難しい箇所も存在します。

DBのレコード数が多い

SmartHR本体はいわゆるtoBサービスであり、ソーシャルゲームを始めとするtoCサービスのような爆発的トラフィックがあるわけではありません。しかし、DBのレコード数は膨大です。前述した履歴の仕様によりデータの更新の度にレコードが増える仕様になっています。

レコード数はコードの理解というより運用の理解という観点で複雑性を上げています。過去のレコードとの整合性を取る難易度は上がり続けますし、一度整合性が破綻してしまうと調査や修正も一筋縄では行きません。

パフォーマンス面でも複雑性を上げています。プロダクト開発ではある程度ナイーブな実装をしてでも提供速度を優先するということがあると思います。ですが、SmartHR本体のレコード数で計算量の多い処理を実装してしまうとユーザーの体験が明確に悪化してしまいます。

どうやってキャッチアップしているのか?

ドキュメントを読む

SmartHRではドキュメントの文化がしっかり根付いています。またオープンであることをカルチャーとしているので基本的にアクセスできないドキュメントは存在しません。自分は入社当時は「ここまでドキュメントがあるのか!」と圧倒されました。

例えば、Bitemporal Gemには丁寧なREADMEがありますが、社内には更に詳細な履歴に関するドキュメントが用意されています。法律の変遷、ユーザーの要求の変化、今まで発生した障害、これからSmartHRのアーキテクチャをどうして行きたいのか、 気になることは基本的にドキュメントがあります。DMを使わない文化なのでSlackで検索しても有用な情報が見つかります。

オンボーディング専用のドキュメントも存在しているので、「そもそもどうやってドキュメントを参照していけばいいのかわからない」ということもありませんでした。もちろんチームのメンバーに質問しても優しく教えてくれます。

チームをまたいだ活動への参加

SmartHR本体は現在7チームで別れて開発しています。ですが、パフォーマンス向上活動やライブラリのアップデート活動等チームの枠を超える開発も行っています。 参加は基本的に自由です。手を上げれば参加できる環境があります。
これらの活動で行うSQLのクエリチューニングやアップデート対応を通して様々なコードを読むことができます。実際に目的があってコードを読むのでキャッチアップしやすいです。

おわりに

筆者自身SmartHR本体を理解しているとは到底言えないのですが、日々助けられてなんとか開発しております。
SmartHRの本体機能は広大で複雑ですが、キャッチアップできる土壌もしっかり整っています。
もしこのSmartHR本体という巨大な課題の解決にご興味ある方がいましたらカジュアル面談お待ちしております!

SmartHR エンジニア採用

ウェブアプリケーションエンジニア(バックエンド) https://open.talentio.com/r/1/c/smarthr/pages/45049

ウェブアプリケーションエンジニア(フロントエンド) https://open.talentio.com/r/1/c/smarthr/pages/45050