「触るな危険」のレッドゾーン。巨大モノリスと僕らの終わらない残業
どうも!ヒロです。いま僕はヨーロッパのとある国で、C#とWPFをメインに使うITエンジニアとして働いています。こっちに来て早数年、コード書いて、ミーティングして、時差ボケのインドチームとチャットして、まぁなんとかサバイブしてる毎日です。
海外で働くエンジニアって言うと、なんかキラキラしたイメージありますか?最新の技術スタックで、スマートな多国籍チームと、アジャイルにスプリント回して…みたいな。
うん、そういう側面もゼロじゃない。でもね、現実はそんな甘くない。
僕ら「海外出稼ぎエンジニア」の前に立ちはだかる最大の敵。それは、**「巨大モノリス・レガシーシステム」**です。
特に僕の主戦場であるWPF。Windowsのデスクトップアプリですね。これがまぁ、曲者(くせもの)なんですよ。金融、製造、物流…いろんな業界の基幹業務で、10年、15年と使われ続けてきた「秘伝のタレ」みたいなアプリケーションがゴロゴロしてる。
僕が今ジョインしてるプロジェクトも、まさにそれ。とある製造業の基幹システムで、設計から製造ラインの管理、在庫チェックまで、ぜーんぶ入りの「全部乗せラーメン」みたいなWPFアプリです。コード行数は…もう誰も数えたくないレベル。
これが、ヤバい。
何がヤバいって、もう「密結合」とかいうレベルじゃない。「融合」してる。
金曜の午後4時。週末前の穏やかな時間。
「ヒロ、ちょっといいか?顧客Aから、納品書画面のフッターに電話番号を追加してほしいって急ぎで頼まれたんだ。今日中にいけるか?」
マネージャーのマークが人の良さそうな顔で言ってくる。
(お、簡単じゃん。XAML(ザムル:WPFの画面定義ファイル)いじって、ViewModelからデータバインドするだけっしょ)
僕は余裕しゃくしゃくで引き受けた。
「OK、マーク。1時間もあれば終わるよ」
…それが、地獄の始まりでした。
まず、ビルドが通らない。いや、正確にはビルドに25分かかる。ちょっと修正して動作確認するたびに、コーヒー休憩が必要になるレベル。
やっとビルドが通って起動。よし、納品書画面を開くぞ…
「Unhandled exception has occurred in your application.」
(ハンドルされてない例外が発生したよ)
出たよ。おなじみのエラーダイアログ。
スタックトレース(エラーの履歴)を追うと、なぜか全然関係ないはずの「在庫管理モジュール」の奥深くでNullポインタ例外(ぬるぽ)が出てる。
なんで!?僕は納品書の「見た目」を触っただけだぞ!?
慌ててコードを追う。すると、納品書画面のベースクラス(親玉みたいなクラス)が、なぜか在庫管理モジュールが内部で使ってる共通ユーティリティクラスを「こっそり」継承していて、そのユーティリティクラスのコンストラクタ(初期化処理)が、今回の僕の修正で意図せず変更されたグローバルな静的変数(Shared Global Variable)を参照して、特定の条件下でNullを返すようになっていた…。
…意味わかります?(笑)
わかんないですよね。僕も書いてて意味わかんない。
でも、これがモノリスの日常なんです。
「あっちを直せば、こっちが壊れる」
「この修正の影響範囲?…神(と、5年前に辞めたロシア人の元エースエンジニア)のみぞ知る」
僕のチームには、シニアエンジニアのピーター(イギリス人)がいます。彼はこのシステムの生き字引みたいな人。
「ピーター、助けて。納品書いじったら在庫管理が死んだ」
「(ふかーいため息)…ああ、またか。ヒロ、それは『悪魔のグローバル変数』の仕業だ。たぶん、SharedConstants.cs の3800行目あたりを見てみろ。そこを false から true に…いや待て、それやると今度は会計モジュールがバグるんだった。えーと…」
もう、カオス。
新しい機能を追加するなんて夢のまた夢。ビジネスサイドからは「競合他社はもうWebで同じことやってるぞ!ウチも早くクラウド対応しろ!」って言われる。
こっちは「無理言うな!このWPFアプリ、ちょっと触っただけで会社が止まるんだぞ!」って言いたい。
これが「技術的負債」ってやつです。
しかも、海外の現場は人の入れ替わりが激しい。ドキュメントは基本、無いか、あっても5年前のバージョン。コードが唯一の真実(Code is the documentation)。
そんな「詰んだ」状況で、みんなどうしてると思います?
たいていのエンジニアは諦めます。
「これは俺の仕事じゃない。俺は言われたバグを直すだけ」
「どうせリプレイスするんでしょ?(永遠に来ないその日を待つ)」
マネージャーも諦めます。
「わかってる。わかってるんだ。だがビジネスを止めるわけにはいかない。頼む、なんとかパッチ(応急処置)を当ててくれ」
そして、みんなで「触るな危険」のレッドゾーンを避けながら、お祈りしながらデプロイする日々。
僕も、正直諦めかけてました。
(C# WPFのキャリア、もう終わりかな…。こんなレガシー触ってたら、市場価値下がる一方だ…)
でも、それ、あなたのせいじゃないんですよ。
こういう「どうしようもないモノリス」を前にして、エンジニアができることって限られてる。
「全部捨てて作り直しましょう!」(ビッグバン・リライト)
なんて提案は、ビジネス的にほぼ100%通りません。だって、作り直してる間、ビジネスは止まるの?既存のバグ修正は誰がやるの?って話だから。
僕らエンジニア、特に海外で結果を出さないといけないサバイバーとしては、もっと「賢く」立ち回る必要があります。
**「人生術」**と言ってもいいかもしれない。
どうやって、この巨大で危険なレガシーシステムと「共存」しながら、ビジネスを止めずに、徐々に、安全に、新しいモダンな世界(クラウドとか、マイクロサービスとか)に移行していくか。
これ、まさに僕が海外の現場で直面して、そして乗り越えようとしている最大の課題です。
ある日、僕らのチームに新しくジョインしたアーキテクトのサラ(ドイツ人)が言いました。
「このモノリス、面白いわね。まるで巨大なイチジクの木みたい」
「…イチジク?」
「そう。**『ストラングラーフィグ(Strangler Fig)』**よ」
ストラングラーフィグ。日本語だと「絞め殺しのイチジク」。
熱帯雨林にある植物で、他の大きな木にツルを巻きつけながら成長し、最終的には宿主の木を覆い尽くし、枯らしてしまう(絞め殺してしまう)植物のことだそうです。
「なにそれ、怖い…」
「でも、これが私たちのアプローチよ。この巨大なモノリス(宿主の木)を、新しいサービス(イチジクのツル)で少しずつ覆っていくの。安全に、確実に、ビジネスを止めずにね」
これが、僕と「ストラングラーフィグ・パターン」との出会いでした。
このブログでは、僕がこの海外の現場で、あのWPFモノリスという「怪物」を相手に、どうやってこの「ストラングラーフィグ」アプローチを実践しているかを、超具体的にシェアしていこうと思います。
これは、単なる技術論じゃありません。
どうやってレガシーコードの「密林」から、安全に切り出せる「独立した機能」を見つけ出すか?
WPFのようなデスクトップアプリの「一部」を、どうやって新しいマイクロサービスのAPIで「包んで」いくか?
そして何より、どうやって「どうせ無理」と諦めているチームの仲間やマネージャーを説得し、この「じわじわ移行」を成功させるか?
そういった、**現場のリアルな「泥臭い戦い方」と「サバイバル術」**の話です。
もしあなたが今、
- 「触るな危険」のレガシーコードに消耗してる
- C#やWPFのキャリアにちょっと不安を感じてる
- 「全部作り直し」以外の現実的な改善策を知りたい
- 海外でエンジニアとして「価値ある仕事」をしたい
そう思ってるなら、絶対に読んで損はさせません。
これを知ってるだけで、あなたのエンジニアとしての「立ち回り方」が、マジで変わるはずですから。
さて、次回【承】では、まず僕らが最初に取り組んだ、あのカオスなコードベースの密林に分け入り、最初の「ツル」を巻きつけるターゲット(機能)をどうやって特定したか…その「泥臭い分析」の話をします。
密林の解剖学。僕らが「最初の獲物」を見つけるまでの泥臭い戦い
(前回のあらすじ)
ビルドに25分、触れば壊れる15年モノの巨大WPFモノリス。絶望する僕ら海外出稼ぎエンジニアチームの前に現れたドイツ人アーキテクトのサラ。彼女が提案したのは、モノリスを徐々に絞め殺していく「ストラングラーフィグ(絞め殺しのイチジク)」パターンだった。
「言うは易し」とはまさにこのこと。僕らの目の前には、相変わらず「融合」したコードの密林が広がっている。
さて、サラが「ストラングラーフィグよ!」と宣言した翌日のミーティング。
チームの空気は、正直言って「最悪」でした(笑)。
「(また始まったよ、アーキテクト様の理想論が…)」
「どうせ『全部マイクロサービスに置き換えよう!』とか言って、現場が疲弊するパターンだろ」
みんなの顔にそう書いてある。
特に、このシステムの生き字引であるシニアエンジニアのピーター(イギリス人)。彼は腕組みしたまま、一言も発しない。彼が一番、このシステムの「ヤバさ」と「不可能性」を知っているからです。
サラはそんな空気をモノともせず、ホワイトボードにデカデカとこう書きました。
“How to identify and isolate independent functionalities?”
(どうやって、独立した機能を見つけ、分離するか?)
「いい、ヒロ。ピーター。みんな聞いて。ストラングラーフィグの第一歩は、コーディングじゃないわ。『探検』よ」
「探検(Exploration)?」
「そう。この巨大なモノリスという『密林』を探検して、どこに『ツル(新しいサービス)』を巻き付け始めるか、最初のターゲットを見つけるの。ここで間違えると、私たちがモノリスを絞め殺す前に、モノリスに絞め殺されるわ」
いや、比喩が怖いよサラ…。
でも、彼女の言いたいことは明確でした。
「全部が全部と繋がってる」ように見えるこの怪物から、どうやって「ここなら切り離せるかも」という、比較的「独立した」機能を見つけ出すか。
これが、僕らの最初の、そして最も重要なミッションになりました。
よくある間違い:「技術レイヤー」で切ろうとすること
僕らエンジニア、特にWPFみたいなMVVM(Model-View-ViewModel)パターンで育ってきた人間は、こういう時つい「技術レイヤー」で切り離そうと考えがちです。
「よし、まずはデータアクセス層(DAL)を全部API化しよう!」
「いや、ビジネスロジック(BL)を別ライブラリに切り出して…」
これ、巨大モノリス相手だと、ほぼ100%失敗します。
なぜなら、そういう「キレイな」層(レイヤー)構造が、もはや存在しないから。
15年の歴史の中で、本来的にはUI(View)にあるべきロジックがViewModelに漏れ出し、ViewModelにあるべきロジックがModelに染み出し、なぜか共通ユーティリティクラス(SharedUtils.cs みたいなやつ)がDBの接続文字列を直接管理してる…なんてのが日常茶飯事。
「起」で僕が死んだのも、まさにそれ。納品書(View)の修正が、在庫管理(Biz Logic?)のグローバル変数(Shared Utils?)を踏み抜いたせいでした。
サラは言います。
「技術レイヤーで切るのは無理。私たちが切るべきは、**『ビジネスの境界(Business Capability)』**よ」
「ビジネスの境界…」
「そう。このシステムは、ビジネス上、何と何をやっているの?『顧客を管理する』『製品カタログを見る』『注文を受ける』『納品書を印刷する』『在庫を管理する』…これらは、本来別々の機能のはずよ」
なるほど。技術的なコードの繋がりじゃなく、ビジネス的な「意味」で分割の単位を探すわけか。
ステップ1:密林の地図作り(という名の説得材料集め)
とはいえ、僕らはビジネスの専門家じゃない。そこで、まずマネージャーのマークを巻き込みました。
「マーク、このシステムって、ビジネス的に一番『困ってる』機能、あるいは『一番変更が多い』機能ってどれ?」
マークは待ってましたとばかりに言いました。
「それなら間違いなく『価格計算エンジン』だ!あそこは毎月のように割引ルールが変わるのに、修正に2ヶ月もかかってる!あそこを分離してくれ!」
…いきなりラスボス来た。
価格計算エンジン。コードを見たら、案の定このモノリスの「心臓部」でした。顧客情報、製品情報、在庫情報、過去の注文履歴…すべてが複雑に絡み合い、もはや「密結合の塊」。
ピーターが冷静にツッコミます。
「マーク、そこを今触ったら、来月の請求書が全部ゼロ円になる可能性があるぞ」
「…それは困る」
サラが仕切り直します。
「OK、マーク。ビジネスインパクト(効果)が大きいのはわかったわ。でも、私たちが今探しているのは、『ローリスク・ハイリターン』な場所。いや、最初は**『ローリスク・ローリターン(低リスク・低効果)』**でもいい」
これ、めちゃくちゃ重要な「人生術」です。
僕らエンジニアは、つい一番カッコよくて、一番難しい問題(ラスボス)から倒しに行きたがる。でも、チームが「どうせ無理」って諦めモードの時にそれをやると、ちょっとした失敗で「ほら、やっぱり無理だったじゃん」とプロジェクト自体が頓挫する。
海外の現場は特にシビア。結果が出ないプロジェクトは、驚くほどあっさり打ち切られます。
だから、最初の成功体験、「あ、これイケるかも」という**「スモールウィン(Small Win)」**を積むことが、チームの士気を上げ、次の(もっと難しい)ステップに進むための「政治力」を確保するために、何よりも重要なんです。
「わかった。じゃあ、ビジネスインパクトはさておき、まずは『安全に』切り離せそうな場所を探そう」
僕らは方針を転換しました。
ステップ2:コードの密林探検(ツールと「勘」を総動員)
さぁ、ここからが僕らC#エンジニアの出番です。
「安全に切り離せる」=「他のモジュールとの依存関係(Dependencies)が少ない」場所を探します。
とはいえ、25分かかるビルドを繰り返しながらコードを全部読むのは不可能。
僕らが使ったのは、以下の「武器」です。
武器1:Visual Studio コードマップ (Code Map)
Visual StudioのEnterprise版にしか入ってないことが多いんですが、これ、マジで便利です。ソリューション全体を解析して、アセンブリ(DLL)間やクラス間の依存関係を、文字通り「地図」として可視化してくれます。
僕らのWPFモノリス(ソリューションファイル .sln)を食わせてみました。
…待つこと15分。
出てきたのは、もはや「地図」ではなく、**「黒いスパゲッティの塊」**でした。
全モジュールが Common.dll や Core.dll といった「神クラス」ライブラリに依存し、その神ライブラリを経由して、結局全員が全員と繋がっている。
「うわぁ…」チーム全員が絶句。
「起」で話した「悪魔のグローバル変数」が、たぶんこの Common.dll の中にいる。
武器2:NDepend (または ReSharper) による静的解析
コードマップが「マクロ(全体図)」なら、こっちは「ミクロ(詳細図)」です。
NDepend(有料ツールですが、ガチな分析には最強)を使って、もっと具体的な「依存元」「依存先」を調べました。
特に注目したのは、「データベース(DB)への直接アクセス」と「外部API呼び出し」、そして**「WPFのUI(View)への直接参照」**です。
もし、あるビジネスロジック(例:ProductCatalogService.cs)が、他のモジュールを一切参照せず、ただDBからデータを読んで返すだけなら…?
それは「独立した機能」の最有力候補です。
解析クエリ(CQLinq)をぶん回します。
「Common.dll を参照 していない クラスを探せ」
→ 結果:0件(絶望)
「じゃあ、Common.dll は参照してるけど、他のビジネスモジュール(Order.dll とか Inventory.dll)を参照 していない クラスは?」
→ 結果:お、いくつか出てきたぞ…?
武器3:生き字引(ピーター)へのヒアリング
ツールが「静的な」依存関係しか教えてくれないのに対し、ピーターは「動的な」依存関係、つまり「実行時にしかわからない繋がり」や「コードには書かれていないビジネスルール」を知っています。
僕:「ピーター、この『製品カタログ表示(ProductCatalogView.xaml)』ってさ、ぶっちゃけ『注文(Order)』モジュールなしでも動くと思う?」
ピーター:「(ふかーいため息の後)…ああ、ProductCatalogView か。あれは厄介だぞ。単体で起動するように見えて、実は『注文画面』から呼び出される時に、SharedConstants.IsOrderMode っていう例のグローバル変数が true になるんだ。その時だけ、カタログの『価格表示』ロジックが変わる。注文モジュールが知らない『割引ルール』を適用するためにな」
出たよ。またお前か、グローバル変数。
ステップ3:ターゲット(最初の獲物)の選定
この泥臭い分析(ツールでの解析と、ピーターへのヒアリング)を2週間続けた結果、僕らはついに、最初のターゲットを見つけ出しました。
それは…「製品カテゴリのツリー表示」機能でした。
地味!めっちゃ地味!(笑)
「価格計算エンジン」に比べたら、ビジネスインパクトなんてほぼゼロ。
WPFアプリの左側にあるナビゲーションペインに表示される、製品の分類(例:『ドリル』→『電動ドリル』→『12Vモデル』)をツリー表示するだけの機能です。
でも、これが「最初の獲物」として最適だったんです。
- 依存関係が(比較的)少ない:
- データは専用の「カテゴリマスタ」テーブルから読んでくるだけ。
- ピーターに確認したところ、「あのツリーは、忌々しいグローバル変数(
SharedConstants)の影響を奇跡的に受けていない、この城の唯一の『聖域(Sanctuary)』だ」とのこと。
- 読み取り専用 (Read Only):
- データの書き込み(Create/Update/Delete)がない。これが超重要。データの整合性を気にする必要がないため、切り離しのリスクが格段に低い。
- UIとして独立している:
- WPFの
UserControl(部品化されたUI)として定義されていて、アプリのメインウィンドウ(MainWindow.xaml)にガツッとハマってるだけ。他の画面からポップアップで呼ばれる、みたいな複雑な使われ方をしていなかった。
- WPFの
サラはニヤリと笑いました。
「決まりね。私たちの最初のターゲットは『製品カテゴリ・ツリー』。地味だけど、ここが私たちの『橋頭堡(きょうとうほ:攻略の足がかり)』よ」
「承」のまとめ:まずは「勝てる戦」から始めよ
巨大モノリスという「密林」を攻略する第一歩。
それは、ヒーロー的なコーディングではなく、考古学者のような地味で泥臭い「分析」でした。
ビジネスサイドを巻き込んで「ビジネス境界」を探り、
コード解析ツールで「静的な依存」を暴き、
生き字引のシニアエンジニアから「動的な罠」を聞き出す。
そして何より、チームの士気を上げるために、最初は絶対に失敗しない**「ローリスク・ローリターン」な読み取り専用機能**を狙う。
この「勝ち戦」を選ぶセンスこそが、レガシーシステムと戦うエンジニアにとって、最強の「人生術」なんだと、僕は海外の現場で学びました。
さて、ターゲットは決まった。
いよいよ、僕らの「ツル(新しいサービス)」を伸ばす時です。
この、WPFモノリスの「一部品(UserControl)」としてガッチリ組み込まれている「カテゴリ・ツリー」を、どうやってモノリス本体から引き剥がし、新しいマイクロサービスのAPIで「ラップ」するのか?
これがまた…WPF特有の「見た目(UI)とロジックの癒着」という、第二の壁にぶち当たるんです。
次回【転】、「WPFの壁」とご対面。APIで既存コンポーネントを「包む」ための、僕らの具体的なステップと「アンチパターン」について、ガッツリ語ります。
WPFの壁とAPIの罠。僕らが学んだ「一度に二つ変えるな」という鉄則
(前回のあらすじ)
僕ら海外出稼ぎエンジニアチームは、巨大モノリスWPFアプリの「最初の獲物」として、地味だが安全な「製品カテゴリのツリー表示」機能を選定した。アーキテクトのサラの戦略は、リスクの低い「スモールウィン」を積むこと。いよいよ、この既存コンポーネントを、新しいマイクロサービスAPIで「ラップする(包む)」という、ストラングラーフィグ(絞め殺しのイチジク)の核心的なステップに取り掛かる。
ターゲットは決まった。「製品カテゴリのツリー表示」だ。
ミーティングルームで、僕は意気揚々と宣言した。
「OK、サラ、ピーター。こいつはWPFの UserControl(ユーザーコントロール:UI部品)になってる。MVVMパターン(※)に(一応)なってるから、ProductCatalogTreeViewModel.cs っていうViewModelファイルがある。こいつが、DBからデータを取ってきて、ツリー構造(ObservableCollection<CategoryNodeViewModel>)を作って、画面(View)にバインドしてる。単純だ」
(※MVVM: Model-View-ViewModel。WPFでよく使われる設計パターン。UI(View)とロジック(ViewModel)を分離するのが目的…なのだが、レガシーコードでは大抵これが破綻している)
「だから、やることはシンプル。まず、カテゴリデータを返すだけの新しいAPI(マイクロサービス)を .NET 7 のASP.NET Core でサクッと作って、このViewModelから HttpClient でそのAPIを async/await で叩くように書き換えれば、はい、モノリスから分離完了っしょ!」
僕は、当時イケてると信じていたモダンなやり方を披露した。
これぞ、海外エンジニア。レガシーコードを華麗にモダン化する俺、カッケー。
…しかし。
生き字引のピーターは、またあの「ふかーいため息」をついた。
サラは、僕の目をじっと見て、静かに言った。
「ヒロ。それが一番やっちゃいけない『アンチパターン』よ」
「…え?」
アンチパターン(失敗例):張り切りすぎた僕の「API直叩き」地獄
なぜ、僕の提案がダメだったのか?
サラが「ノー」と言ったにもかかわらず、僕は(若気の至りで)「いや、いけるはずだ」と自分のローカル環境でこっそり試してみたんです。
やったこと(失敗例):
- 爆速でAPI構築:
dotnet new webapi -n CategoryApiでASP.NET Core Web APIのプロジェクトを作成。モノリスが使ってるのと同じDB(のカテゴリマスタテーブル)を読み取り、GET /api/v1/categoriesでJSONを返すエンドポイントを作った。ここまでは完璧。 - ViewModelの魔改造: ターゲットの ProductCatalogTreeViewModel.cs を開く。案の定、コンストラクタ(初期化処理)の中で、あの「悪魔のCommon.dll」経由でDBから同期的に(!)データを取ってくる古いコードが直書きされていた。「うわ、だっさ。こんなの HttpClient で非同期に置き換えてやる」僕はその古いコードをコメントアウトし、代わりに HttpClient を使って、さっき作った GET /api/v1/categories を async/await で呼び出し、返ってきたJSONをデシリアライズしてツリー構造に詰めるコードを書いた。
「よし、動いた!ちゃんとツリーが表示される!モダン化、大成功!」
…そう思ったのは、わずか30分でした。
地獄その1:UIフリーズとパフォーマンスの悪夢
WPFアプリを起動すると、カテゴリ・ツリーが表示されるまで、一瞬(体感0.5秒)だが、**確実にUIが「カクつく」**ようになった。
古いコードは、DBから直接データを読むとはいえ、同じLAN内にあるDBサーバーとのやり取りだったから速かった。
しかし、新しいAPIは「ネットワーク越しのHTTP通信」。どんなに速いAPIでも、ネットワークの遅延(レイテンシ)や、JSONのシリアライズ・デシリアライズのコストが乗っかる。
WPFのようなデスクトップアプリは、UIスレッド(画面を描画するメインスレッド)が命。async/await を使って非同期にしたつもりでも、起動時のコンストラクタで呼んでしまったり、Result プロパティで無理やり同期的に待ってしまったりする(レガシーコードあるある)と、その瞬間にUIスレッドがブロックされ、ユーザー体験は最悪になる。
ピーター:「(僕のPCの画面を覗き込み)…ヒロ、クリックするたびにマウスカーソルが『砂時計』になってるぞ。これじゃユーザーからクレームの嵐だ」
地獄その2:認証・認可の迷宮
僕らのモノリスWPFアプリは、Windows認証(Active Directory)で動いていた。「誰がアプリを使っているか」は、Windowsのログイン情報で自動的に管理されていた。
しかし、僕が新しく作ったAPIは、デフォルトのまま。つまり「認証なし」か、せいぜいJWTトークン認証を想定していた。
ローカル環境では動いた。でも、ステージング環境(本番に近い環境)にデプロイした途-たん、APIが「401 Unauthorized(認証エラー)」を返してきた。
モノリス(WPSアプリ)が持ってるWindows認証の「資格情報」を、どうやって新しいAPI(マイクロサービス)に引き継ぐんだ?HttpClient に UseDefaultCredentials = true をつければいい?いや、APIサーバー側がWindows認証を受け付ける設定になってない…。
セキュリティという、まったく別の巨大な問題に直面してしまった。
地獄その3:デバッグ(切り分け)の崩壊
一番最悪だったのがこれ。
「ヒロ、カテゴリ・ツリーの一部が表示されないってバグ報告が来てるぞ」
「え!?…ローカルでは動いたのに…」
さぁ、原因調査だ。
これは、モノリス(WPF)のViewModelのバグか?JSONのデシリアライズ処理のミスか?それともネットワークの問題か?ファイアウォールでブロックされてる?それとも、新しいAPI側のロジックがバグってる?
僕は、モノリスのデバッガと、APIサーバーのログと、ネットワーク監視ツール(Fiddler)を同時に睨めっこするハメになった。
「一度に二つのことを変えた」
これが、僕の致命的な失敗でした。
僕は、「①DBアクセスロジックの置き換え」と「②ネットワーク越しのAPI呼び出しへの変更」という、まったく性質の異なる二つの変更を、同時にやってしまった。
だから、問題が起きた時に、どっちが原因か特定できなくなったんです。
「転」の核心:『ファサード』で、まず「壁」を作れ
ミーティングルームで、僕のローカルでの惨状を報告すると、サラは「だから言ったでしょ」という顔で、ホワイトボードに向かった。
「ヒロ。いい教訓になったわね。ストラングラーフィグ(絞め殺し)は、一気に首を絞めたら宿主(モノリス)が暴れて、こっちがやられるのよ」
「じゃあ、どうすれば…」
「**『ファサード(Facade)』**よ。まず、モノリスの中に『壁』を作るの」
サラが示した、正しい「ラップ」の実践的ステップはこうだ。
ステップ1:API(ツル)を作る(※これはさっきと同じ)
GET /api/v1/categories を返すASP.NET Core Web APIを作る。これはOK。ただし、まだWPFアプリからは「呼ばない」。こいつはまだ出番待ち。
ステップ2:モノリス側に「インターフェース」を定義する
これが最重要。
モノリスのWPFプロジェクト(あるいは共通ライブラリ)に、新しいインターフェースを1枚切る。
C#
// ICategoryService.cs
// これから「カテゴリ情報が欲しい」時は、必ずこのインターフェース経由で要求する、という「新しいルール」を作る。
public interface ICategoryService
{
// ViewModelが欲しいのは、結局これ(ツリー構造のデータ)だけ。
Task<IEnumerable<CategoryNode>> GetCategoryTreeAsync();
}
ステップ3:インターフェースの「古い実装(ファサード)」を作る
次に、このインターフェースの実装クラスを作る。
こいつが「ファサード(建物の正面)」=「ラップ(包むもの)」になる。
C#
// LegacyCategoryServiceFacade.cs
// 「ファサード」クラス。中身は「古いロジック」のまま。
public class LegacyCategoryServiceFacade : ICategoryService
{
public async Task<IEnumerable<CategoryNode>> GetCategoryTreeAsync()
{
// ★★★ココが最重要★★★
// 既存の ProductCatalogTreeViewModel.cs に直書きされていた
// 「あの忌々しい Common.dll 経由でDB直叩き」の
// 古いコードを、そのままコピペしてくる!
// (あるいは、古いクラスを内部で呼び出す)
// (例:古いロジックがこんな感じだったとする)
var oldData = LegacyDbAccessor.GetCategoriesFromDb();
var tree = BuildTreeFromOldData(oldData);
// 非同期インターフェースに合わせるため、形だけ Task.FromResult で包む
return await Task.FromResult(tree);
}
}
ステップ4:ViewModel を「インターフェース依存」に書き換える
最後に、ターゲットの ProductCatalogTreeViewModel.cs を書き換える。
でも、HttpClient でAPIを呼ぶコードにはしない。
C#
// ProductCatalogTreeViewModel.cs (改修後)
public class ProductCatalogTreeViewModel : BaseViewModel
{
private readonly ICategoryService _categoryService;
public ObservableCollection<CategoryNodeViewModel> CategoryTree { get; }
// ★コンストラクタで「インターフェース」を受け取るようにする(DI:依存性の注入)
public ProductCatalogTreeViewModel(ICategoryService categoryService)
{
_categoryService = categoryService;
CategoryTree = new ObservableCollection<CategoryNodeViewModel>();
LoadCategoriesAsync(); // 非同期でロード
}
private async void LoadCategoriesAsync()
{
// ★古いロジック(DB直叩き)は完全に消えた!
// ★ViewModel は「インターフェース」を呼ぶだけ。
// この裏側が「古いロジック」なのか「新しいAPI」なのか、ViewModel は知らない。
var nodes = await _categoryService.GetCategoryTreeAsync();
// (あとは受け取ったデータを画面用のViewModelに詰め替える処理...)
}
}
(※DIコンテナ(Unityとか)を使って、ICategoryService が呼ばれたら LegacyCategoryServiceFacade のインスタンスを渡すように設定する)
これが、本当の「ラップ」だ。
何が起きたかわかりますか?
僕らは、WPFアプリ(ViewModel)の動作(振る舞い)を一切変えていない。
LegacyCategoryServiceFacade の中身は、古いロジックそのものだから。
でも、僕らは「壁」を作った。
ProductCatalogTreeViewModel は、もう「DB直叩き」という「レガシーな実装」を知らなくなった。彼は ICategoryService という「抽象的な窓口」とだけ会話するようになったんです。
これが「WPFの壁(UIとロジックの癒着)」を安全に引き剥がす、唯一の方法でした。
「サラ…すごい。これなら、パフォーマンスも変わらないし、認証の問題も起きない。だって、何も変えてないんだから」
「そうよ、ヒロ。私たちは今、**『切り替えスイッチ』**を手に入れたの」
彼女の言う通り。
僕らは今、ICategoryService という「切り替えスイッチ」を手に入れた。
今は、このスイッチは「古いロジック(Legacy)」側に倒れている。
でも、もし僕らが、新しいAPI(GET /api/v1/categories)を呼ぶ、別の実装クラスを作ったら?
C#
// NewApiCategoryService.cs (未来の実装)
public class NewApiCategoryService : ICategoryService
{
private readonly HttpClient _httpClient;
public NewApiCategoryService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<IEnumerable<CategoryNode>> GetCategoryTreeAsync()
{
// こっちは「新しいAPI」を呼ぶ!
// (認証やパフォーマンスの問題は、このクラスの中で頑張って解決する)
var json = await _httpClient.GetStringAsync("/api/v1/categories");
var nodes = JsonConvert.DeserializeObject<IEnumerable<CategoryNode>>(json);
return nodes;
}
}
…そう。
あとは、DIコンテナの設定を LegacyCategoryServiceFacade から NewApiCategoryService に切り替えるだけで、ViewModel に一行も触れることなく、裏側のロジックを「古いDB直叩き」から「新しいAPI呼び出し」に差し替えることができる。
これが、ストラングラーフィグ・パターンにおける「既存コンポーネントのラップ」の実践的なステップでした。
「一度に二つ変えるな」。
「まず『抽象(インターフェース)』という壁で『ラップ』しろ」。
「『差し替え』は、その後でやれ」。
この海外の現場で学んだ「人生術」は、僕のエンジニアリングのアプローチを根底から変えることになりました。
さて、スイッチは手に入れた。
でも、いつ切り替える?いきなり全ユーザーを「New」に切り替えて、また地獄を見たら?
そう。僕らの戦いはまだ終わらない。
次回【結】。この「スイッチ」をどう安全に切り替えていくか。モノリスとマイクロサービスを「並行稼働」させ、僕らがどうやって「本当の勝利」を掴んだのか。その現実的な移行戦略(マイグレーション)の例について、お話しします。
「並行稼働」という名の安全綱。僕らが手にした「レガシーから卒業する権利」
(前回のあらすじ)
僕(ヒロ)は、WPFモノリスの「製品カテゴリ・ツリー」機能を分離するため、HttpClient でAPIを直叩きして大失敗。「一度に二つ変えるな」というサラの教えのもと、「インターフェース(ICategoryService)」という「壁」を作り、古いロジック(Legacy…Facade)と新しいAPIロジック(NewApi…Service)を差し替え可能にする「切り替えスイッチ」を手に入れた。しかし、このスイッチ、いつ、どうやって切り替えるのが正解なんだ?
「切り替えスイッチ」は手に入れた。
ICategoryService というインターフェースをDI(依存性の注入)コンテナに登録する時、LegacyCategoryServiceFacade を指すか、NewApiCategoryService を指すか。
「よし、サラ!切り替えの準備はできた。明日の朝、リリースする。DIコンテナの設定を『New』に切り替えるぞ!」
僕がそう言うと、サラはまた、あの「やれやれ」という顔で首を振った。
「ヒロ。それじゃ、あなたの『API直叩き』の失敗と、リスクの大きさが何も変わってないわ」
「え…?でも、インターフェースで分離したんだから、問題あればすぐ『Legacy』に戻せるじゃないか」
「『問題あれば』?その問題は誰が検知するの?全ユーザーが一斉にバグを踏んで、サポートデスクが炎上してから?私たちは『ストラングラー(絞め殺し)』であって、『爆弾魔(Bomber)』じゃないのよ」
「結」の核心:フィーチャートグル(Feature Toggle)という最強の「人生術」
サラが提案したのは、この移行戦略における、まさに「切り札」でした。
それは、**「フィーチャートグル(Feature Toggle)」**という手法です。
聞いたことありますか?
ざっくり言うと、「コードの中に埋め込む、機能のON/OFFスイッチ」のこと。
僕らがやったのは、ICategoryService の実装を「どっちか」に決めることではありませんでした。
ICategoryService の「第三の、そして最強の実装」を作ったんです。
C#
// SmartCategoryService.cs (第三の実装)
// こいつが「賢い」切り替えスイッチ本体になる
public class SmartCategoryService : ICategoryService
{
private readonly ICategoryService _legacyService; // 古いロジック
private readonly ICategoryService _newApiService; // 新しいAPIロジック
private readonly IFeatureToggleService _toggleService; // ON/OFFを管理するサービス
// 古いのも新しいのも、両方DIで受け取る
public SmartCategoryService(
LegacyCategoryServiceFacade legacyService,
NewApiCategoryService newApiService,
IFeatureToggleService toggleService)
{
_legacyService = legacyService;
_newApiService = newApiService;
_toggleService = toggleService;
}
// ★★★ココが核心★★★
public async Task<IEnumerable<CategoryNode>> GetCategoryTreeAsync()
{
// まず、トグルサービスに「今、新APIを使っていいか?」と尋ねる
if (_toggleService.IsNewCategoryApiEnabled())
{
// --- ONの場合 ---
try
{
// 新しいAPIを叩きにいく
return await _newApiService.GetCategoryTreeAsync();
}
catch (Exception ex)
{
// もし、新しいAPIがコケたら...(例:ネットワーク障害、APIが500エラー)
// ログだけ記録して...
Log.Error("New Category API failed!", ex);
// ★フォールバック(安全綱)★
// こっそり古いロジックを動かして、ユーザーにはエラーを見せない!
return await _legacyService.GetCategoryTreeAsync();
}
}
else
{
// --- OFFの場合 ---
// 何もせず、今まで通り古いロジックを動かす
return await _legacyService.GetCategoryTreeAsync();
}
}
}
そして、DIコンテナには、この SmartCategoryService を ICategoryService の実装として登録する。
何が起きたか。
僕らは、**「並行稼働(Parallel Run)」と「安全綱(Fallback)」**を手に入れたんです。
ViewModel(ProductCatalogTreeViewModel)は、相変わらず ICategoryService を呼んでるだけ。
その裏側で、この「賢いスイッチ」が、新しいAPIがONかOFFかを判断し、もしONで、かつAPIが失敗したとしても、自動的に古いロジックが動いてくれる。
これ、最強じゃないですか?
WPFアプリ(モノリス)の安定性を100%担保したまま、新しいAPI(マイクロサービス)を「試す」ことができるようになったんです。
実録:僕らの「リアルな移行」ステップ
この「賢いスイッチ」を握りしめ、僕らはいよいよ、現実世界での移行(マイグレーション)を開始しました。これが、僕が体験した「リアルなストラングラーフィグ」です。
ステップ1:カナリア・リリース(開発チームが毒見役)
IFeatureToggleService の中身をこう実装しました。
「もし、今ログインしているユーザーが『ヒロ』か『サラ』か『ピーター』(つまり開発チーム)だったら、IsNewCategoryApiEnabled() は true を返せ。それ以外は false を返せ」
そして、本番環境(Production)にリリース!
…案の定、僕らの画面だけ、カテゴリ・ツリーが正しく表示されませんでした(笑)。
「転」で僕がハマった「認証」の問題が、本番環境で再発したんです。
でも、被害は僕らだけ。他の全ユーザーは、今まで通り古いロジック(Legacy…)が動いているので、何も気づかない。
僕らは慌てず、NewApi…Service 側の認証ロジックを修正し、再デプロイ。
ついに、僕らの画面で「新しいAPIから取得したカテゴリ・ツリー」が表示されました。
ピーターが「…おお。動いてる。しかも、古いコードより速いぞ」と呟いた時、マジでガッツポーズでしたね。
ステップ2:パワーユーザーへの展開
次に、トグルサービスを書き換え。
「開発チーム」に加えて、「経理部のアンナさん(PCに詳しいパワーユーザー)」も true に。
「アンナ、なんかツリー表示、変なとこない?」
「ええ、特に。あ、でも前より一瞬ロードが速くなったかも?」
よっしゃ!
ステップ3:段階的ロールアウト
次に、トグルサービスをさらに書き換え。
「全ユーザーのうち、ユーザーIDの末尾が『1』の人だけ true にする」(=全ユーザーの10%)
これで1週間、APIの負荷やエラーログを監視。問題なし。
「じゃあ、末尾が『1』~『5』の人」(=50%)
…
そして、リリースから1ヶ月後。
ついに、IsNewCategoryApiEnabled() の中身を、**「常に true を返す」**に書き換えたんです。
全ユーザーが、新しいAPIを使うようになった。
古いロジック(LegacyCategoryServiceFacade)は、万が一のための「安全綱」として、まだコードの中に残っています。
僕らが手にした「本当の勝利」
僕らは、モノリスを倒したわけじゃない。
でも、モノリスの「一部(カテゴリ・ツリー)」を、安全に、誰にも迷惑をかけず、ビジネスを止めることなく、「絞め殺す」ことに成功したんです。
モノリスの中にあった古いコードは、もう二度と実行されることはない(APIが落ちない限り)。
それは事実上、「死んだ」コードになりました。
これが、ストラングラーフィグ・パターンの「結」です。
このアプローチを知って、僕のエンジニア人生は変わりました。
もう、巨大なレガシーコードを前に「どうせ無理だ」「全部作り直すまで待つしかない」と諦める必要はなくなった。
エンジニアは、「レガシーから卒業する権利」を、自分たちの手で掴み取れるんです。
このブログを読んでくれた、かつての僕と同じようにレガシーシステムに消耗しているあなたへ。
「ストラングラーフィグ」は、単なる技術パターンじゃありません。
それは、**「どうやって賢く、リスクを最小限にして、現実を変えていくか」**という、最強の「人生術」です。
- **「ビジネス境界」**で切り出せる、小さな獲物(ローリスク・ローリターン)を見つけること。
- **「インターフェース」**という壁で、古い実装と新しい実装を「ラップ」すること。
- **「フィーチャートグル」**という安全綱を使い、古いものと新しいものを「並行稼働」させること。
これさえ知っていれば、あなたはもう「コードの奴隷」じゃない。
巨大なモノリスを少しずつ、確実に「飼いならす」ことができる、**「レガシー・テイマー(Legacy Tamer)」**です。
僕らの戦いはまだ始まったばかり。モノリスはまだデカい。
でも、僕らは最初の「ツル」を巻き付けた。
次は、どの機能を「絞め殺し」に行こうか。ピーターとサラと、次の「獲物」を探す僕らの目は、もう絶望していません。

コメント