Objective-C
イベント
該当するコンテンツが見つかりませんでした
マガジン
該当するコンテンツが見つかりませんでした
技術ブログ
はじめに こんにちは、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
はじめに こんにちは、スタメンでEMをしている あさしん( @asashin227 )です。 4月12日〜14日の3日間、立川ステージガーデンで開催された try! Swift Tokyo 2026 に参加してきました。 try! Swift Tokyoは、世界中からAppleプラットフォームの開発者が集まる国内最大級のSwiftカンファレンスです。今年は21セッション・5ワークショップの充実したプログラムが用意されました。 tryswift.jp 会場:立川ステージガーデン 昨年から会場が変更され、立川ステージガーデンでの開催となりました。渋谷や新宿といった都心と比べて街が落ち着いており、自然も多く過ごしやすい環境でした。 今年は ステージ背面がオープン になり、外と繋がった構造になっていたことが印象的でした。開放感があり、外から発表をのぞき見ることもできる点が新鮮でした。また、昨年から引き続き スライド下にリアルタイム翻訳が表示 されるようになっており、英語セッションでもストレスなく内容を追えるようになった点は、カンファレンス体験として大変満足度の高いものでした。 私はワークショップ(4/12)+カンファレンス2日間(4/13〜14)のフルで参加しました。 集合写真 iOS Private Playgrounds ワークショップ体験記 5つのワークショップの中から、 iOS Private Playgrounds を選択しました。テーマはその名の通り、iOSのプライベートAPIへアクセスすることです。 プライベートAPIとは Appleはフレームワークの内部実装を非公開にしており、公式ドキュメントに載っていないクラスやメソッドが多数存在します。これらにアクセスすることはApp Storeへの提出では許可されませんが、デバッグや内部挙動の理解、テスト環境での活用といった用途には有効です。 探索ツール:headers.82flex.com プライベートAPIを探す際には headers.82flex.com が有用です。iOSの内部ヘッダーが公開されており、クラス名やメソッド名を検索できます。ワークショップでは実際にこのサイトでメソッドを調べながら実装を進めました。 アクセス方法: value(forKey:) と perform(_:) プライベートAPIへのアクセスには主に2つのアプローチを使います。 // プロパティアクセス(KVC: Key-Value Coding) let value = object.value(forKey : "privatePropertyName" ) // メソッド呼び出し(引数なしのシンプルなケース) object.perform(NSSelectorFromString( "privateMethodName" )) NSObject のKVC(Key-Value Coding)を利用した value(forKey:) でプロパティ値を取得し、 perform(_:) で引数なしのメソッドを呼び出します。ただし、Int型などのprimitive引数を持つメソッドには perform が使えないため、 objc_msgSend を直接呼ぶC言語ヘルパーが必要になります。Obj-Cの型をSwiftに名前で公開したい場合は、Bridging Headerにヘッダーファイルをインポートすることで対応できます。 Objective-Cを書いていた頃にKVCの文脈でよく使っていたメソッドが登場し、懐かしさを感じながら作業しました。当時との違いは、SwiftUIからObj-Cの型を直接参照するために Bridging Header へのヘッダーインポートが必要な点です。 挑戦:Face IDインターフェースの呼び出し ワークショップ内では Face IDのインターフェースをプライベートAPIで呼び出せないか を試みました。 内部ヘッダーを調べながら実装を進めましたが、最終的にFace ID認証の画面そのものを呼び出すことはできませんでした。一方で、 Touch IDの登録画面を表示させること には成功しました。Face IDとTouch IDで内部的な仕組みが異なることを体感できた経験でした。 Touch ID設定画面をiPhone 17 Pro Maxで表示できた 実際のプロダクト開発には使えないものの、iOSのシステム内部を覗く体験としては非常に刺激的なワークショップでした。 印象に残ったセッション 21セッションの中から、特に印象深かった3つを紹介します。 SwiftUIってなんでこうなるの? — Paul Hudson 発表者 : Paul Hudson( @twostraws ) youtu.be Paul Hudsonが「SwiftUIの内部構造を理解することでより良いコードが書ける」というテーマで発表しました。 最も印象的だったのは、 @ViewBuilder 内の if-else が _ConditionalContent に変換される という仕組みの解説です。 // ❌ @ViewBuilder内ではtrueとfalseが「別のView」として扱われる var body : some View { if scaleUp { TestView().scaleEffect( 2 ) } else { TestView().scaleEffect( 1 ) } } // → スケールアニメーションにならず、フェードイン/アウトになる @ViewBuilder が if-else を _ConditionalContent<TrueView, FalseView> に変換するため、SwiftUIは「2つの異なるView」として認識します。アニメーションではなくトランジションになるのはこのためです。 解決策は三項演算子を使うか、 @ViewBuilder のない別プロパティに切り出すことです: // ✅ 三項演算子で同一Viewの状態変化として扱わせる var body : some View { TestView().scaleEffect(scaleUp ? 2 : 1 ) } if-else や switch による分岐で表されるViewが内部的にそれぞれ 異なるViewとして保持されている という点は、SwiftUIを深く理解する上での重要な知識です。アニメーションが意図通りに動かない原因がこの仕組みにあると知り、腑に落ちました。 Swiftの型システムはAIエージェントをどう導くのか — Yuta Koshizawa 発表者 : Yuta Koshizawa( @koher ) スライド : SpeakerDeck youtu.be Swiftの型システムがAIエージェントのコーディング精度にどう影響するかを、実験データで示した発表です。 throwsが型の一部であることの意味 Swiftでは throws が関数の型の一部です: func findUser (name : String ) throws -> User TypeScriptなどの多くの言語では、関数がエラーを投げるかどうかは型の一部ではありません。宣言だけでは判断できず、コードを読む必要があります。 実験結果 Claude Codeを使って、SwiftとTypeScriptで同等のダミー関数群に対して同じタスク(エラーハンドリング付きの関数実装)を合計20回実施しました: 言語 正確なエラー処理(5回中) Swift(throwing) 5/5(100%) Swift(non-throwing) 5/5(100%) TypeScript(throwing相当) 0/5(0%) TypeScript(non-throwing相当) 2/5(40%) AIは関数宣言のみを確認してエラー処理の要否を判断しており、 throws の明示がない場合はコードを末端まで追跡しませんでした。 「より厳格なプロンプト」を与えたTypeScriptでも精度は80%に改善しましたが、入力トークン数が約2倍(187,900 → 400,000)に増加しました。Swiftではほぼ増加なし。 Swiftの型システム——特に throws が関数の型の一部として明示されること——が、AIエージェントのコンテキストそのものになり意図通りのアウトプットを可能にするという言説が非常に興味深かったです。 Swift 6でのActor isolationなど直近の破壊的変更に対して「なぜここまで型で縛るのか」と感じることもありましたが、それらの妥当性を補完する視点としても腑に落ちる内容でした。Swift言語への愛着がさらに高まるセッションでした。 XCUITestコード生成をAIに任せる — Yusuke Kita 発表者 : Yusuke Kita( @kitasuke ) スライド : SpeakerDeck youtu.be E2Eテストパイプラインの構築を通じて発見した知見—— 「より良いプロンプト」ではなく「より良い構造(Structure)」が鍵 ——を語ったセッションです。 LLMがテストコードを書く際の3つの失敗パターン LLMに「ログインテストを書いて」と指示すると、一見正しそうで動かないコードが生成されます: パターン 説明 架空のセレクタ 存在しないアクセシビリティIDを自信満々に生成 不正確なテキスト 「Login」と「Sign in」の1単語の違いでテストが失敗 待機戦略の欠如 CI環境での処理時間を考慮しない 解決策:4レイヤーの構造化コンテキスト 詳細なプロンプトを書こうとするのが直感ですが、それではスケールしません。答えは LLMが読み取れるファイルを与えること です: 信頼できるAI生成テストコード = IR(何をテストするか) + Scenarios(どの手順か) + Definitions(何が存在するか) + Templates(どう書くか) Definitions ファイルにアクセシビリティIDや関数シグネチャを定義しておくことで「架空のセレクタ問題」を直接解決します。IDが変わっても1ファイルの更新で全テストに反映されます。 「AIのために構築するのではなく、チームのために構築してください」 この仕組みはAIのためだけに作られたわけではなく、チームのコンテキスト共有のために作られたもの。AIはたまたまこの構造の上でうまく機能しているだけ、という締めが印象的でした。 koherさんの「型がAIのコンテキストになる」という話との思想的な親和性が高く、 「AIへのコンテキストをいかに構造化するか」という問いが、AI活用の本質的な課題 だと改めて感じました。 まとめ try!Swift Tokyoは今年で10周年を迎え、カンファレンスとしての体験やセッション、ワークショップのレベルも年々上がっておりApple プラットフォームに関わるエンジニアにとって最高の3日間になりました。 カンファレンス最終日、ふらっと立ち寄った店にtry! Swiftの参加者が自然と集まり、そこでの交流も印象的でした。 数年ぶりに再会できた方もおり、このような場の価値は、セッションの内容と同等かそれ以上にあると感じています。 try! Swift Tokyoは、世界中からAppleプラットフォームの開発者が集まる国内最大級のカンファレンスです。スキル向上はもちろん、同じ技術を扱う仲間との交流やキャリアを広げる場として最適です。 運営やスピーカーの皆様、そして参加者の皆様のおかげで、とても充実したカンファレンスでした。本当にありがとうございました。 また来年、参加できることを楽しみにしています。 herp.careers
はじめに こんにちは、ZOZOTOWN開発2部iOSブロックのらぷ( @laprasdrum )です。普段はZOZOTOWN iOSアプリを開発するチームで各メンバーの開発における設計や技術課題のフォローアップを担当しています。また、iOS領域におけるテックリードとして社内の技術共有会や ZOZO.swift などを運営しており、各プロダクトのiOSチーム全体をつなげる横断活動に従事しています。 ZOZOTOWN iOSアプリは2010年11月にリリースされ、15年以上にわたって開発が続くプロダクトです。長い歴史の中でチームと技術が変遷し続け、Fat ViewControllerやObjective-Cコードの残存といった技術的負債を抱えていました。これに対してチームは2023年からアーキテクチャの刷新に本格的に取り組んできました。 本記事では、その3年間の変遷を振り返り、アーキテクチャがどのように進化し、設計をレビューする力がチーム全体にどう広がったかをお伝えします。 なお、チーム運営の全体像は ZOZOTOWNのiOSチームを支えるチーム運用 で紹介しています。Fat ViewController解消の具体的な手法は ZOZOTOWN iOSアプリでのFat ViewController解消への取り組み を参照してください。 目次 はじめに 目次 アーキテクチャ変遷の全体像 最初の一歩 — 2023年の型作り MVVMの採用と方針ドキュメントの策定 アーキテクチャレビューの始動 意思決定を支えた二層構造 アーキテクチャの判断と見直し レイヤー設計の選定と見直し Translatorレイヤーの導入(2024年5月) MVVM + UseCaseの採用(2024年9月) ドメインレイヤーの廃止とRepositoryパターンへの集約(2026年3月) 開発プロセスの進化 定量データで見るチームの変化 アーキテクチャ移行の規模 設計レビューの担い手の逆転 AIによる設計知識のチーム展開 1. Geminiによるレビューコメントの要約 2. Copilotレビュー指示の導入・強化 3. チーム共有コマンドへの昇華 4. アーキテクチャ学習サイトと理解度クイズ まとめ アーキテクチャ変遷の全体像 各トピックの詳細へ入る前に、3年間の変遷と技術スタックの移行を俯瞰します。 まず、主要な取り組みの時系列です。 2023年 — MVVM化の型作り Fat ViewControllerの解消を目指し、対象画面を決めてリファクタリングを実施した。 2024年前半 — Translatorレイヤーの導入・アーキテクチャレビューの再設計 API通信モジュールとUIレイヤーの依存を断ち切るTranslatorを導入した。PRレビュー課題の体系的分析から、関心の分離をレビューする運用に刷新している。 2024年後半 — MVVM化の複数画面への展開・Objective-CからSwiftへの移行 商品詳細を中心にMVVM + UseCaseを適用した(詳細は 前述の記事 を参照)。 2025年 — 大規模画面の並行リファクタリング 大規模画面にMVVM + UseCaseを適用し、Storyboardの一部削除やBaseViewControllerの廃止を進めた。 2025年後半 — ガイドラインと設計制約の明文化 MVVM + UseCase統一ガイドやアーキテクチャガイドラインを策定した。 2026年〜 — Repositoryパターン本格導入 実装経験を経てUseCaseが不要なケースを特定し、Repositoryパターンを本格適用している。 技術スタックも同時期に大きく変わっています。 UIアーキテクチャ(2023〜2026) MVC(Fat ViewController)からMVVM + Repositoryへ移行した。 UIフレームワーク(2023〜2026) UIKit + XIB/StoryboardからUIKit(コードベース)+ SwiftUIへ移行した。 非同期・リアクティブ(2024〜2026) ReactiveSwiftからCombineへ移行し、非同期処理にSwift Concurrencyを導入した。 言語(2023〜2025) Objective-CとSwiftの混在状態からSwift中心へ移行した。Swift比率は94.7%(2023年)から99.5%(2026年)へ向上している。数値上は約5ポイントの変化だが、2025年に削除した12クラスはのべ64箇所から参照されており、1クラスの削除に最大17ファイルの改修を伴った。通常の案件開発と並行しながら段階的に解消した。 テスト(2025〜2026) XCTest + NimbleからSwift Testing + 振る舞い駆動のテスト記述へ段階的に移行している。 これらの移行は一括で実施したのではなく、チームの習熟度に合わせて段階的にスコープを拡大していきました。その意思決定の裏側を以降のセクションで掘り下げます。 最初の一歩 — 2023年の型作り MVVMの採用と方針ドキュメントの策定 Fat ViewControllerの解消にあたり、チームはアーキテクチャとしてMVVMを採用しました。実装に先立ち、2022年末からMVVMの実装方針をチームで統一するためのドキュメント作成が始まり、2023年初頭にWIPを外してチームに公開されました。これにより、MVVM化の共通認識となる土台が整いました。このドキュメントは以降も継続的に更新されています。 アーキテクチャレビューの始動 方針が共有された直後の2023年3月、最初のMVVM化が始まりました。この取り組みで特に意識されたのは、単なる1画面の改修ではなく「今後他の人がMVVMで画面を実装する際の参考になるか」という点です。 実際にこのレビューには25件のアーキテクチャレベルのコメントが付きました。レビュアがデータフロー図を描いてViewModelの状態更新フローを可視化するなど、コードではなく設計を先にレビューする文化の萌芽となっています。 続く2画面目(2023年4月)では、レビュー依頼の本文に設計図を含む「アーキテクチャレビュー」セクションが初めて明示され、テンプレートとして定着しました。 こうしてMVVM化の最初の型は作れたものの、これを複数画面に展開し、チーム全体で設計判断の質を維持していくには、個々のPRレビューだけでは限界がありました。チームとして設計を議論し、方針を決め、知識を共有する仕組みが必要だったのです。 意思決定を支えた二層構造 チーム運用の記事 で紹介した「開発生産性MTG」と「Rethink!」について改めて簡単に説明します。 開発生産性MTG :シニアエンジニアとマネージャーが主導し、コードベースの技術的課題を除くチームの課題を議論する場である(2023年10月〜、約30回開催)。各案件の開発マネジメント、PRレビュー、メンバー間の情報格差など、チームがより成熟するために必要な課題を整理し、アクションを設計していた。 Rethink! :技術的な課題をチームメンバーで定期的に見直す週次勉強会である(2024年2月〜通算90回以上)。技術的負債と感じていること、Appleが提示している技術への所感や適応方針、自分だけしか知らないかもしれないことなど、お題は多岐にわたる。話した内容はアーカイブとして残し、過去の意思決定の資料として参照している。 この2つは、案件開発の流れの中だけで方針を決めず、立ち止まって振り返り分析するためのイベントです。 なお、現在は開発生産性MTGという場を設けなくても、メンバー各々がときに立ち止まり、チームに課題を投げかけるコミュニケーションが日常的に行われるようになりました。立ち止まって考える姿勢が特定の会議体に閉じず、チーム全体に浸透した結果、開発生産性MTGは役割を終えています。 この二層構造から3つの重要な方針が生まれました。コード課題の根本原因の特定、リファクタリングのゴール定義、そしてレビュー工程の分離です。 まず、コード品質の課題を時間をかけて分析し、関心の分離・疎結合がコード課題の原因の8割という結論に到達しました。暗黙知が多く「良いコードとは何か」の定義が揃っていないことが本質的な問題であり、「きれいなコードの定義」を目指すのではなく「アンチパターンの提示」で十分という方針が生まれました。 この分析を受けて、リファクタリングのゴールを「きれいなコードをメンバー全員が理解すること」と定義しました。リリースすることはMUSTではなく、認識を揃えることが目的です。この方針のもと、Rethink!をチーム全体での実践場として活用することが決まりました。 同時に、レビューが特定のメンバーに集中しマージ待ちが常態化していた問題も分析しました。その結果、外部品質(仕様通りに動くか)と内部品質(設計・保守性)の確認がひとつのレビューに混在していることが負荷の原因と判明しました。これを受けて、基本設計レビュー(仕様の漏れと実現可能性)、アーキテクチャレビュー(レイヤー間の関心分離)、コードレビュー(実装)の3段階にレビュー工程を分離しました。 これらの方針は、Rethink!を通じてチーム全体の実践に落とし込まれました。全員が同じ題材で設計を提出して観点を揃えたり、実際のPRを題材にレビューの進め方を学んだりしました。レイヤー設計やレビュー手法といったアーキテクチャの具体的な意思決定もRethink!から生まれています。 開発生産性MTGが方向性を定め、Rethink!がチーム全体で実践し、ドキュメントとして蓄積していくことで、暗黙知が特定の個人に閉じることなく、チーム全体の形式知として更新される構造を実現しました。PRレビューのコメントでも日常の雑談でも、「Rethink!で話してみますか」という言葉が自然に出てくるようになりました。 アーキテクチャの判断と見直し 二層構造の仕組みが実際にアーキテクチャの判断をどう動かしたのか、レイヤー設計と開発プロセスの2つの観点から見ていきます。 レイヤー設計の選定と見直し チームはレイヤー構成を3回にわたって選定・見直しました。 Translatorレイヤーの導入(2024年5月) 「関心の分離が課題の8割」という方針を受けて、最初に着手したのはデータレイヤーの整備でした。ZOZOTOWN iOSでは、API通信の処理をアプリ本体とは独立したモジュールとして切り出しています。このモジュールがAPIリクエストの発行とレスポンスのデコードを担い、アプリ側はモジュールが提供するクライアントを呼び出す構成です。 しかし当時は、このAPI通信モジュールが返すレスポンス型をViewModelがそのまま扱っており、モデル変換のロジックがUIレイヤーに漏れ出していました。UIレイヤーがAPI通信モジュールの型に直接依存する状態です。 この課題に対して導入されたのがTranslatorレイヤーです。API通信モジュールのレスポンスをアプリ内のモデルに変換する責務を一手に担い、UIレイヤーとAPI通信モジュールの依存を断ち切りました。2024年5月に最初の実装がマージされ、同時期にMVVM方針ドキュメントにもTranslatorの項目が追記されています。 この分離は設計方針の浸透とテスタビリティの両面で効果がありました。ViewModelはアプリ内のモデルだけを扱う前提になるため、「ViewModelはアプリ内のモデルを扱う層である」という方針をチームに伝えやすくなりました。また、ViewModelのテストからAPI通信モジュールへの依存がなくなり、テストビルド時に不要なモジュール依存を削除できるようになりました。 Translatorの導入により、データレイヤーの一部が確立されました。次の課題は、ViewModelとこのTranslatorの間、つまりドメインレイヤーをどう設計するかです。 MVVM + UseCaseの採用(2024年9月) データレイヤーやドメインレイヤーの責務分割にはチーム方針がなく、メンバーによるコード品質の差が大きくなるポイントでした。一方で、馴染みのないアーキテクチャを導入してメンバーの認知負荷を上げるわけにもいかず、最低限のレイヤー定義が求められました。開発生産性MTGでUseCaseの責務を議題に絞り込み、Rethink!で3つの選択肢を比較しました。 Androidの推奨アプリアーキテクチャ方式 :UseCaseはオプショナルで、複雑なビジネスロジックのカプセル化や、複数のViewModelから再利用されるロジックの共通化を担う。ただしデータレイヤーのRepositoryがある前提 Clean Architecture + DDD方式 :UseCaseがドメインロジックを担い、ドメインモデルとペアで設計する。画面に紐づかずドメイン単位で定義するため、境界設計が難しくなる MVVM + UseCase方式 :UseCaseがドメインレイヤー(本来のUseCase)とデータレイヤー(Repository)の両方をカバーする。データソースが単一ならRepositoryを別途作るコストをスキップできる チームが選んだのはMVVM + UseCaseでした。ZOZOTOWN iOSではデータソースが単一のケースが多く、Repositoryを別途設ける必要性が低かったためです。「レイヤーの責務が肥大化しすぎた場合はClean Architecture方向に進化させる」という留保付きの判断でした。 ドメインレイヤーの廃止とRepositoryパターンへの集約(2026年3月) この判断のもと、チームは複数画面でMVVM + UseCaseを実装していきました。しかし約1年半の運用を経て、Rethink!で「ドメインレイヤー(UseCase)を廃止し、データレイヤー(Repository)に責務を集約すべきではないか」という議論が持ち上がりました。実装を重ねる中で見えてきたのは以下の点です。 ZOZOTOWN iOSではドメインモデルやそれを用いるビジネスロジックが稀であること APIClientとTranslatorの組み合わせが実質的にRepositoryの役割を果たしており、UseCaseとの責務が重複していたこと UseCaseを設けても責務の理解が揃わず、チーム内に混乱を招いていたこと 重要だったのは「各レイヤーの責務の理解が揃うこと」であり、UseCaseという層を設けること自体が目的ではなかったという気づきです。結果、MVVM + Repository(UseCaseなし)をチームの方針としました。 これは単なる揺り戻しではなく、「レイヤーの責務が肥大化しすぎた場合はClean Architecture方向に進化させる」という最初の留保に対する回答です。実際に使ってみた結果「肥大化ではなく責務の重複が問題であり、むしろ層を減らすほうが適切だった」という結論に至りました。 開発プロセスの進化 同時期に、設計の進め方そのものにも変化がありました。 アーキテクチャレビューでは、当初は重厚なPlantUML図を用いて設計を可視化していました。しかし「作図コストが高い割に手戻りは防げない」ことがわかり、Protocol定義をPRにして設計をレビューする軽量な手法に転換しました(2024年8月)。XIB/Storyboardからの移行方針(2025年4月)など、UIフレームワークの選定もRethink!で議論しています。 こうした議論と意思決定の積み重ねは、チームの活動にどのような変化をもたらしたのでしょうか。定量データで確認します。 定量データで見るチームの変化 アーキテクチャ移行の規模 ViewModel / UseCase / Repository / Translatorを含むコミットメッセージを年別に集計しました。 年 コミット数 前年比 2023 217 — 2024 302 +39% 2025 608 +101% 2026(〜2月) 194 — コミット数の増加と並行して、レガシーコードの削減も進みました。以下は2025年のレガシーコード削減実績です。 指標 2025年1月 2025年12月 削減率 Objective-C .mファイル 19 3 84% Objective-Cコード行数 2,768 788 72% XIB/Storyboard 88 58 34% 特筆すべきは、2024年に積み重ねた依存解消が2025年の大規模削除を可能にした点です。一度に大きく変えるのではなく、依存を個別に剥がし続けたことで、翌年のレガシーコード削減につながりました。 設計レビューの担い手の逆転 アーキテクチャ移行の規模が拡大する中で、その設計レビューを誰が担うかにも大きな変化が起きました。 2023年末時点では、GitHubのCODEOWNERSに筆者のみが設定されており、すべてのPRが筆者のApproveなしにマージできない状態でした。この期間にマージされたPRは月平均約53件にのぼります。一部案件のスプリントレビュー時にレビュー待ちチケットが残留し、「マージ待ちになっている時間が結構ある」という声が上がるほどで、まさに「できる人がやる」体制の限界でした。 2024年6月、この問題を受けて筆者ともう1名のシニアエンジニアによる2名体制に移行し、アーキテクチャレビューとコードレビューの役割を分担しました。同時に、それまでレビューに関わる機会のなかった他のメンバーをピアレビュアとして外部品質のレビューに参加してもらう体制を整え、レビュー文化の醸成とレビュア育成がここから始まりました。 PRレビューコメントのうち設計に関するものを定量分析した結果、シニアレビュアの比率は2024年10-12月の89%から2025年1-3月には25%へ低下しました。2026年1-2月時点では9%まで下がり、ピアレビュアが設計コメントの91%を担っています。2026年2月には固定レビュア制度そのものが廃止され、ピアレビュアのランダム選出に移行しています。 この変化の背景には、開発生産性MTGによる制度設計とRethink!での実践という二層構造の仕組みがありました。 数字の裏にある変化を一人のメンバーの軌跡で見ると、より具体的になります。初期(2024年)はレイアウト設計の範囲内にとどまっていたレビューが、UseCase設計の責務境界の理解(2025年1-3月)を経て成長しました。最終的にはディレクトリ構造・命名規則の統一提案・APIレスポンス型の設計方針など、プロジェクト全体を俯瞰するレビューに到達しています。 設計レビューの担い手が広がる一方で、シニアレビュアが持っていたレビュー観点そのものを、人に依存しない形で残す取り組みも並行して進めました。 AIによる設計知識のチーム展開 アーキテクチャの形式知化の最終段階として、設計知識とレビュー観点をAIを活用した4つの手段でチームに展開しました。 1. Geminiによるレビューコメントの要約 レビュア育成とシニアレビュアへのレビュー依存の解消を目指し、週次でPRレビューコメントを収集しGeminiに要約させてSlackに投稿しています。要約結果をチームの週次定例で振り返り、レビュー観点の共有やレビュア同士の知見交流、まだレビュアになっていないメンバーへのレビュー観点インプットに活用しています。 2. Copilotレビュー指示の導入・強化 GitHub CopilotによるPRへのインラインコメントは、開発者が環境構築や新しいツールを導入する必要がなく、チームメンバーがAIの恩恵を素早く実感できる手段でした。 copilot-instructions.md にレビュー観点を定義し、アーキテクチャ準拠・iOSベストプラクティス・テスタビリティ等の知見を追加しました。 Copilotによるアーキテクチャ関連コメントは月間28件から94件に増加しました(2025年8月から2026年2月)。従来はシニアレビュアが暗黙的にチェックしていた観点が自動的にレビューされるようになりました。 3. チーム共有コマンドへの昇華 レビュー観点をClaude Codeのコマンドとして言語化し、チーム共有のセルフレビュースキルとして整備しました。マージ先ブランチの確認、開発チケットや設計ドキュメントからのコンテキスト取得、アーキテクチャ準拠チェック、動作確認シナリオの生成まで、PR作成前に開発者自身でセルフレビューできるようにしました。 4. アーキテクチャ学習サイトと理解度クイズ アーキテクチャガイドラインやテンプレートの内容をもとに、インタラクティブな学習サイトの生成とCLI上でのクイズによる理解度チェックをClaude Codeのコマンドとして整備しました。レイヤー構成やデータフロー、アンチパターンなどをブラウザ上で視覚的に学べるほか、クイズではドキュメントに明記されたルールから出題し、回答ごとに根拠となるドキュメント箇所を提示します。 この4つは、暗黙知の移転経路がそれぞれ異なります。 Gemini要約 :人のレビューコメントをAIが集約して全員に届ける仕組み Copilot指示 :人からAIへの知識の埋め込みによる自動レビュー セルフレビューコマンド :人がAIを介して自分自身に観点を返す仕組み 学習サイトとクイズ :ドキュメントをAIが対話的な教材に変換する仕組み 経路が異なるからこそ、どれかひとつが欠けても他で補完できる構造になっています。 まとめ ZOZOTOWN iOSチームのアーキテクチャは、2023年の1画面でのMVVM化から始まりました。2024年のTranslator導入と複数画面への展開、2025年の大規模並行リファクタリング、2026年のRepositoryパターンへの集約と段階的に進化しています。チームの習熟度に合わせてスコープを拡大し、実装経験をもとにレイヤー構成そのものを見直す判断もできるようになりました。 この過程を支えたのが、開発生産性MTGで方向性を定め、Rethink!でチーム全体が実践し、ドキュメントとして蓄積していく二層構造の知識形成フローです。暗黙知が特定の個人に閉じることなく、チーム全体の形式知として更新される仕組みを作りました。 さらに、Geminiによるレビューコメント要約、Copilotレビュー指示、Claude Codeのセルフレビューコマンド、アーキテクチャ学習サイトの4つを整備しました。これらを通じて、設計知識とレビュー観点をAIを介してチームに展開しています。 設計レビューの担い手もシニアレビュア89%からピアレビュア91%へ完全に逆転しています。 この変化はチームに具体的な効果をもたらしています。責務やデータフローの定義がチーム全体で揃ったことで、PRレビューの場で合意形成がスムーズに進むようになりました。設計の相談先が特定の個人に閉じなくなり、意思決定の属人化も解消されています。かつては口伝に頼っていた知識が体系化されたことで、新メンバーが自律的に学べる環境も整いました。 「できる人がやる」から「全員で設計をレビューできる」への転換が、アーキテクチャの一貫性とチームのスケーラビリティの両立を実現しています。 ZOZOでは一緒に働くエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
動画
該当するコンテンツが見つかりませんでした






