未踏ジュニア終わったから超大規模リファクタリングしてみた
こんにちは。そうまめです。
今日は私が現在開発しているアプリ、TutoriaLLMというアプリを大規模にリファクタリングした話をしようと思います。だいぶ慣れてきたとはいえ、まだまだ界隈の方から見ると素人なので一般的に良くないとされるコードの書き方をしているかもしれません。あったらそっと教えてください。連絡先はこちら
TutoriaLLMとは?
Scratchのようなブロックプログラミングのチュートリアルを簡単に作成し、AIを利用して提供することができるセルフホスト型のソフトウェアです。
TutoriaLLMは、わかりやすく言ってしまえば、ScratchやMakecodeで起きた不満や、思いついたアイデアなどをたくさん盛り込んだ、先生と子供を対象としたブロックプログラミングの学習ソフトウェアです。
アプリ全体を監視しながら、先生のアシスタントとして子供にブロックプログラミングを教えるAI機能や、作成したコードをサーバーサイドで実行するVMシステムなどの技術を使い、教える人にかかるコストを下げ、プログラミング教育へのアクセスをしやすくすることを目指しています。
アプリは今年の5月ごろから、クリエータ支援プログラム「未踏ジュニア」からの支援を経て開発しており、11月4日に成果報告会を行いました。また、その後17日に開催されたアプリ甲子園では、なんと優勝することができました。びっくりです。このアプリはプログラミング歴2年の私が初めてまともに開発するアプリケーションだったので、不慣れな中コードを書いている私を温かい目で見守り、たくさんのことを教えていただいた未踏ジュニアのみなさんには感謝しきれません。
リファクタリングする理由
しかし、成果報告会を終え、アプリ甲子園で優勝し、満足げになった私ですが...短い時間でなんとか完成させた自分のアプリのコードを触っていると、「...なんか、コード汚くね?」と思うようになってしまったわけです。技術力が上がったということなのでしょうか。アプリはお世辞にも安定しているとは言えず、デモサイトのSentryには毎日のようにエラー通知がきます。技術デモとしてはかなり高く評価されているこのアプリも、実際に世の中に実装するにはまだまだ不十分で、使い物にならないのです。
このアプリは先生や子供が使うことを想定しています。今までは高校生が作ったアプリとしてみんな多めに見てくれたのかもしれませんが、実際にユーザーが使ってエラーが出てしまうと大問題ですよね。
また、このアプリはオープンソースとしており、今はまだあまり参加してくれている方はいないとはいえ、今後貢献してくれる人が現れるかもしれません。そのために、開発者が気持ちよくコードを書ける環境を整える必要があると思いました...というか、友人に指摘されました。
5月にコードを書き始めた頃は、全てが手探りで何もわからなかったけど、今ならなんとかできる気がする!ということで、リファクタリングをすることにしました。
リファクタリング開始!
リファクタリングにあたって、ネットの情報だけから作るのではなく、度々手伝ってもらっている助っ人Yutaさんにリポジトリの設定とか、流行りのフレームワークとかについて、さまざまな助言をいただきました。やっぱり実際にプロダクション環境でアプリを使えるようにするとなると、実際にやったことがある人から話を聞くのが一番良いのかもしれないですね。
DBの再設計
まず初めに、データベースをすべて作り直すということから始めました。
従来はPostgreSQLとRedisを組み合わせたものを使っていたのですが、これをPostgreSQLに統合することにしました。
元々TutoriaLLMでは、LLMの処理や、複数ユーザーによる同一セッションのアクセス、サーバーサイドでのコード実行を実現するために、ユーザーの作成したプログラムをリアルタイムにサーバーのストレージと同期していました。これを実現するために、Redisを使っていました。当時はあんまりPostgreSQLについての理解がなく、また、試験段階ということもあり、とにかく動くバージョンを早く作ることを優先していたので、シンプルに使いこなすことができ、すぐに使えるという理由で選びました。また、セッションのデータはJSONで記述されており、これを毎回Redisに上書きするということをしていたので、インメモリによる高速な処理を謳っているというのも理由としてありました。
しかし、月日が経ち、実際に人に使ってもらう段階にあたって、「とにかく動く」ものより、「安定して動く」ことが求められるようになりました。Redisはインメモリなので、特に何もせずにサーバーを停止すると、全部データが吹き飛びます。実際に子供が使っている最中にサーバーが一瞬でも落ちたら子供が作っている途中だったデータは全て吹き飛び、大惨事です。また、データの更新方法も差分ベースにしたことで、一度に読み書きする量も減りました。これだったら...PostgreSQLで十分ですよね。
というわけで、とにかく動くデモを作る段階から、実際に人に使ってもらう段階に移るにあたって、DBを設計し直すことにしました。
選定した技術
設計し直したと言っても、セッション管理の部分をPostgreSQLに統合しただけなので、結構簡単にできました。ただ、後方互換などをサポートするのは厳しかったのでやめました。(実稼働でこれが起きるととても大変ですよね)
PostgreSQLのサーバーには、AI関連の機能でVectorデータ(普通のPGにはなく、拡張する必要がある)を保存する必要があったので、初めからそれがサポートされているDockerイメージを使いました。
これらの操作にあたっては、Drizzle ORMを使いました。Vectorのサポートもしていて、Javascript/Typescriptで書いている人はこれが一番良いと思います。
さようなら、Express
TutoriaLLMでは、Expressでバックエンド、Viteでフロントエンド、というスタックだったので、これらを早く作るために、Vite-expressというそのまんまの名前のものを使っていました。
フレームワークというより、フルスタックを簡単にデプロイするためのラッパーとでもいえば良いのでしょうか(間違っていたらすいません)。アプリは静的ファイルで提供しているので、こういうものを使えばViteでビルド→それをExpressで読み込む→フロントエンド/バックエンドの提供...までの一連の流れを簡単にできたわけです。開発した当初はこれめっちゃ便利じゃん!と思っていました。確かに便利です。しかし、こういうものを使ってしまうと、フロントエンドとバックエンドを切り離すことができなくなってしまいます。
TutoriaLLMはDockerイメージとして提供していますが、これだとスケーラビリティのかけらもありません。先ほどと同じように、動くデモを作るなら良かったのですが、使ってもらうとなると...ちょっと心配なわけです。
モノレポ作戦
ということで、Yutaさんの助けを借りながら、アプリケーションをモノレポにするところから始めることにしました。pnpmを利用していたので、pnpmワークスペースを利用してフロントエンドとバックエンドを切り離します。実質ViteとExpressなので、ディレクトリを移動させればすぐにできる...はず...
型定義の問題
しかし、現実はそう甘くありませんでした。5ヶ月前のわたしは、フロントエンドとバックエンドでTypescriptの型定義を共有していたのです。
フロントエンドとバックエンドの間を通信させる際は、何もしない場合、型定義をつけることはできません。型定義をつける方法としてはいろいろあるのですが...面倒臭くて、Any使おうとしていました(全世界のTypescripterを敵に回しました)
型定義専用のモジュールをpnpmワークスペースで作って、そこに管理する...とか、考えるだけで面倒臭いし、二重定義なんてしたら多分管理面倒くさくなってまたAny地獄へ逆戻りしてしまいそうです
Hono + RPC
そんな面倒くさがりの個人開発者にアツいフレームワークがありました。Honoです。
Honoだったら、バックエンドでzodなどを使って定義した型定義をフロントエンドに持ってくることができます。
すごいですよね。一体どういう技術が使われているのか、私は見当がつきません(勉強しなさい)が、これを使えば、そのままフロントエンドに型を持っていくことができます。しかも、Zod+OpenAPIの組み合わせをもとにこれらを生成することもできます。APIの仕様を定義して、それに合わせたレスポンスを記述するだけで、バックエンド側も完全に型が効いた状態で開発を行うことができるわけです。
そんな感じでどう考えても今の状況にはHonoの方がマッチしているので、Expressと決別することにしました。Expressってサーバーサイドを作るのにめちゃめちゃ使われているフレームワークではあるし、大規模なアプリケーションで使われているケースが多く、安定性も高いと思うのですが、少なくとも自分のようなケースだとこっちの方が良いです。
元々TutoriaLLMでもHonoはユーザーのコードを実行する時に使っていました。
TutoriaLLMのコード実行機能では、Minecraftのようなゲームとかと接続してコードを実行することができるのですが、この時にHonoにあるWebsocketのヘルパーを利用することで、軽量なWebsocketの接続環境を用意していました。結構この手のwebsocketサーバーってマイクラみたいな特殊なクライアントと接続できないケースがあるのですが、Honoはとてもシンプルに書かれているので、こう言ったトラブルも少ない印象があります。
という感じで今回のリファクタリングにあたっては、このHonoをアプリ全体に導入することになったのですが、使い勝手が良かったので、今後はHono信者になってHonoのことを推していこうと思います。
時間と成果
という感じでいかがだったでしょうか。
初めて作ったWebアプリを、初めてリファクタリングした割には、かなり良い改善が行えたのではないかなと思います。
ここまでの変更にかかった時間に関しては、約3週間程度でした。技術選定、DBの再設計、リポジトリの大移動、モノレポ化、フレームワークの変更、型共有の設定などをすべて行って、この時間だったら上出来だと思います。
この変更によって、TutoriaLLMの安定性はとても良くなったと思います。今まで仮で動く状態だったものが結構きちんと動くようになってくれたので、とても満足しています。
とはいえこのリファクタリングを経ても動作が不安定な箇所はあり、もう少し直さないといけないところはあるのですが、このタイミングでしっかりとした開発の基盤をここで整え、アップデートを重ねつつ、いずれはプログラミングの教育を行っている教育機関に対してこのアプリを実稼働にデプロイする(使ってもらう)ことなどを考えています。
未踏ジュニア期間では、とにかく興味を持ってもらう人を集めるために、とりあえず動くデモを作成することに専念していました。もちろんそうやって、安定性を無視して開発を行うということもありだとは思うのですが、いつかはこうやって書き換えないといけない時が来てしまいます。安定して動くことも目指していかないといけません。これからも頑張ろうと思います。
Discussion