Null Safetyへの移行対応からDart 3の世界へ

はじめに

こんにちは。お会計チームの yamakazu (@yamarkz) です。

Dartで導入準備が進められていたNull Safetyと呼ばれる言語機能への対応が、先日ようやくStailerでも完了しました。

プログラミング言語としてNull Safetyへの移行 (機能導入) は頻繁に起こるものではなく、珍しい話題です。なので、せっかく対応をしてきたのなら、その記録として何を考え、何を行いNull Safetyに対応していったのか、Stailerを題材に紹介しようと思います。

ニッチな話題ですが、読み物として楽しんでいただければ。

目次

StailerとDart

10Xが開発運営するStailerというプロダクトは、主開発言語にDartを採用しています。

この辺りの背景話はこれまでもだいぶ発信してきているので、他の記事に譲ろうと思います。ちなみに今回の主題でもある「Null Safetyへの移行」は、サーバーアプリケーション方面での話です。

type.jp

Null Safetyを備えた言語への進化

medium.com

Dartは5月にリリースされたDart 3から、晴れて完全にNull Safetyの機能を備えた言語になりました。 🎉🎉🎉

Dart 3からはNullの存在が厳格に扱われるようになります。

Null Safetyへの対応で嬉しいことは挙げれば色々と浮かびますが、自身で1つ挙げるとするなら コードの安全性向上 が何より嬉しいことかなと。

ここでの安全性を平たく言うと「予期せぬバグを生む確率が減る」「Nullであることがコードの表現上明示的にすることで、想定していない値の扱いにより発生するランタイムエラーを減らせる」ことを指します。

言語機能を厚く (熱く) 語りたいところですが、公式が丁寧なドキュメントをいくつも用意してくれているので、詳細はそちらを参照してください。

dart.dev

StailerにおけるNull Safetyへの進み方

自分たちのNull Safetyへの進み方は、次のステップを踏んで進めました。

  1. 依存する公開パッケージの対応版へのアップデート
  2. 内製パッケージへの適用
  3. アプリケーションへの適用
  4. テストへの適用
  5. エントリーポイントへの適用とシステムテストの実施
  6. プロダクション環境へのリリース

まず最初に取り組み始めたのが、1の依存している公開パッケージのアップデートです。

当たり前ですがNull Safetyはプログラム言語そのものに加わる機能であるため、言語を使うコミュニティ全体で進めていく必要があります。

2年前から公式が移行アナウンスと移行の手引きを公開し、Dartの開発コミュニティでは続々とパッケージのNull Safety対応が進められてきました。

自分たちで使っているパッケージもその対応の進行に合わせて随時アップデートを進めていき、パッケージのアップデート不備が移行の足枷にならないように注意していました。

次に取り組んだのが内製パッケージでの適用です。

自分たちのアプリケーション開発では内製しているパッケージがいくつか存在しています。例えばログ出力を行うLoggerや、パートナー連携で使用する独自のAPI Clientなど。

これらは所謂アプリケーション全体に関わるインフラ的な立ち位置のパッケージ群で、アプリケーションコードをNull Safetyに移行する前段階で先に対応を進めて行かなければならず、昨年の秋ごろから少しづつ移行を進めていきました。

公式が示す以下の図にある通り、Null Safetyへの以降は依存先のパッケージから対応を進めて行くためです。

(公式が示す移行対応順序の推奨方針)

Null Safetyへの移行とは、具体的に何をやるのか?というと。「コードコメントを外し、コードをあるべき表現に調整する」ことを繰り返す作業になります。

移行対応を遅らせてもバージョンの整合性担保を行えるように、コードコメントを記述することで、Null Safetyの有効性をコンパイルタイムで制御できるような補助がDartにはありました。

before

// @dart=2.9

void main() {
  print('Hello World');
}

after

void main() {
  print('Hello World');
}

単純に見ると上記のようなコードにある // @dart=2.9 の記述を取り除いて、下記のように変えるだけです。

1行のコードコメントを消すだけなら簡単な話なのですが、実際は消すことによって静的解析時点からNull Safetyを前提とした解析が有効になり、表現記述が誤っている箇所を指摘されたりすることがほとんどで、それらを実装の文脈を読み解きながら適切な記述に変えていく作業を繰り返します。

適切な記述というのは、型変換箇所をNullableにすること(as String?)や、Nullableな値を許容する引数型に変えた後、Nullだった場合にガード節で早期リターン (if (arg == null) return) を書き換えていくことなどで、既存の動作を担保する記述に変えていきます。

パッケージ群のNull Safety対応が完了すると、いよいよアプリケーション全体への適用が始まります。

3と4のステップを細かくイテレーション刻みながら、徐々にNull Safetyの適応を行なっていきます。

この間、適応過程で静的解析的にエラーとみなされる記述のエラーなどに頻繁に遭遇しますが、都度コードの文脈を読み解いて適切な記述に書き換える作業を重ねます。

(1月にはドメインごとのサポート対応を進めていた)

機械的に移行すること(スクリプトを実行して完了など)ができず、移行作業は地味な対応の繰り返しが続きます。流石にこれだけを1日やるのは生産性がないため、通常の開発業務と並行して1日1善の気持ちで毎日少しずつ移行を進めていました。

エントリーポイントを残すコード全てで対応が完了すると、いよいよ最後の反映です。

静的解析、そしてコンパイルタイムの時点では問題ないと判断されたとて、ランタイムで起きうる予期せぬエラーは避けなければいけません。

API, Batch, Worker それぞれで時間を空けて開発環境で可能な限りのシステムテストと実行確認を実施し、動作の安全性が確認を踏まえてリリースを行いました。

以上が、自分たちのNull Safetyへの移行のアプローチです。


ここまで読んでいただければ分かる通り、DartのNull Safetyへの移行対応には残念ながら魔法のような取り組みはありませんでした。

何度か立ち止まって自動化なども検討してみましたが (公式には移行ツールの提示もあったものの、安全性と品質保証の観点から使わない(使えない)と判断しました) 、前提にある要求水準や、機械的には修正しきれない型変換とNull checkコードの修正は人の手を介さなければ越えられない壁だったからです。

移行作業を愚直に繰り返すことで突破しました。

次に、この過程の全てを振り返って遭遇した苦労と良かった点をいくつか紹介します。

Null Safetyへの移行を進める中での苦労

苦労1. パッケージの対応が進んでいない

(migrate to null safety #5)

公開パッケージにはNull Safetyへの移行が進んでいないものもありました。

そういった場合には、自分たちでNull Safetyの移行を進めるようなPRを作成して、移行を進めてもらうようにパッケージオーナーにお願いしていく動きで解決していきました。 また、一部のパッケージでは残念ながらオーナーシップを失っているものもあったため、そのケースに該当したものは別パッケージへの乗り換えも行っています。

swdyhさんがsendgrid_mailerのNull Safety移行を進めるPRを作っていたのが懐かしいです。(よく見たら2年前…)

苦労2. Nullの扱いが曖昧なコードがクリティカルな領域に多く存在する

Nullの扱いが曖昧なコードがクリティカルな領域に多くありました。

具体的にはマスターデータや決済といった、サービスの根幹となるような領域です。

"結局システムテストするから大丈夫" という見方もできなくはないのですが、そもそものテストが大変な領域であるのと、サービスの根幹と呼べるくらい多くのユースケースから参照される処理、多様で多量なデータを扱う処理であることを考えての移行対応は大変でした。

進め方の工夫として、1度に対応を進める範囲をドメイン単位で区切って、ドメインの有識者にコードレビューをしてもらいながら移行対応を進めています。

クリティカルな領域こそ、QCDのQualityにこだわって丁寧な作りを目指していきたいですね。

苦労3. エンタープライズシステムとしての規模の大きさ

Stailerもローンチから3年が経ち、既存サービスをラップしたライトなアプリ提供からヘビーなエンタープライズのシステムに進化してきました。コード規模も当時の10倍以上の大きさに成長してきています。

クリティカルな領域が全体の1割程度しかないとしても、元々の規模が大きいというのはそれだけで認知負荷が大きくなりますし、対応の作業量が増えます。 正確な参考指標とはならないですが、2023/06時点ではコードボリュームとして40万行近く、1,800ファイル存在しています (!)

この規模との戦いも1つ大きな苦労でした。

だからこそ、もっと早い段階で対応しておきたかったなとも思えるのですが、事業の引力やDartコミュニティの成熟度を考慮すると、このタイミングに完了する流れで進めたことがベストだったかもしれません。

苦労4. 要求される品質を守りながらの進行

エンタープライズシステムとしての規模もそうですが、要求されるサービスレベルも非常に高いものが求められるようになりました。店舗の運営業務を支える方面のAPIは高い可用性と安定性が譲れない特性となり、それらに可能な限り影響を及ぼさないことを保証した上で、段階的に進めなければいけなかったのも難しかったところです。

品質を多少犠牲にできるのであれば、大胆に進む道もあったかもしれませんが、その選択肢が取れない中で ”いかに合理的に変更リスクと対応負荷を最小化して進めていくか” には頭を使いました。この考えに対する答えは、苦労2で触れた "対応範囲を限定化" と後に紹介する "段階的な移行対応でなんとか進めること" です。

振り返った中で良かったこと

ここまでで苦労を取り上げてきましたが、もちろん良かった点もあり、今後もなんらかの取り組みで参考にしたい点でもあるので紹介します。

$1. 進行を可視化することでモチベーションを保つ

(Slackに流しているDart Code Metrics)

まず何より良かったのは、進行を可視化していたことでした。

果てしない道のりの進捗が可視化されることで、日々の地味な移行に対する取り組みのモチベーションを一定に保ち続けられたことが、移行対応において一番の成功要因だったように思います。

これは所謂「進化的アーキテクチャ」の文脈でいう”適応度関数”のことで、ブログでも以前紹介しましたが、Slackに定期的に特性の獲得に向けた定量指標が投稿される仕組みを設けていました。

今回のNull Safetyへの移行はを教科書的に捉えると、コードの保守性 (安全性, 可読性, 堅牢性, 変更容易性) の獲得を目指した取り組みだと言えます。

$2. メンバーの助けをもらう

(カナリアリリースによる検証で影響範囲を限定的に進めていく)

移行に際して同僚からは多くの助けをもらいました。

特にテストの実施やテスト環境の整備、週末の運用検証に際しての調整や準備には多くのサポートと気遣いをもらえたことに、この場を借りて感謝を伝えたいです。

Special Thanks! swdyh, tapih, horimislime, ynonomur, mgi166 and QA Team.

APIは厚めのリグレッションテストのおかげて障害0でAPI全てを移行し切ることができました。

100近くの検証容易性の低いCron Commandの段階的な移行実施はなかなか忍耐力のいる取り組みでしたが、こちらも大きな障害もなく無事完了に至ることができました。

流石にこの規模のプロダクトになると専門性を持つメンバーとの厚い(熱い)協力なくしては、大きな成果は作れないなと、そう実感できる取り組みでした。

$3. 移行実施のタイミング

取り組みを進めるタイミングは今がベストだったと思っています。

公式のアナウンスがなされた当初から進めたい気持ちはありましたが、2年前は今よりも事業の生死に対する緊張感と新規開発の引力が強く、移行に労力を割く余剰が組織にありませんでした。また、パッケージの周辺環境も不安定でもあったため、遅らせた方が安全だと判断する根拠があったのも理由として大きいです。

技術投資は角度 (どこに力を加えて何を変えるか) も重要ですが、タイミング (時間) も重要だと思っています。それを強く実感できたのがこの取り組みでした。

プロダクトの成長に伴って開発にかかる認知的負荷が高まり続ける中、効果的な機能が盛り込まれたメジャーアップデートが目前に迫り、周辺環境の整備も進んできている。そういった推進の強い動機となる要素が揃い切るまでは "あえて" 遅らせること。いつかやるけど "今は" やらない、と。前提の状況を正確に汲み取って合理的に判断を下すことが、技術投資の意思決定には必要なのかもしれません。

コードの堅牢性と可読性を得られている手応え

苦労話と良かった点に触れてきましたが、では最終的な成果として何を得られているのか?という話を最後にすると、主に定性面で大きく3つ得られたことがありました。

  1. Dartのメジャーアップデート実施準備が完了
  2. Nullの扱いを型システムに委ねられることにより、実装判断に必要とする注意量が減った
  3. Nullの扱いが明示的なったことで、実装に潜む暗黙的な考慮に対する不安がなくなった

Null Safetyの移行対応は、Dartのメジャーアップデート実施に必要な通過点でした。結果として堅牢性と可読性を得られた手応えを感じています。

かけた労力に対して得られた成果の数自体は少ないものの、効果として得られたものは期待値を含めれば十分なものではないかなと、全てを振り返って思います。

最後に

Stailerで取り組んだDartのNull Safetyへの移行対応を取り上げて紹介してきました。

今でもよく「Dartを選んで大変なことはないですか?」とカジュアル面談などで質問をいただきます。それに対して「まぁそれなりにありますけど、なんとかできてます」といった無難な回答をすることもありましたが、今回取り上げた話はまさに、”それなりにある” 大変なことの1つでした。

大変でしたが、開発言語にDartを選んで開発する道を選んだ以上、避けて通ることができない道でしたし、何より事業が立ち上がってきた中で選んだことにより得られた成果も多くあったと思えているので、これからもDartと共に可能な限りの進化を続けていきたいなと思っています。

そしてついに、StailerでもDart 3へのバージョンアップがまもなく実施されます。

3つの大きな機能 (Record, Patterns, Class Modifiers) が加わることで、パターンマッチや可視範囲を従来よりも上手く表現できるようになり、開発の幅が広がりそうで楽しみです。

もしDartでの開発に興味を持っていただけたらぜひカジュアル面談からお話ししましょう。10Xでは現場を向いたプロダクト開発はもちろん、技術に投資して高いレバレッジをかけ続ける開発にトライできる環境があります。

[共通]カジュアル面談 / 株式会社10X

それではまた。👋

参考資料