🧠

実装負荷が大きすぎる。で、諦めてた機能をAIでリリースして得た知見

に公開

業務アプリのオフライン対応をリリースしました。電波がなくても作業を続けることができ、オンラインに復帰したら自動でサーバーと同期するものです。

実はこれ、必要だとわかっていながら、今まで諦めていた機能でした。実装負荷が大きすぎるんですよね。

製品に載せたあと、私の中にちょっと変な感覚が残りました。いつもは「ここまでは私が理解したコード」という感覚があったんだけど、今回は明らかに「私の頭では追いきれない量」が載ってる。それでもちゃんと動いてる。少し怖いけど、少し嬉しい、みたいな。

生成AIを使って開発が速くなったという話は、もう珍しくありません。ただ、今回は速くなっただけでなく、これまで届かなかった機能を製品に載せ、プロダクトをもう一段よくできました。

これは、AIを使えば知識がなくても難しい機能を出せる、という話ではありません。Developerの知識と経験がAIでブーストされたことが、一番おもしろいと感じた変化でした。

オフラインは業務現場では当たり前

倉庫とか厨房、配送先、イベント会場で使う業務アプリは、電波が安定しないのが普通です。けれど、電波がないからといって現場の作業は止まりません。

今までは電波が悪い場所では作業を待ってもらったり、簡易なローカルストレージに一時保存して、あとから手動で合わせたりしていました。ただ、操作の途中でアプリを落としたり、別の端末が同じデータを触ったりすると整合性が崩れます。

例えば「在庫は減ったのに履歴が残っていない」とか、「通信が切れたので再送したら同じ商品を2回処理した」とかですね。業務アプリでこれは本当にまずい。

そこで、ネットワークがなくても操作を続け、復帰したら自動で同期する本格的なオフライン対応が必要でした。要件だけを読むと普通なんですが、実装すると全然普通ではありません。

Service Workerでは要件に合わなかった

Webアプリのオフライン対応と聞くと、最初にService Workerが思い浮かびます。HTMLやJavaScriptなどをキャッシュして、ネットワークがなくてもアプリを開けるようにするのが得意な仕組みです。

オンラインへ復帰した時に通信を再開するBackground Sync APIもあります。ただ、これはSafariではサポートされていません。Service Workerそのものではなく、あとから同期処理を実行させるSyncManagerが使えない、ということです。

ただ、Safari対応より大きな問題がありました。Background Syncが提供するのは、オンラインへ戻ったあとにService Workerで処理を実行するきっかけです。リクエストを送り、その結果をローカルへ反映する処理は自分で実装しなければなりません。

今回必要なのは、操作をサーバーへ送るだけではありません。処理結果を端末のSQLiteへ反映し、画面表示や印刷に使うデータまで最新にする。別の端末による更新とぶつかったら、どちらを残すかも決める。必要だったのは通信の再送ではなく、業務データを合成し、反映し、競合を解決するアプリケーションとしての同期処理でした。

MySQLとSQLiteは、そのまま写せなかった

サーバー側はMySQLで、正規化されたテーブル群を使っています。一方、端末側はSQLiteです。最初はMySQLのテーブルをそのままSQLiteに写せばいいんじゃないかと思うのですが、そう簡単にはいきません。

サーバーでは、商品マスタ、ラベルテンプレート、在庫履歴、印刷設定などが別々のテーブルに分かれています。けれど端末で画面をレンダリングしたり印刷したりするたびに、何度もJOINするのは避けたい。オフラインで使うなら、必要な情報を1つにまとめておいた方が動作も予測しやすいです。

そこで、サーバーから端末へ送る時は複数のテーブルを合成して、端末からサーバーへ戻す時は更新箇所を分解する変換層をつくりました。

MySQLの複数テーブル
        ↓ 合成
SQLiteの印刷データ
        ↓ オフラインで操作
端末内の操作キュー
        ↓ オンライン復帰後に分解
MySQLへ反映

ローカルでは1枚のレコードとしてシンプルに読み書きでき、サーバー側は今までのテーブル構造を崩さずに済みます。ここだけみるときれいな設計ですよね。

問題は、合成と分解のマッピングが膨大になることでした。

MySQLの数十テーブルからSQLiteの合成レコードへ、そしてまた逆方向へ戻す。カラムの意味、デフォルト値、外部キー、トリガー、履歴の扱いなど、全部を正しく揃えなければなりません。手作業で変換コードを書くと、まぁ、どこかでミスが入ります。

通信は困るところで切れる

オフライン中の操作は、当然すぐにはサーバーへ届きません。そのため、操作を小さな単位で端末内のキューへ積み、オンラインに復帰した時に順番に処理します。

ここで大事にしたのは、「半分だけ成功した状態」を残さないことです。

1つの業務として完結すべき処理は、まとめて成功するか、まとめて失敗するかにします。サーバーへ同期する時もトランザクションで囲み、途中で通信が切れたらローカルのキューは残す。次に復帰した時、そこから再開すればいいわけです。

同じ操作を再送しても同じ結果になることも重要です。通信が切れたからもう一度送っただけなのに、在庫が2回減っては困りますよね。

さらに、端末がオフラインの間に、別の端末が同じデータを更新している可能性もあります。同期直前にアプリを落とした、サーバーの処理は終わったけどローカルへの反映前に通信が切れた、ローカルでは更新したけどサーバーは失敗した、などのパターンもあります。

言葉にすると数行なんですが、「いつ切れたか」「どこまで成功したか」「その間に別の端末が何をしたか」の組み合わせを考えはじめると、本当にきりがありません。

Developerの知識を、AIが実装量と検証量に変える

この規模のオフライン対応を、AIを使わず私がすべて実装すれば何ヶ月もかかります。不具合が出た時の調査が特に重いです。

そこで、LLMには以下のような作業を任せました。

  • スキーマの差分からマッピングコードを生成
  • 状態の組み合わせを洗い出してテストケースを生成
  • 操作ログをトレースして、不具合の原因を推定
  • 途中までしか成功しない可能性があるコードを検出

特に不具合調査はLLMが強かったです。

私が「この操作をしたあと、ここの値がおかしくなった」と伝えると、操作キューとサーバー側のトランザクションログを照らし合わせて、どの段階で整合性が崩れたかを絞り込んでくれます。今まで数時間かけていた調査でも、最初に疑う場所を数分で示してくれる。これはめちゃくちゃ助かりました。

ただし、LLMが出した答えをそのまま正解として扱ったわけではありません。

MySQLとSQLiteでなぜデータの持ち方を変えるのか、どこまでを1つのトランザクションにするのか、競合した時にどちらの値を残すのか。AIはコードやテストを大量につくれますが、どの状態が業務として正しいのかまでは勝手に決めてくれないんですよね。

今まで私の時間を使っていた実装や調査をAIに任せ、私は設計と結果の判断に時間を使う。Developerが持っている知識と経験を、AIが実装量と検証量に変換してくれた、という方が近いです。

乱暴に数字で例えるなら、知識がゼロの人がAIによっていきなり100のプロダクトをつくれるようになったのではなく、100の力を持つDeveloperが、今までなら手が回らなかった300の実装や検証へ届くようになった。生成AIのブーストって、こういうことなんじゃないかなと思います。

すべてのコードを追いきれないのに、なぜリリースできたのか

ここが今回、私自身でも一番おもしろかったところです。

これまでの開発では、品質の根拠のひとつに「実装者がコードを理解していること」がありました。もちろん今回も設計は理解していますし、重要なコードはレビューします。ただ、LLMが生成した変換ロジック、状態の組み合わせを網羅するテスト、不具合解析まで含めると、すべてをひとりの頭に入れることはできません。

それでもリリースできました。

なぜかというと、コードを1行ずつすべて追いきれなくても、AIが越えてはいけない境界は決めることができたからです。

  • 守るべき境界を私が決める
  • その境界を自社のESLint Rulesやテストにする
  • 実際の動作とログから結果を検証する
  • AIの出力が境界を越えたらCIで止める

プロンプトで「こう書いて」とお願いするだけでなく、間違ったら機械的に止まるようにする。以前、自社ESLint Rulesのすゝめ。汎用ルールではAIコーディングは制せないでも書いたのですが、プロンプトは説明書で、ESLintはガードレールです。

全部の道順を覚えていなくても、少なくとも決めたガードレールを越えていないことは機械的に確認できます。もちろん、それだけで品質が保証されるわけではないので、テストやレビュー、実機での動作確認は別に必要です。今回リリースできたのは、これらを組み合わせて判断できたからでした。

なので、これは「AIが書いたコードは読まなくていい」という話ではありません。ガードレールも検証もなければ、「私の理解を超えた」は成果ではなく、ただの危険物です。

まとめ

実装負荷が大きすぎて諦めていた、業務アプリのオフライン対応をリリースしました。MySQLとSQLiteの間を合成と分解でつなぎ、通信がどこで切れても中途半端な状態を残さない仕組みです。

今回得た知見は、AIはDeveloperの知識を不要にするのではなく、その知識が届く範囲を広げること。そして、コードを1行ずつすべて追いきれなくても、設計、ガードレール、テスト、レビュー、実機確認を組み合わせればプロダクトは制御できることです。

AIによって私の頭だけでは追いきれないものをつくれるようになったからこそ、それを手に負える状態にしておく仕組みが、これから大事になるんじゃないかなと思います。

それではまた。

Discussion