こんにちは、バックエンドエンジニアのKazです。
前々回の記事「PHPからGoへ乗り換えるリスクとは?LIGが自社サービス開発にGo言語を採用したお話」で、LIGが開発言語をPHPからGo言語に乗り換えるにあたって直面した数々のリスクと、そのうち「学習コスト」のヘッジをどのように考えたかをご紹介しました。
PHPからGoへ乗り換えるリスクとは?LIGが自社サービス開発にGo言語を採用したお話
今回はその続きとして「言語を乗り換えるための移行コスト」と、それらをいかにして乗り越えるかをご紹介します。
なぜ移行コストが重くなるのか?モノリシックからの脱却
まずは想像してみてください。長年運用し続けてきたモノリシック(巨大)なサービスやアプリケーションを、根幹レベルからすべて新しい言語で書き直すとしたら? それにかかる労力や時間的コストは、恐ろしいほど膨大なものになります。
またこの規模での開発に要する社内提案を考えてみると、おそらくその理由付けとして「新しい機能を追加したい」「いままで技術的に対応できなかった問題を抜本的に解決したい」といった何らかの付加要素が求められることになると思います。こうなってくると開発規模はさらに肥大化してしまうため、サービス全体を一挙に作り直すという決断をするのは非常に困難です。
そこで、近年注目されている「マイクロサービス」の設計手法を参考にしてみましょう。すでに動いている巨大なシステムを分割するのは至難の技なので、あくまで既存のシステムは動かしつつ、一つ一つの機能を徐々に新システムに移行していくのです。
- モノリシックサービス
-
ひとつのサービスにあらゆる機能を内包する設計。昔ながらのサービス設計は大体これで、機能が密結合になっていてメンテナンスが大変。
ECサイトを例に挙げると、ひとつのコードベースですべての機能を実装している一般的な設計。
- マイクロサービス
-
サービスを機能ごとに細かく切り分ける設計。各機能を疎結合にすることで改変の影響範囲を局所化でき、メンテナンスが容易。近年注目の設計手法。
ECサイトだと、たとえば「商品情報を提供するサービス」「会員機能のサービス」「決済機能のサービス」を独立して設計し、各サービス間をAPIで疎結合させるもの。API仕様だけ確定すればよいため、各サービスの実装に手を加えても他サービスには波及しない。また共通の会員機能を使った新しいサイトなどを簡単に作れる。
具体的には一部の機能、たとえばユーザー情報を取得・保存する機能などを切り出し、部分部分を新しいコードで実装し、共通仕様のAPIを作って旧コードと連携していくのです。このように段階的に機能を移行すれば、定期的に細かいマイルストーンで移行を進めることができます。
古いコードと新しいコードを言語の壁を超えて共存させるというのは、想像するに恐ろしいことのように思えますが、機能を完全に分離した上で厳格に仕様化したAPIをもって連携を行えば、うまく共存させることも夢ではありません。
API化による機能単位の移行
こうしたAPIによる段階的移行の流れを、ECサイトを例に挙げて考えてみましょう。仮に旧システムがすべてPHP、新しいシステムをGo言語で設計するとします。そして大規模移行に併せて新機能の追加が要件に含まれることとします。
まずはECサイトの構成を機能ごとに分離して考えていきましょう。今回は下記の構成を想定します。
- モデル
- 商品データ
- 会員データ
- ショッピングカート情報
- コントローラー
- 商品表示機能
- 決済機能
- …
- ビュー
- 商品系ページ
- …
MVCモデルを前提とした表記を行っていますが、MVCで書かれていないアプリケーションであっても「モデルに相当する処理」などとして読み進めてください。
このとき、機能ごとに徐々に移行する手立てとしていくつかの選択肢があります。
A案:旧コードと新コードで同じものを実装する場合
まずは一番単純な案を考えてみます。これはAPIも何も使わず、旧コードはそのまま残した上で新機能の追加に向けて必要な処理を新コードで書いていく方法です。この場合新機能を完全に新コードで実行し、旧来の機能はそのまま旧コードで実行し続ける形となります。
この方法のデメリットは同一の処理が別コードとして存在し続けることで、新旧コードが共存する間ふたつのコードのメンテナンスを並行で行わなければならなくなることが挙げられます。こうなるとテストの工数も増える上、人間が作業をする以上どちらか一方に対応忘れがあった場合などバグのリスクも増えてきます。こういった事情があるため複数のコードベースを並列でメンテナンスし続けることは得策ではなく、一般的に敬遠される傾向にあります。
短期的には後述のB案よりも早く新コードが完成するかもしれませんが、メンテナンスまで含めて考えると総合的な負荷が最も高くなる可能性があるため、このアプローチを採る場合はなるべく短期決戦ですべてのコードを新コードに移行してしまいたいところです。
B案:旧コードの一部を新コードに置き換える場合
もうひとつの方法では、モデルに関連するロジックを新しいコードで再実装し、旧コードからAPIを通じてそれにアクセスする形を採ります。
具体的な例を考えてみましょう。まず上記のECサイトで、商品データに関連する処理から新しいコードに移行を進めるとします。この場合モデルにおける「商品データの保存・取得・検索」処理をまず新しいGo言語のアプリケーションに実装し、PHP側のコードを修正してデータベースの代わりに常にAPIにデータを取りに行くように処理を変更していきます。
仮にAPIをRESTfulに沿って設計するとして、下記のエンドポイントを新しいコードで実装するとします。
GET /products (商品一覧・検索) GET /products/:id (個別商品取得) POST /products (商品作成) PUT /products/:id (商品更新)
PHPではモデルを実装しているクラスを書き換え、データベースにSQLを投げる代わりにcURLでこれらの新しいAPIを叩くようにします。こうして新APIの実装と旧コードとの繋ぎこみを繰り返すことで、まずはMVCモデルにおけるモデル相当の処理をすべてGo言語に寄せていきます。
こうしておおよそのモデルの移行が済んでしまえば新しいコードで新機能を実装できるようになるため、必要なタイミングで新機能を実装したり、その後は徐々に残った旧コードの処理を新コードで書き直していくという流れになします。
この方法はおそらくリファクタリングが大好きなエンジニアにとっては一番楽しい方法となりますが、どうしても旧コードに大幅に手を加えなければならない以上作業量は増えてしまいます。
しかし逆に「古いページは移行せずにそのまま残しでいい」「旧ページについてはモデルのみ刷新し、コントローラー・ビューはそのまま残す」といった場合などは、このアプローチにより古いページの内部処理も一部新しいコードに寄せることができるようになります。その場合は作業量を抑えつつ新コードの恩恵を受けられるようになります。
C案:新コードから旧コードを参照する場合
もうひとつのアプローチとして、旧コードにAPIを新設し(あるいはすでに存在する場合はそれを用いて)、新コードからそちらを参照するという方法もあります。特に旧コードベースが巨大で、かつなるべく早く新しい機能を提供したい場合はこのアプローチが最短の方法となります。
旧ページの移行方法についてはB案と真逆で、ビューやコントローラーを先に新コードで実装し、最後にモデルの移行を行う形になります。このため手っ取り早く表側をすべて新コードに置き換えることができ、その後急ぐことなく徐々に裏側の処理を移行していくことができるというメリットがあります。
LIGでGo言語からPHPへと移行した際は、新機能の必要性が大きく速やかに提供する必要があったためこちらのC案を採用しました。
段階的な移行によるメリット
さて、このようにして徐々に移行を進めることでどのようなメリットを享受できるのでしょうか。
全体の移行完了を待つことなく新機能を提供できる
すべてのコードを書き換えて一挙に移行作業を行う場合、冒頭で述べたようにその開発期間は膨大なものになってしまいます。
この場合、新しいコードに依存した新機能をリリースするためには長い開発期間を待つ必要が出てきますが、段階的に移行することで新機能に必要な箇所だけ優先的に実装・リリースすることが可能になります。
段階的に成果をリリースできる
前述の新機能の優先提供とも繋がりますが、エンジニア以外の人間にとってその必要性を理解しがたい「言語やプラットフォームの移行」に対して長い開発期間を投じることは、ある種の社内政治的なリスクが伴います。
ここまで大きな規模の開発となるとどうしても潜在的な問題が発生したり、また途中で幾度も仕様変更が入ったりと、開発コストがさらに伸びる可能性も増えてきます。この結果、最悪「途中で開発中止」などの判断が下されてしまうと、すべての成果が無駄になりかねません。
ここで段階的なリリースを採用しておけば短いスパンで定期的に成果を上げられるようになるため、平和に開発を進めることができるようになります。
潜在的不具合に対応しやすい
大規模リリースを行うとその規模に応じて不具合が発生する確率が上がってきます。それを防ぐためのテストはもちろん必要なのですが、テストケース漏れなどどうしても潜在的な不具合が残る可能性は拭いきれません。
また大規模リリースでもし甚大な不具合が見つかってしまった場合、リリースの差し戻し対応なども相応に大変なものになってきます。
小規模のリリースを段階的に行う方式であれば、一つ一つのテストケースを限定できるため、場所を絞ってより徹底的にテストを行うこともできますし、個々のリリースに不具合が潜んでいたとしても件数を少なく抑えられるため、対応が比較的容易になります。
望むべくは「緩やかな移行」
このように、言語の移行を一発でやってのけることには相応のリスクが伴ってきます。これによって稼働中のサービスが死んだり、移行プロジェクトそのものが頓挫してしまっては元も子もありません。
段階的に新コードに移行し、旧コードを徐々に少なくしていくことで、穏やかで安心安全な移行作業とすることができるのです。
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。