
Google Analytics
イベント
マガジン
技術ブログ
こんにちは。 ファインディ株式会社でテックリードマネージャーをやらせてもらっている戸田です。 生成AIが開発現場に入り込んでから1年あまり。Claude CodeやGitHub Copilotなどのエージェント型ツールも一般的になってきました。 その一方で、「AIを導入したのに、思ったほど速くなっていない」「むしろレビューが大変になった」という声を、社内外でよく聞くようになりました。 そんな中で先日、弊社主催の「AI Engineering Summit Tokyo 2026」にて 「速く作る」から「正しく作る」へ ─ 生成AI時代の開発フロー改革のロードマップと実行 ─ と題して登壇してきました。 ファインディ社内で1年強かけて見えてきた「AI導入の落とし穴」と、そこから組み立てた3段階のロードマップを共有する内容です。 ai-engineering-summit-tokyo.findy-tools.io この記事では登壇内容を振り返りつつ、AI導入の効果が伸び悩んでいる組織に向けて、ファインディがどのような順番で開発フローを作り変えてきたかを紹介します。 それでは見ていきましょう! AIを入れたのに、アウトプットは伸びていなかった 「速く作る」だけでは限界がある AI活用レベル レベル 1:AIエージェントでコード生成 レベル 2:AIエージェントでモノを作る レベル 3:AIで価値を生み出す まとめ AIを入れたのに、アウトプットは伸びていなかった ファインディも同様に生成AIの本格活用を進め、Claude CodeやCodexなどのAIエージェントが日常的な開発フローに入り込んできました。社内の体感としては「1人あたりのPR作成数も増えていそうだし、開発のリードタイムも短くなっているはず」というものでした。 しかし、Findy Team+で1年分の数値を計測してみると、想像とは違う景色が見えてきました。 まずポジティブな変化として、PR作成総数は前年比で伸びていました。ただ、その内訳を見ると、稼働メンバー数が約1.5倍に増えていたことが大きく、1人あたりのPR作成数はほぼ横ばいだったのです。 さらに、レビュー開始からApproveまでの時間は前年比でおよそ20分延び、PR1本あたりの平均コメント数・レビュー数も約30%増えていました。AIによるコード生成が増えた一方で、レビュー側の負荷が確実に積み上がっていたわけです。 シニア層と若手層で傾向を分けて見ると、もう一段深い構造が見えてきました。AIの出力を読んで検証できるシニアメンバーはアウトプットが上がる一方、経験年数が浅いほどAI出力の合否判断に苦戦する傾向があり、結果としてレビュー側に判断の負荷が集中していました。 組織全体として、 AIに使われている 状態に近かったとも言えます。「体感と事実がズレているかもしれない」と疑い、各種数値を可視化していたからこそ、実は「1人あたりのPR作成数は増えておらず、それどころかレビューの負荷が増えており、結果的に開発のリードタイムは長くなっていた」ことに気づくことができたのです。 「速く作る」だけでは限界がある 計測結果から見えてきたのは、「コードを書く速度が上がっても、ボトルネックがレビューに移り変わったために、全体のリードタイムは短くならなかった」、ということでした。 AIでコードを書くスピードは確かに上がります。一方で、内容を十分理解せずに生成するケースが増えると、PRの一貫性や正確性が落ちます。 指摘の量が増えると、リードクラスのレビュー時間が膨らみ、リードタイムが悪化します。最終的に、トータルのアウトプットはAI導入前とほぼ変わらない、という結果に着地します。 AIの成果物に対する確認や検証に時間がかかるようになり、レビューの負荷が増える。これがAI導入の落とし穴の一つです。そのため、AIの成果物の品質を再現性高く担保するための仕組みを整えることが必要になります。 そのために必要になるのが、「正しい作り方と手順」を仕組み化することです。馬を御する馬具(ハーネス)になぞらえ、AIの動きを暴走させず目的の方向へ導くための仕組みを整えることを ハーネスエンジニアリング と呼びます。 このハーネス化を、ファインディは 開発フロー改革 として進めてきました。具体的には、AI活用のレベルを3段階に分け、土台から段階的に積み上げていくロードマップを描いています。 AI活用レベル 開発フローを分解し、AIで何を肩代わりできるかをマッピングすると、3つのレベルが浮かび上がりました。 レベル 1:速く作る :コード変更とPR作成を中心に、AIで代替できる範囲を自動化する レベル 2:正しく作る :タスク分解とIssue作成までAIに任せ、「正しく作る」仕組みを整える レベル 3:必要なものを作る :要件定義やQAという「AIで代替しづらい」とされてきた領域に踏み込む ポイントは、「どれか1つを単独でやる」のではなく、Lv1 → Lv2 → Lv3 と段階的に積み上げて初めて効果が出るという点です。ここから各レベルを順に見ていきます。 レベル 1:AIエージェントでコード生成 レベル 1の目的は、コード変更とPull request作成までをAIエージェントに任せ、人間は本質的なレビューに集中することです。 このフェーズで真っ先に向き合う必要があるのは、「AIが出したコードの責任は誰にあるか」という問いです。 どれだけ自動化されても、AIが出力したコードの責任は人間にあります。品質と判断の最終責任は人間が引き受けることになります。 その前提の上で、レベル 1の工程を整理してみました。 コード変更とPull request作成はAIに全て任せることが出来ます。そしてレビューに関しては、コードの責任という観点から人間が行う必要があると考えていました。しかし、本当にそうなのか疑問に思いました。 自分自身がコードを書いてPull requestを作成していた頃を思い出してみてください。自分でコードを書いて、Pull requestを作成する。レビュー依頼を出す前にやっていたことがありました。セルフレビューです。 そしてセルフレビューで気づく内容と、実際に他のメンバーからもらうレビューの内容は観点や内容が違います。これを今回の開発フロー改革に当てはめました。 レビューをセルフレビューとレビューに分け、レベル 1ではセルフレビューまでをAIに任せることにしました。セルフレビューではコード変更そのものに対するレビュー、レビューでは人間が最終判断しないといけない内容にフォーカスしてレビューというように切り分けることにしました。 ここで重要なのは、AIを入れる前提として「AIと関係なく当たり前のこと」が揃っている必要がある、という点です。 アーキテクチャ・命名規則・型定義といったコード設計、十分なテストカバレッジ、一貫した設計パターン、そしてPRの適切な粒度・レビュー文化・タスク分解の習慣。 これらはAI以前から品質を保つために必須でしたが、AIエージェントが入ると一気に効いてきます。土台が弱いと、AIはその弱さを増幅する方向に働くからです。 ファインディがこの「土台」をどう積み上げてきたかは、次の記事で詳述しています。 tech.findy.co.jp その土台の上に、AIが参照するドキュメントとルールをガードレールとして整備します。 READMEやプロジェクトドキュメントで開発前提・アーキテクチャ・運用ルールを記述し、AGENT.mdやrulesでコード規約・命名規則・テスト方針をAIに自動参照させ、カスタムコマンドやプロンプトテンプレートで依頼タスクを規格化する。 この整備があって初めて、AIは使い物になるコードを出力してくれます。 ファインディではレベル 1を支える仕組みとして、Claude CodeのSkillを複数組み合わせています。代表的なものは次の通りです。 Pull request作成:typecheck/lint/test/buildといった品質チェックの自動実行、ブランチ命名規則の強制、Conventional Commitに沿ったコミット生成、PRテンプレートからのbody自動生成までを1コマンドで実行 Pull request作成前の自動セルフレビュー:セキュリティ/コード品質/規約準拠/Simplify観点/要件検証/チェックリスト照合の6観点で並列分析。信頼度の高い指摘のみを報告してノイズを抑制し、2026年4月時点で1500以上のPRで運用中 AI併用レビュー:Codex CLIを別系統として並行運用し、メインAIのレビューと統合してPRコメントに提示。AIの偏りに依存しない複眼チェックを実現 定期セルフレビュー自動化:平日の朝方にGitHub Actionsで起動し、直近1ヶ月変更されていない技術的負債となりうる既存コードに対して修正Pull requestを自動作成 チェックリスト自動更新:過去レビューコメントをGitHub APIで収集し、LLMで指摘パターンを分類してチェックリストへ反映。レビュアーの暗黙知をSkillに形式知として残す セルフレビュー周りの仕組みについては、それぞれ次の記事でも紹介しています。 tech.findy.co.jp tech.findy.co.jp これらは1リポジトリにSkill/Sub Agent/MCPとしてまとめており、Pluginとして運用することで /plugin install によるワンコマンド配布を実現しています。全員がcontributeできる構造にすることで、改善がそのまま組織全体に反映される回り方になっています。 レベル 2:AIエージェントでモノを作る レベル 1で「速く作る」の足回りが整うと、次にぶつかるのが「要件をどう実現するか」の手順自体がAIフレンドリーではない、という壁です。タスクの粒度や手順を誰も明示的に決めていないため、生成AIに何を渡せば精度よく動くかが属人化していました。 ここで必要になるのが、「作りたいもの(What)」と「作り方の設計図(How)」を分離して扱う発想です。 Whatをタスク分解の形でHowに落とし込み、それをAIに渡せば、AIはそのステップどおりに実装してくれます。そしてレビューでは、出来上がったコードよりも先に「作り方と実現方法が合っているか」を検証し、設計図のほうにフィードバックする。タスク分解の品質が、そのままアウトプットの品質を決める構造です。 このフェーズでは、AIとの関係性が「協働」から「委任」に変わります。 Vibe Codingが「AIは隣で並走するパートナー」だとすると、Agentic Workflowは「AIは自走する実行エージェントで、人間はその指揮者」になります。 任せる粒度も、1行〜1関数のレベルから、タスク/PR/フロー全体へと拡張されます。 Agentic Workflowの定義として4つの自律性を意識しています。 ゴール指向 :「何を」を与え、「どう実現するか」はAIが組み立てる 計画と分解 :大きなタスクをサブタスクに分けて順序付けて実行 ツール使用 :ファイル・Skill・コマンド・検索・MCPを能動的に使う 自己検証ループ :テスト失敗→修正→再実行を自律的に繰り返す 興味深かったのは、AI委任の前提が変わると開発環境そのものが変わったことです。 2026年に入ってから、ファインディではコード生成のメインツールがIDEからターミナルへ変化しました。 1ウインドウで1タスクずつ進めるのではなく、複数ウインドウ・ペインで同時にAIへ委任するスタイルになったため、並列委任しやすい場所として、ターミナル+tmuxのような構成に自然と寄せていく流れになっています。IDEの役割はコードを書く場から、広域に渡るコードリーディングや理解を深める場へとシフトしています。 このレベル 2を支えるのが、要件構造化&Issue自動生成のSkillです。次の6ステップで動きます。 要件理解 ─ インタラクティブな質問で曖昧さを解消 コード探索 ─ 並列の探索Agentが複数観点で同時調査 要件明確化 ─ 不足情報を補完してスコープを定義 設計提案 ─ 実装方針のドラフトを生成 タスク分解 ─ 実装単位に分解(粒度判定Skill連携) Issue作成 ─ Sub Issue/relationshipを含む構造化Issue このSkillで生成したIssueは累計3000以上にのぼり、親Issue(Feature)→子Issue(DB層→API層など)が blocked_by の依存関係付きで自動構成されます。 例えば「ユーザー通知機能の追加」という親Issueに対し、「#1 DB層:通知テーブル追加」「#2 POST API追加」「#3 DELETE API追加」のような子Issueが、依存関係込みで一気に並ぶイメージです。 実装フェーズでは、これを「Issue × Worktree × Agent」の並列モデルで走らせています。Team Lead Agentが blocked_by に従ってLayerごとにWorker Agentを起動・同期し、同じLayer内はworktreeを切ってWorker Agentが完全並列で実装する。Layer 0でDB層が完了したら、Layer 1のPOST/DELETE APIを2つのworktreeで同時に進める、といった動かし方ができます。 この並列モデルの詳細は次の記事で解説しています。 tech.findy.co.jp コードレビューの分担も、レベル 2では明示的に再定義しています。 担当 レビュー領域 AI コード規約・命名、型定義、テストコード・テストケース 人間 ビジネスロジックの要件適合、アーキテクチャや設計、データベース構造、明確なセキュリティリスク 視点はコードそのものから抽象的なところに寄せていきます。 結果として、レベル 2の導入後、1人あたりのPR作成数は前年比で1.5倍を超えました。AIフレンドリーな「設計図(タスク分解+構造化Issue)」を誰でも作れる状態になり、作りたいものを再現性高くアウトプットできるようになった、というのがその答えです。 レベル 3:AIで価値を生み出す レベル 2まで進むと、開発スピードに対する次のボトルネックが見えてきます。「何を作るか」の上流が詰まり、せっかく整えた実装力を活かしきれない状態です。 具体的には、要件の実現可能性を調査できるのがエンジニアだけになっていたり、システムとプロダクトの概念が離れていて、お互いを十分知らないまま施策や検証が進んでしまったりします。 レベル 3の目的は、要件定義(PdM領域)とQA領域というAIで代替しづらいとされてきた領域に、AIで踏み込むことです。 レベル 3の起点になるのが 現状把握 です。現状把握の対象は広い範囲に及びます。コードベース・Google Analytics・プロダクト文書・GitHub Issues/PR・Datadog・各種KPIなど、必要なコンテキストは多岐にわたります。 まず要件定義では、これらを毎回手動で集めるのは現実的ではないため、専門Agentチームが各ソースから必要な分だけ自動収集する仕組みを組みました。 ファインディの要件定義Skillでは、7つの専門Agentが並列で動きます。 目的・成果分析 ─ WHY/WHAT仮説の自律生成 データ・コンテキスト収集 ─ GitHub Issues・Notionから数値収集 プロダクト文書抽出 ─ docs/配下のKPI・ポリシーを抽出 コードベース分析 ─ リポジトリの制約・パターンを分析 スコープ分割 ─ MVPと拡張項目に分割 技術的実現可能性評価 ─ 解決アプローチの実現可能性を評価 アクセス解析データ収集 ─ Google Analyticsから自動収集 これらのAgentがAgentTeamsとして並列で稼働し、お互い会話しながら必要な情報を集めて分析します。 ユーザーは分析結果を修正・補足し、最後にAIが構造化&品質チェックしてGitHub Issueとして出力します。ユーザー操作は入力・レビュー・承認の3回のみで、それ以外はAIが自律的に進める設計です。出力されたWHY/WHAT構造化済みIssueは、そのままレベル 2のIssue自動分解Skillに連携できます。 もう1つの挑戦がQA領域です。ユーザビリティ・アクセシビリティ・UI/UXといった非機能要件のテストはAI単独では難しいため、AIで代替しづらい領域です。ファインディは「代替」ではなく「支援」、つまりAIがQAエンジニアの判断を最大化する形を仮説にしています。 次にQA領域は3つのSkillで一気通貫にしました。仕様ソース(Issue/Figma/Notion)を入力に、次の流れで進みます。 QA観点抽出:観点を自動抽出してQA観点mdを出力 QAテストケース生成:観点→ステップ/期待結果/前提条件に展開してMarkdown+CSV化 QA自動実行:Playwright MCP経由でClaude Codeから直接ブラウザを操作し、Pass/Failとスクリーンショット付きのレポートを出力 QA観点抽出では、仕様分析/画面構造・UX探索/影響範囲判定の3つのAgentが並列で動き、観点設計のたたき台を数分で生成します。 テストケース生成では、観点→具体ケースへの落とし込みにかかる工数が数時間から数分に短縮されました。生成されたQAリストは、認証・認可/入力バリデーション/表示・UI/ファイル操作/外部連携/メール送信の6軸で共通基準と照合し、観点漏れ・粒度のばらつきを検出します。 人間が集中すべきは、ユーザビリティ評価、例外シナリオ、クライアント要件の確認といった「判断が必要な領域」です。反復可能なケースはAIが淡々と実行し、失敗時はスクリーンショット付きレポートで原因特定が速くなりました。 まとめ AI活用のレベルをレベル 1(速く作る)/レベル 2(正しく作る)/レベル 3(必要なものを作る)と分けてきましたが、最大の主張は、これらは「どれか1つ」ではなく段階的に積み上げて初めて成立する、ということです。 そして、ここで強調したいのが 順番を間違えない ことです。土台が弱いと、ガードレールもAI Skillも成果を出せません。ファインディが踏んだ順番は次の4段でした。 裏返すと、AIエージェントを入れる前にやっておくべきことは、AI以前から変わっていません。統一規約、テストコード、PR粒度、レビューなどの開発文化といった 基本の徹底 こそがAI活用における大前提です。 AI時代の本丸は、「速く作る」ではなく、「正しく作る」「必要なものを作る」への段階的越境です。あなたの組織が今どのレベルにいるか、そして次のレベルへ進むためにどの土台が弱いかを確認する目安として、このロードマップが役立てば幸いです。 ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。 herp.careers
はじめに こんにちは、ZOZOTOWN開発1部iOSブロックの荻野です( @juginon )。 みなさんに日々使っていただいているZOZOTOWN iOSアプリのホーム画面ですが、実は2024年秋から2026年の年初まで約1年半、水面下でリアーキテクチャを行っていました。 リアーキテクチャに着手する前の当時の私はアーキテクチャ設計への理解がまだ浅く、「実際に手を動かしながら身につけたい」という動機でこのリアーキテクチャを主導しました。自分にとってはチャレンジングな取り組みで、アーキテクチャ設計やテスト設計への理解が実践を通して大きく深まったプロジェクトになりました。 本記事では、そのリアーキテクチャのすべての軌跡と、そこで得た学びをお伝えします。 なお、本記事で紹介するホーム画面リファクタリングは、iOSチーム全体で取り組んでいるアーキテクチャ刷新の具体的な事例の1つでもあります。チームとしての取り組みや知識共有の仕組みについては ZOZOTOWNのiOSアーキテクチャとチーム進化の軌跡 にもまとめています。本記事と合わせて読むと、個々の取り組みとチーム全体の文脈をより立体的に理解できます。 目次 はじめに 目次 ホーム画面について タブ モジュール ホーム画面が抱えていた課題 当初の設計と、その後の運用実態の乖離 継承構造が不要になった ログ管理の複雑化 MVCによるViewControllerへの責務集中 高い改修頻度 リファクタリングの設計方針 方針1: 影響範囲を最小化しながら段階的に進める 方針2: 段階的に責務を分離する Step1: Objective-Cレガシー型への依存を剥がす Step2: 最も独立性の高いAPIを対象に、ViewModel/UseCaseを部分導入する 小さく始めることの重要性 Step3: MallHomeViewController全体にViewModel/UseCaseを導入する 問題1. Swift Concurrencyへの移行 問題2. モジュール構築メソッドの整理 Step3完了後にログに関するバグが発覚 バグを引き起こした原因 Step3-ex: 命名整理とユニットテストの追加 命名の整理 ユニットテストの追加 不確かさに気づいた時点でテストを書く Step4: HomeViewControllerにViewModel/UseCaseを導入する TDDによる設計の共有 オンボーディング周りの状態管理 リファクタリング前の課題 ステートマシンによる再設計 長期リファクタリングを進める上でのポイント おわりに ホーム画面について ZOZOTOWN iOSアプリのホーム画面は以下のように、主にタブとモジュールによって構成されています。 タブ 画面上部に表示されている「すべて」「コスメ」部分を指します。タブは切り替えが可能で、すべてタブではアパレル・シューズ・コスメ等すべての商品が、コスメタブではコスメ商品特化の画面表示になります。 実装上は以下の2種類のViewControllerで構成されています。 HomeViewController : ホームタブのルート画面となる画面全体を管理するViewController ヘッダーや検索窓など、両方のタブで共通して表示する部分、ホーム画面全体の管理を担う MallHomeViewController : すべてタブ/コスメタブのコンテンツを管理するViewController それぞれのタブで表示が変わる部分の管理を担う モジュール 各タブのコンテンツは、複数の「モジュール」と呼ばれるブロックが縦に並んだ構成です。モジュールとは、性別選択・バナー・チェックしたアイテムといった、個々のコンテンツ単位のことです。 ユーザーがホーム画面をスクロールすると、これらのモジュールが順番に表示されます。 ホーム画面が抱えていた課題 当初の設計と、その後の運用実態の乖離 ホーム画面の複雑さを理解するには、2021年のフルリニューアル時の背景を知る必要があります。 2021年3月のZOZOTOWNフルリニューアルで初めてタブ構成が導入されました。当時は3つのタブがあり、 MallHomeViewController を基底クラスとした3つのサブクラスによる継承構成を採用しました。各タブで固有の処理が発生することを見越した設計です。 当時の取り組みについては ZOZOTOWNアプリ Home画面のリニューアルにおけるアーキテクチャ再設計 でも詳しく紹介されています。 しかし、フルリニューアルから3年以上が経過し、運用を重ねる中で当初の設計前提が変わっていきました。 継承構造が不要になった 従来では MallHomeViewController を継承する各タブのクラスを作成していましたが、各タブで固有の処理は実際にはほとんど発生しませんでした。 タブの種類を保持するだけで十分な状態で、各タブで専用のクラスを作成する構造はかえって全体像の把握を難しくしていました。 ログ管理の複雑化 リニューアル当初はGA(Google Analytics)のみだったログ送信を専用のLoggerクラスが管理していました。しかしその後、社内分析用ログなど複数種別のログが追加されていく中で、Logger自身が複雑な状態管理を担うようになっていきました。 複数のフラグがLoggerの内部に積み重なり、 MallHomeViewController が持つ状態と常に同期させる必要が生じました。また、ログに関する責務分離が適切に行われていない部分もあり、こういった構造がコードを読む際のコストを高める要因の1つになっていきました。 MVCによるViewControllerへの責務集中 2021年当時はMVCアーキテクチャを採用していたため、API呼び出し・UI状態管理・ビジネスロジックの調整が MallHomeViewController に集中していました。前述のLoggerクラスとの状態同期もVCが直接担っており、改修を加えるたびにVC・Logger双方への影響を考慮しなければなりませんでした。こうした積み重ねで行数は再び1000行を超えるまでに膨らんでしまっていました。 特に問題だったのは、UICollectionViewへのデータ構築と商品押下時のログデータ作成が混在する500行弱の巨大なメソッドです。どこを触れば何が変わるのか把握するだけで大きなコストがかかる状態でした。 高い改修頻度 ZOZOTOWNのホーム画面は平均月1ペースで改修案件が入り、多い時期には3案件が同時並行で走ることもあります。 このリアーキテクチャが開始してから現在まででも、ホーム画面のモジュールを無限スクロールできる機能や、モジュール内のアイテムで動画を表示する機能など、規模の大きな案件がリリースされています。 影響範囲の把握が困難なFat ViewControllerは、改修のたびにリスクを伴い、チームの開発速度を下げる原因になっていました。 リファクタリングの設計方針 課題は明確でしたが、1000行超のVCを一気に書き換えるのはリスクが高すぎます。そこで以下の方針を立てました。 なお、このリファクタリングは通常の機能開発と並行して進めており、稼働の約2割をこの取り組みに充てながら進めていました。1年半という期間はそのためです。 方針1: 影響範囲を最小化しながら段階的に進める 各ステップの影響範囲を小さく保つことで、問題発生時の修正コストを抑えられ、PRの変更量も少なくなりレビューの負担を減らせます。また各ステップを独立してリリース可能な単位とすることで、他案件の進行をブロックしません。 以上のメリットを意識しながら以下のステップで進める計画を立てました(当初は4ステップ、結果として5ステップになりました)。 ステップ 内容 Step1 Objective-Cレガシー型への依存を剥がす Step2 最も独立性の高いAPIを対象に、ViewModel/UseCaseを部分導入する Step3 MallHomeViewController全体にViewModel/UseCaseを導入する Step3-ex Step3完了後にバグが発覚し、命名整理とユニットテストを追加 Step4 HomeViewControllerにViewModel/UseCaseを導入する ステップを設計する上でのポイントを3点紹介します。 Step1を最初に行った理由 MallHomeViewController にはObjective-Cのレガシーな型への依存がありました。MVVM化を先に進めると、ViewModel/UseCaseはObjCの型を扱う設計になります。その後ObjC依存を除去すると、ViewModel/UseCaseの設計変更も必要になり手戻りが発生します。そのため、MVVM化の前段階として依存の除去を最初のステップとしました。 MallHomeViewControllerから先に着手した理由 タブの中身を管理している MallHomeViewController は、着手開始から間もなく後続案件の改修が入る予定でした。そのため、それより前にMVVM化を完遂させることを優先しました。 Step2とStep3を分けた理由 ホーム画面では複数のAPIを呼び出しており、最初から全APIを対象とするとMVVM化の影響範囲が大きくなりすぎます。まず独立性の高い一部のAPIに絞ってViewModel/UseCaseを導入することで、アーキテクチャの全体像を小さな変更で確認でき、問題が発生した際の修正コストも抑えられます。 方針2: 段階的に責務を分離する UseCase → ViewModel → ViewControllerの順で責務を分離していき、最終的に以下の構成を目指しました。当時のアーキテクチャガイドラインではUseCaseの採用が定められていました。またAPIリクエスト・ログ送信・ビジネスロジックが複合的に絡むホーム画面の規模感においても、ViewModelの肥大化を防ぐうえで適切な設計判断でした。 ここで紹介している大まかな全体方針は、以前チームメンバーの なんしー さんが行った 商品詳細画面のリアーキテクチャにおける進め方 を参考にしています。 Step1: Objective-Cレガシー型への依存を剥がす MallHomeViewController では、商品情報を表示する部分がObjective-Cで書かれたレガシーな型に依存しており、APIレスポンスからレガシーな型へ変換する不要な依存がありました。そのため、最初のステップはMVVM化でなく 不要な依存の除去 から始めました。 以下の3段階で依存を剥がしました。 商品の情報表示において必要な情報を持つUIModelを作成 APIレスポンスをそのUIModelに変換するTranslatorを作成 Translatorは外部APIのレスポンス型をUIModelの型に変換する責務を持つ 外部APIの型定義が変更されてもViewModelやVCへ直接影響しない構造になる レガシーな型を使わない新しいセルを実装し、移行 最終的に MallHomeViewController からObjective-Cレガシー型への依存を完全に除去しました。 Step2: 最も独立性の高いAPIを対象に、ViewModel/UseCaseを部分導入する Step1でクリーンな基盤ができたため、いよいよMVVM化に着手します。設計計画で「最も独立性が高い」と判断した 世代別ランキングモジュール から始めました。 世代別ランキングモジュールとは、ユーザーが世代(~10代、20代など)を選択すると、その世代の人気アイテムがランキング形式で表示されるモジュールです。 ヘッダーの世代選択ボタンをタップして切り替えると、対応するランキングが再取得・再表示されます。 以下の特徴があったため、ホーム画面のMVVM化における最初のステップとして工数がかからず、アーキテクチャの全体像を実装しながら理解できる最適な題材と判断し、着手しました。 世代別ランキング専用の独立したAPIを持つ ユーザーが世代を選択したときだけ更新される 他のモジュールの更新と独立して動作する 小さく始めることの重要性 Step2は全部で7つのPRを作成しました。UseCase作成→UIModel作成→ViewModel作成→ViewControllerからUseCase/ViewModelへ処理を移動する流れで修正を加えていきました。 巨大なViewControllerを一気に書き換えようとすると、変更が大きくなりすぎてレビューが困難になり、バグ混入リスクも高まります。Step2でOpenした7つのPRのほとんどが100行未満のコード追加に収まっており、レビューでの指摘もほとんどなくスムーズにマージできました。 また、Step2を通して PRの分割方法 や 変更を加えるレイヤーの順番 が明確になり、次のステップであるモジュール更新全体のリアーキテクチャへの自信がつきました。大規模なリファクタリングに着手する際は、最も独立性の高い部分から始めることで、レビューでの問題検知やバグ混入の防止に直結します。最初の小さなステップを通じてPRの分割方法や変更を加えるレイヤーの順番を把握しておくと、後続の大きなステップをより自信を持って進められます。 Step3: MallHomeViewController全体にViewModel/UseCaseを導入する ホーム画面では、世代別ランキングモジュールの取得API以外に合計4つのAPIを並行して呼び出しています。Step3ではそれらの主要APIを呼び出している部分すべてにViewModel/UseCaseを導入しました。Step3はStep2のようにスムーズには行かず、いくつかの問題に直面しました。代表的な問題を紹介します。 問題1. Swift Concurrencyへの移行 当時の MallHomeViewController では、一部分のAPI呼び出しに BrightFutures を使っていました。このライブラリは2022年にEOLとなっており、チーム内でも新規実装では非推奨としていたため、このタイミングでSwift Concurrencyへ移行しました。 Swift Concurrency対応に関してもこのときが初めての経験で、その中で色々と学びがありました。 並行処理によるビュー表示時の表示順担保 クロージャベースのコードでは、複数のモジュール取得APIを直列で呼び出しており、すべてのレスポンスが揃ってから一括で描画していました。Swift Concurrencyへ移行して並行呼び出しにしたことで、どのAPIレスポンスが先に返ってくるかが不定になります。レスポンスを受け取った順にUIModelを積んでいく実装のままでは表示順が変わってしまいますが、実装当初はこの問題に気づいていませんでした。 UIModelの配列に常に決まった順序で格納する実装に修正することで解決しました。すべてのAPIレスポンスが揃ってから正しい順序でまとめて描画するという基本的な流れは変わらず、並行取得による速度改善と表示順の保証を両立しています。 withCheckedThrowingContinuation にキャンセルが伝播しなかった 特定のAPI呼び出しにはタイムアウト処理が必要でした。 withThrowingTaskGroup を使い、 データ取得タスク と 一定時間後にタイムアウトエラーを投げるタスク を並走させました。どちらかが完了したら group.cancelAll() でもう一方をキャンセルする実装を採用していました。 しかし実際にはキャンセルが正しく機能していませんでした。通信が切断された状態でリロードを繰り返すと、タイムアウトが発生して group.cancelAll() が呼び出されているにもかかわらず、ローディングが永遠に続く不具合が発生していました。 原因は、コールバック型のサードパーティSDKを withCheckedThrowingContinuation でブリッジしていた部分にありました。このSDKは通信切断時にコールバックを呼び出さない場合があります。タスクグループのキャンセルは withCheckedThrowingContinuation 内には自動で伝播しません。コールバックが呼ばれない限り、continuationは解決されないままとなります。 // 修正前: キャンセルが continuation に伝播しない func fetchData () async throws -> Response { try await withCheckedThrowingContinuation { continuation in legacySDK.fetch { result in // 通信切断時はここが呼ばれない場合がある // group.cancelAll() されても continuation は resolve されないまま continuation.resume(with : result ) } } } // 修正後: withTaskCancellationHandler を追加し、キャンセル時に continuation を resolve する func fetchData () async throws -> Response { let holder = ContinuationHolder() return try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in holder.continuation = continuation legacySDK.fetch { result in holder.continuation?.resume(with : result ) } } } onCancel : { // タスクがキャンセルされたとき、onCancel でエラーを投げて continuation を解決する holder.continuation?.resume(throwing : APIError.cancelled ) } } 対応方法は withTaskCancellationHandler を追加することでした。タスクがキャンセルされると onCancel クロージャが呼ばれ、そこでcontinuationにエラーを投げることで、コールバックが返ってこない状態でもタスクを終了できます。continuationへの参照を class で保持しているのは、 onCancel クロージャが別コンテキストで実行されるためです。 var ではSwift Concurrencyの警告が出ます。 withCheckedThrowingContinuation はコールバック型APIを async/await に変換する手段として有効ですが、タスクキャンセルは自動では伝播しません。キャンセルに対応させるには withTaskCancellationHandler と組み合わせて、 onCancel 時に明示的にcontinuationを解決する必要があります。 問題2. モジュール構築メソッドの整理 Step3の終盤では、ViewControllerに置かれていた500行弱の巨大なモジュール構築メソッドを整理しました。 このメソッドには2つの責務が混在していました。 UICollectionViewに表示するデータソースの作成(VC側の責務) 商品押下時のログ送信に必要なモジュール内位置情報の計算(VM側の責務) 後者をViewModelへ移動し、各モジュールの同一性比較を可能な構造とすることで、位置情報を適切に取得できるようにしました。 やること自体は一文で書けるようにとてもシンプルなものです。実装当時の自分の認識も同様で、この整理に関してはスムーズに進み、そのままStep3をリリースしました。 しかし、ここで今回のリアーキテクチャにおける最大の壁にぶつかってしまいます。 Step3完了後にログに関するバグが発覚 Step3のリリース後、モジュールを管理しているチームから「カルーセルバナーのタップログで、バナーの位置が正常に送られていない」という問い合わせが届きました。 調査の結果、カルーセルバナーのタップ時のログに含まれる「バナーの位置」として、 ホーム画面全体におけるセクションの表示位置 を誤って送信していたことが判明しました。本来送るべき値は カルーセル内のバナーの位置(何枚目のバナーか) でした。 バグを引き起こした原因 モジュール/セクション/インデックスなどの位置に関する命名の曖昧さ ホーム画面は複数のコンテンツを縦に並べた構成です。「画面上の表示順(セクション位置)」と「各コンテンツ内の位置(インデックス)」という2種類の"位置"が存在しますが、コード上でこれらを区別する命名が不明確でした。 APIから取得したレスポンス名/ログ送信用パラメーター名/内部で使用している変数名のそれぞれの使い分けが曖昧なまま実装を積み重ねており、コードを読む際に混同しやすい状態でした。 ログの値の正しさをテストで検証できていなかった 「ログが送信されること」は手動確認で検証していましたが、「送信されたログの値の正しさ」まで検証できていませんでした。 当時はユニットテストが整備されていなかったため、コードレビューだけでは防ぎきれませんでした。ユニットテストがあれば、このバグはリリース前に検知できたはずです。 これらを検知できなかった背景として、Swift Concurrency対応での想定外の工数による焦りと、ログの重要度を甘く見積もっていたことが挙げられます。 Step3の終盤のPRはStep2とは打って変わって500行を超える大きなPRになってしまい、レビュアにも大きな負担をかけてしまいました。「小さく分割して進める」という当初の方針を貫けなかった点も反省の1つです。 Step3-ex: 命名整理とユニットテストの追加 バグを迅速に修正した後、Step3の延長として命名の整理とユニットテストを追加しました。 命名の整理 Step3でのバグ原因の1つが「ログ送信コードの読みにくさ」にあったため、まず命名を整理してからテストを書くという順序を選びました。 モジュール・セクション命名の統一 UICollectionView上の概念の呼び方と変数の型を整理し、「モジュール」と「セクション」の使い分けルールを明確にしました。 ログ送信の位置情報に関する命名統一 セクションの表示位置とセクション内の商品位置を表す変数名を、それぞれ明確に区別できる名前に統一しました。 ユニットテストの追加 バグを引き起こしてしまったログ送信時のセクション位置に関するテストをはじめとして、モジュールの取得、性別変更、画面遷移、ライフサイクルイベントなど多数のシナリオをカバーしました。Step2, Step3でUseCaseをプロトコルでDIできる構造になっていたため、Mockを使ったテストが書けるようになっています。 ユニットテストを新規で書いていくのも初めての経験だったため、テストに関する知識が豊富なチームメンバーにモブレビューを行ってもらいました。 命名整理とテスト追加を終えた時点で、MallHomeViewModelのテストカバレッジは38%から99%に向上しました。 不確かさに気づいた時点でテストを書く Step3では「アーキテクチャを整備してからテストを書けばいい」という考えからバグを引き起こしてしまい、その考えの危うさを実感しました。バグや不確かさに気づいたタイミングでテストを書くことで、結果的に次のステップを安心して進める力になります。 Step4: HomeViewControllerにViewModel/UseCaseを導入する 最終ステップのStep4ではホーム画面全体を管理している HomeViewController のリアーキテクチャを行いました。このステップでは、Step3までの失敗と学びを活かして TDD(テスト駆動開発) を採用しました。また、Step3でのPR分割の粒度ミスを踏まえ、レビューしやすい粒度でPRを作成しレビュアへの負担も考慮したPR戦略を取りました。 TDDによる設計の共有 Step4で特筆すべきは、 UseCase/ViewModelのテストケースをProtocol/実装より先に作成した ことです。UseCase/ViewModelのテスト雛形作成 → テストケースの作成 → UseCase/ViewModelとProtocolの作成 → 実装、という順番で進めました。 このTDDアプローチが特に威力を発揮したのが、 ログ送信周りの仕様整理 でした。 HomeViewController のログ送信ロジックは複雑で、起動経路(通常起動・プッシュ通知・Deeplink)やタブ切り替えに応じてどのログをどのタイミングで送るかが変わります。また、同じ画面遷移でも複数のライフサイクルイベントが連続して発火するため、ログの二重送信を防止する制御も必要です。このような仕様では実装者ごとに解釈が分かれやすく、Step3と同じ轍を踏む可能性もありました。 そこで実装に先立ち、起動経路ごとのログ送信フローをドキュメントとして整理し、 チームで仕様を合意した上でテストケースを設計する というプロセスを踏みました。ドキュメントには どの動線でどのログが何回送られるべきか を網羅的に記述し、それをそのままテストの仕様として共有しました。 テスト設計において重要な方針として、 内部のフラグ状態ではなくユーザーの動線単位でテストを記述する ことを採用しました。例えば以下のようなシナリオをそのままテスト名として記述しています。 通常のアプリ起動でホーム画面を表示したとき、ログが1度だけ送信されること プッシュ通知でアプリを起動したとき、特定のログは送信しないこと Deeplinkでホーム画面に遷移したとき、viewWillAppearでのログ送信はスキップすること 「どの動線で何が起きるべきか」という形でテストを書くことで、テストが仕様書として機能するようになります。内部実装がリファクタリングで変わっても、動線ベースのテストはそのまま維持できるため、保守性も高まりました。 テストを先に書くことで、「このUseCaseは何をすべきか」をチームで議論しながら設計を進めることができました。Step3でロジックの漏れがバグにつながったという反省が、ここで活きています。 オンボーディング周りの状態管理 HomeViewController は オンボーディング (初回起動時の案内フロー)周りの状態管理も複雑です。 リファクタリング前の課題 初めてZOZOTOWNアプリをインストールしたユーザーは、ホーム画面が表示されるまでに複数の案内画面を経由します。 問題は、この一連のフローを管理するために 5つ以上のBoolフラグ が複数のファイルにまたがって散在していたことでした。例えば「ログイン画面の表示が完了したか」「プッシュ通知許諾を表示したか」「訴求バナーの表示が必要か」といったフラグが各所に分散していました。それらを組み合わせた条件分岐によって次の表示内容が決まる構造になっていました。これにより、「今どのフラグがどの状態のとき何が起きるのか」を把握するだけでもかなりのコストがかかっていました。 このような複雑さが原因の1つとなり、オンボーディングに関する不具合が発生したこともありました。 ステートマシンによる再設計 Step4ではこのオンボーディングフローをステートマシンとして再設計しました。 オンボーディングは以下の4つの状態(State)と、それぞれを遷移させるイベント(Event)によってモデル化されます。 ViewModelはこの状態を購読し、状態に応じてどの画面を表示するかを宣言的に記述します。 この設計により、「現在のフローのどこにいるか」が状態として一点に集約され、遷移のトリガーとなるイベントも明示的になりました。それまでのフラグの組み合わせによる暗黙的な状態管理から脱却し、コードを読むだけでオンボーディングフローの全体像が把握できるようになりました。 また、「どのイベントでどの状態に遷移するか」をテストで直接検証できるようになりました。将来的にオンボーディングのステップが追加・変更されても、状態遷移の定義を修正するだけで対応できます。 こうして、約1年5か月にわたるホーム画面リアーキテクチャが完了しました。Step4に関しては、ホーム画面に起因する障害や問い合わせは発生しませんでした。 Step3で体験したバグと、その後段階的に整備したテストが、実際の品質保証として機能している結果だと感じています。 ホーム画面リアーキテクチャ完了後、後続案件でホーム画面を触った他のメンバーから「実装が楽になった」というフィードバックをもらいました。これは、責務が適切に分割されたことで改修の影響範囲が把握しやすくなったことを示しています。 また、ログ周りの修正が入ったときも「テストで挙動が担保できるようになった」という声がありました。Step3で体験したバグに対して、Step3-ex以降で構築したテストが実際に機能している瞬間でした。 長期リファクタリングを進める上でのポイント 今回のリアーキテクチャを通しての学びやポイントは各ステップで紹介しましたが、全体を通じて特に重要だと感じた点として、 設計ドキュメントの継続的な整備 を挙げます。 設計計画(段階的なステップ計画、インタフェース設計)を文書化しておくことは、長期にわたるプロジェクトをチームで共有する土台になります。「なぜこの設計にしたか」が残っていることで、後続のステップでも一貫した判断ができます。また、AIを活用したコーディングが一般的になった現在では、設計方針が文書化されていることはより一層重要です。AIへの指示の精度が上がるだけでなく、生成されたコードがプロジェクトの設計意図と一致しているかの検証にも役立ちます。 おわりに このリアーキテクチャを振り返ると、最初は「アーキテクチャについての理解を深めたい」という動機から始まりました。しかし実際には、「テストの重要性」「段階的な変更の価値」「失敗を次に活かすこと」という、より本質的なことを学んだプロジェクトになりました。 特に、Step3後のバグ発覚→Step3-exのテスト追加→Step4でのTDD採用でバグ0を達成できたことは、自分の成長を強く実感できたポイントでした。 ZOZOTOWN iOSアプリのリアーキテクチャはまだ続いています。このホーム画面での経験をチームの資産として積み上げながら、より良いアプリを作り続けていきたいと思います。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
みなさんこんにちは!ワンキャリアでエンジニアをしている佐藤(GitHub: seiya2130 )です。 今回は、AIによるコーディング支援(Claude Code)をチームで本格的に活用していくにあたり、 AIにプロジェクト固有のルールを教え込む(育てる)プロセスを自動化した取り組み についてご紹介します。
動画
該当するコンテンツが見つかりませんでした












