CTOのmeijinです。
今日は「ギリギリ公開できる技術的負債TOP3」と題しまして、昨年8月に正式リリースした「オンライン家庭教師マナリンク」というサービスの技術的負債をギリギリな感じでお話(釈明)していこうと思います。
事の発端としては以下のツイートに思わぬ投票が集まってしまったことです。
弊社は現在採用活動中なのでWantedlyにストーリーを投稿するわけですが、記事ネタに困っています。何がいいかなとツイッターでアンケートを取った結果、37票投票いただいた上、技術的負債が第1位になったという流れです(困ったけど自業自得ですねw)。
そこで、言えそうなもの、言えなさそうなものを両方手元で社内Notionに書き出しました。そこからギリギリ言えるかな〜な負債のTOP3と、それだけじゃ私の精神がやられるのでなんか負債解決エピソードでも書こうかなと思います!
注意
マナリンクは昨年(2020年)8月にリリースされたばかりのサービスですので、一般的にみなさんが連想されるような技術的負債(10年前に開発された管理画面が未だにPHP4系だ・・・など)とかではありません。ご了承ください。
本記事における「技術的負債」について
本記事中での「技術的負債」については、ざっくり以下のように定義して話を進めます。
- 本当は別の方法が良いことは分かっているが、現状何らかの理由でその方法が取れていないもの
- 良い=セキュリティに優れる、データ設計として望ましい、仕様変更時にバグが起きにくいなど
- 何らかの理由=リソースの問題、コストの問題、事業の安定性の問題など
- かつ、その負債を受け入れることでサービスの提供速度、検証速度が上がっているもの
- 開発した機能が速攻クローズとかあるので、早すぎる最適化は検証速度を無駄にしてしまう
第3位 科目の扱い方がややこしい
影響度:中 修正難易度:高
どんな負債?
第3位は、「科目の扱い方」についてです。
マナリンクでは科目に「科目ID」というものを振っています。
国語は1,数学は2、物理は3・・・といった感じです。
オンライン家庭教師の先生方が作成する「指導コース」や、ユーザーが先生を探す「科目別先生一覧画面」、メディアとして公開されている「科目別記事」などに科目IDが紐付いているイメージをしてください。
ここで問題になるのは、それぞれが紐づく科目の粒度が違うのに、同じIDを使いまわしていることです。
「指導コース」は具体的な指導内容を紐付けるべきなので、「数学1A」や「現代文」といった粒度で科目を紐付けることができます。もちろん、数学1Aには10、現代文には15、といった感じでIDが紐付いています。
しかし、現代文は国語の集合の中の一つですから、「国語を指導できるオンライン家庭教師一覧画面」では、現代文の指導コースを持っている先生も表示する、といった要件があったりします。このように、科目IDは1次元でひたすら並べて管理するのではなく、科目をひとまとめにするグループのような概念があります。
これらの要件を満たすために、ソースコードで「指導コースに紐付け可能な科目ID」と「ユーザーが先生を検索するときの科目ID」をマッピングする処理を書いているわけですが、まあややこしいです。
統一した科目IDにすることで、そのときそのときの機能追加のスピードは高いわけですが、機能追加時にこの科目はどの機能で使われている科目でどうマッピングしているんだ?というのを都度考えないといけなくなってしまいました。
理想は?
科目IDを統一しないことが理想だと思います。具体的には、「teaching-course-subjects」や「teacher-search-subjects」といった、利用場面ごとの科目マスタテーブル、およびそれらのマッピングを示す関連用のテーブルを作ります。初期実装は面倒になりますが、こうしたほうが仕様が見えやすいと思いますし、それぞれ変更した時にお互いに悪影響する可能性が低いです。
また、科目IDを数値で管理せず、数学なら「math」、数学1Aなら「math-1a」といった英字ベースのIDを使うほうが良かったかなと思います。連番の数値で管理するメリットが特に無い上、人間の目で見ても直感的じゃないです。
第2位 管理画面の実装とユーザー向け機能の実装が同居
影響度:中 修正難易度:中
どんな負債?
マナリンクのバックエンドはLaravelを使っているのですが、そのAPIサーバー内に、管理画面向けの実装とユーザー向けの実装が同居しています。
もちろん、管理画面向けのコードには○○○ByAdminといった命名をしたクラスを用いるなど工夫はしていますが、肝心のEloquent Modelだったり、Entityといったドメインオブジェクトは共有して使ってしまっているため、なにかの拍子にうっかり管理画面向けのコードをユーザー向けに再利用してしまったり、といったことは物理的には可能になってしまっています。
この状況に依るリスクは、管理画面を改修したつもりがユーザー向けに影響する、またはその逆といったリスクがまず1つ。もう1つは、実装するエンジニアにとっても紛らわしいことです。あるデータAの中のプロパティaが変更可能という前提で実装されていたが、ユーザー向けの仕様としてはそのプロパティaは不変とされている、実は管理画面経由でaは変更可能なのでMutableで実装するしかなかった。みたいなことが起こります。
要は、管理画面は権限最強だけど、ユーザー向け機能は権限を適宜制限するわけなので、近い場所にソースを置くと破綻するということです。
理想は?
別サーバー、別リポジトリ、別Namespaceに丸ごと切り分けるのが理想です。加えて弊社のようにDDDを使っている場合、Entity単位でAdminと切り分けるという方法もありまして、これが最も手軽な方針だと思います(が、やるなら徹底的に分けたい気持ちもある。物理的に分けたら脳死で影響しないと言い切れるから)。
実を言いますと、以前はフロントエンドのNuxtも管理画面とユーザー向け機能がまるっと1つのリポジトリ、1つの本番サーバーで動いていました。
しかし、マナリンクの正式リリースの際に、管理画面向けのNuxtを切り出して、SSR時のコードをそのまま使いつつ管理画面がサーバー費用を消費しないためにFull Static Exportを使ったSSGで管理画面を構築し、S3にデプロイするようにしました。
この結果、管理画面のリリースとユーザー向け機能のリリースを切り分けて行えるようになり、リスクヘッジができています。
これが上手く行ったので、管理画面向けのバックエンドの機能を切り分けるのも効果的だろうと考える次第です。
第1位 手動運用
影響度:高 修正難易度:高
どんな負債?
栄えある第1位は手動運用です!
実装の手が回らない&作りたての機能で検証優先&起こるケースがレア(月に数回程度)な現象に関しては、手動で対応しているものがいくつかあります(エンジニアだけでなく、ビジネスサイドが手動運用しているケースもあります)。
手動運用それ自体は、単純にそのつど数分程度の工数を掛けていっているだけなので大した負債ではないです。問題は、手動でサッと終わらせたいがために、データになんらかの妥協をしていることが大半だということです。
手動で運用するということは、そのアクションを実行したというユーザーのアクセスログなどが残らないですし、例えば新規データの登録であれば、システムで登録すればリレーションなどしっかり張れたものが張れなかったり、削除であっても関連データを削除しきれなかったりします。
手動でしばらく運用したものをあとから機能として実装するとき、過去のデータとの整合性を取るために変な苦労することも多いですし、手動運用そのものというより、手動運用の結果残っているデータに負債が潜んでいる、という感覚がしっくりきます。
理想は?
まあ・・・隙を見て治すしかないですね。まだデータ数が少ないので回っている側面もあり、こういった手動運用体制はスケールすると急に大問題になる見込みが大きいです。
番外編:ミニ負債
番外編として、ミニ負債をいくつかリストアップしておきます。
- Vue.jsを使っているので、Vue3アップデート時の差分がまあまあ大きいことが予想される
- Vue3対応のeslintを予め入れてdeprecatedを検知している
- composition-apiを使い始めている
- ピボットする過程で消しきれなかったカラムが本番データベースに残っている
- 消すしかない
- あと、最初は外部キーで紐付いていたけど、リレーションの定義が変わってNullableな外部キーになってしまったテーブルとかは、扱いが難しくなるので若干負債だと思う
- Vuetify
- 巨大UIフレームワークで便利な一方、バンドルサイズを肥大化させパフォーマンスに難あり
- 管理画面とSEO必要なページを別ドメインや別サーバーで切り分けることで根本解消を目指す
- Laravel
- 2021年年始にヤバめの脆弱性が見つかっていることはもちろん、破壊的変更がマイナーバージョンで普通に入ってきたりするので油断ならないフレームワーク
- なんだかんだ結構依存している
- ちなみにパフォーマンスはOPCache+PHP7.4系で、SQL気をつければ重くてもHTTP単位で100ms前後で現状は十分速いので気にしてない
- BFFとか無いのでNuxtから複数のAPIを叩くケースがあるが、基本Promise.allでやってる
- Firebase
- Firestoreのデータ設計がそこそこ大変。Firestoreのクエリも随時進化しておりキャッチアップも含め大変
- Functionsの性能がバラつきがあるので要件を満たせるか随時検討しなきゃいけない。サーバーレスは安いけどそれはそれでトレードオフがある
- Laravelと併用するためにカスタムトークンを使った認証をしているが、PHPに公式が対応しておらず非公式SDKを使わざるを得ないのもちょっと心配。非公式SDKの対応状況が微妙のため仕方なく一部LaravelコンテナにTypeScriptをぶち込んで解決している箇所もある
番外編その2:負債解消した例
ちゃんとこれまでも負債解消してきています、というのも(私の個人的精神安定のため)書いておきます。実は昨年6月Notionに技術的負債をまとめていたのですが、それの半分程度がさっき見たら解決済みだったので、嬉しくなりました。日頃から意識しておき、隙を見て治すのが大事ですね。
- Wordpress -> EC2 -> Fargateへの変遷
- もともとWordPressで立ち上がったサービスですが、節目節目でAWSに移行したり、AWS内でもEC2からECSに移行しています
- デプロイフローも、masterブランチにマージするだけでデプロイしたり、自動テストも、GitHub ActionでPHPUnitやjestを回す体制に持ってきた
- コンテナベースのデプロイになったので前述したLaravelコンテナに一部TS入れるとかも一応やりやすい。メモリリークとか怖いけど
- CloudWatchからCPU利用率やエラーログの通知をSlackに流す体制も作れた
- https://qiita.com/mejileben/items/f68a50ec9164b261b9cd
- https://note.com/noschool_dev/n/n2e4883315603
- フロントエンドのPugを卒業
- 当初は原則Pugで実装していたが、Veturなどエコシステムが成熟しきらなかった
- pug-to-htmlといったライブラリを使って一気に置換して解消
- 管理画面フロントエンドのモノリシック
- 先程も説明したとおり、管理画面のフロントはS3へのデプロイによって完全に切り離しました
- アップロードされた画像のドメイン
- 以前は同じドメイン以下にアップロードされたが、現在は別ドメインにアップロードされるようにしている
- これにより、imgixというCDNを簡単に導入できた。同じドメインだったら少々大変だったかもしれない
まとめ
以上のような環境で開発を進めています。
(もちろんギリギリ公開できる、という条件なので、ギリギリ公開できないものもあります)(知りたい方は入社してください!w)
いわゆる技術的負債系の問題は、建築でいうと基礎みたいなイメージで、基礎を手抜けば速く建築できるけど思いがけず高い建物が立ったら倒れちゃうし、あとから基礎をやり直すのは無駄に大変になる、しかし一戸建て程度の建築に最初から高層ビル並みの基礎をしっかりやるのも高コスト。というジレンマがあります。
なので、一戸建てが立ち上がったあとに、よっしゃ高層ビルにするぞ!と言われたタイミングで、きっちり納期を多めに確保して基礎から打ち直すといった打ち手を選ぶことが多い印象です。
マナリンクはすでに使って頂いている顧客がいて、合格実績も出始めているサービスです。これから伸ばしていくので、あえて負債を戦略的に作っていく段階ともいえます。
▼例えば生徒さんの合格体験記はこちら
https://manalink.jp/passed-stories/f_lhea4vq
▼CTOの開発に対する考え方はこのへん
https://zenn.dev/meijin/articles/5cb73354486ec0eb54b3
もっと詳しく話を聴きたいなどと思っていただいた方は、お気軽にご応募ください!お待ちしております。