Objective-C
イベント
該当するコンテンツが見つかりませんでした
マガジン
該当するコンテンツが見つかりませんでした
技術ブログ
はじめに こんにちは、スタメンで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
本記事は【 Advent Calendar 】の9日目の記事です。 AI戦略室のM・Wです。この記事を書いているのは12月1日です。 11月30日にジャパンカップで カランダガン の単勝と三連単を取りました。現地で観戦をしていたのですが、大変興奮したレースでした。ゴール間際の2頭の叩き合いに混ざる空馬。 ぜひYoutubeで動画を見てください。以上、12月11日の記事でした! 沼の淵 突然ですが、forkやspawnの違いについて知っていますでしょうか。 私は知らなかったです。いまも正直わかりません。 その結果、沼にハマってしまいました。抜け出せないまま今日を過ごしています。 これは自分用のメモ+同じようなことを実装する人がいた際の一助になればいいなと思って執筆しています。 まずとあるAgentを考えます。このAgentは「環境」を知覚し「行動」を決定するAgentです。 Agentは「行動」を行うと「環境」から「報酬」を手にすることができ、「環境」は次の「状態」に遷移します。 図にするとこんな感じです。これは基本的な強化学習を説明する際に用いられる簡略図です。 今回、ボン◯ーマンのようなPvPをするゲームについて、強化学習を使ってエージェントを作る機会がありました。つまり上の画像に照らし合わせると下記の表となります。 用語 対応 環境 ゲーム自体 エージェント 操作キャラクター 状態 その時時の盤面 行動 移動+ボム配置+キック 報酬 勝利/敗北など レギュレーションとして自陣営は2つのBotを用意する2vs2の対決と提示されたため、上記のエージェントが2つ必要なマルチエージェント学習を行う必要がありました。 (ちなみに特に実装は指定されなかったのでロジックで記述するでもOKです。AI部署なので強化学習で挑みました) 沼にダイブ ここでいよいよ掲題に出てきたマルチプロセス学習の話ができてます。 最新の強化学習アルゴリズムに疎かったので、一旦Actor-CriticベースのA3Cアルゴリズムを選択しました。実装したモデル構造は下記となります。 盤面のエンコードやモデル設計については以下のモデルを参考としています。Cursor先生ありがとう! Multi-Agent Training for Pommerman: Curriculum Learning and Population-based Self-Play Approach: https://arxiv.org/abs/2407.00662 マルチプロセス学習の全体感としては下記の図です。 ゲーム盤面を提供するホスト側とエージェント間はWebSocketによる疎通を行う必要があるため、各BotごとにWebSocketサーバを子プロセスとして作成します。 つまり子プロセス上では、ゲーム開始から終了まで実行されますが、そこで収集したデータについては何かしらの方法を使って親プロセス上に移動させないと、子プロセスがterminateされた際に破棄されてしまう問題があります。 そのため 共有メモリ を使って誤差勾配を親プロセス側と共有を行い、最適化器を使って共有モデルのパラメータを更新、次の学習ループで子プロセスへ配分を繰り返し学習を行っていきます。 沼の底 学習モデルを共有メモリに配置するためには model.share_memory() を実施しますが、今回の実装のケースにおいてたびたび子プロセスの model.backward() が失敗する事象がありました。 いくつか問題はありましたが、 ・下記のログが出力され、backward処理が失敗するケース objc[8623]: +[MPSGraphObject initialize] may have been in progress in another thread when fork() was called.objc[8623]: +[MPSGraphObject initialize] may have been in progress in another thread when fork() was called. We cannot safely call it or ignore it in the fork() child process. Crashing instead. Set a breakpoint on objc_initializeAfterForkError to debug. ・backward()が呼び出された際に子プロセスがクラッシュして音もなく消える 使用してる端末がMacOSであるため、前者のケースでは $export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES を実行することでエラー文が出力されなくなります。しかしながらこれはワークアラウンドとなります。 MacOSでは fork() が実行された際に、親プロセスで既に初期化されつつあったObjective-Cのデータ構造が子プロセスに引き継がれると、子プロセス側でその初期化が安全に完了できない状況が発生することがあります。これにより、子プロセスがクラッシュ(通常はEXC_BAD_ACCESS)したり、デッドロックしたりします。 Pythonのmultiprocessingは、デフォルトでは「fork」方式を使って新しいプロセスを生成するため、この問題に直面しやすいです。そのため2点目の音もなく子プロセスが消えた原因も含めて、backward()処理がうまく動作しないのは fork() による子プロセスの生成に問題があると結論付けました。 ちなみにPyTorchからマルチプロセス学習する際のベストプラクティスでも「Use an alternative process start methods, such as spawn or forkserver, which ensures a clean initialization of each process.」と記載があります。 https://docs.pytorch.org/docs/stable/notes/multiprocessing.html#poison-fork-in-multiprocessing spawn は fork とは違い、Pythonインタープリタごとプロセスを生成します。起動が遅いというデメリットはありますが、必要な情報のみを親プロセスから引き継ぐ点や上記の初期化に起因するようなエラーを回避できるメリットがあります。 実験 簡単なNNを構築してspawnを使用したbackward()がうまくいくかどうかを実験してみます。 import torchimport torch.nn as nnimport torch.multiprocessing as mpimport torch.nn.functional as Fimport torch.optim as optimclass ExampleNN(nn.Module): def __init__(self): super(ExampleNN, self).__init__() self.fc = nn.Linear(10, 1) def forward(self, x): out = self.fc(x) return F.sigmoid(out)def train(xs, target): model = ExampleNN() optimizer = optim.SGD(model.parameters()) out = model(xs) loss = F.binary_cross_entropy(out, target) optimizer.zero_grad() loss.backward() optimizer.step() print('trainメソッドが終了したよ') 上記のtrainメソッドをforkで動かしてみます。 if __name__ == '__main__': x = torch.rand(1, 5, 10) t = torch.Tensor([0, 1, 1, 0, 1]).reshape(1, 5, 1) train(x, t) print('-'*54) ctx = mp.get_context('fork') processes = [] for i in range(5): p = ctx.Process(target=train, args=(x, t)) p.start() processes.append(p) for p in processes: p.join()>>> trainメソッドが終了したよ------------------------------------------------------RuntimeError: Unable to handle autograd's threading in combination with fork-based multiprocessing. See https://github.com/pytorch/pytorch/wiki/Autograd-and-Fork 無事エラーが出ました。 ちなみに途中の train(x, t) をコメントアウトすると正常に動作します。これは上記のGithubにも記載の通り、backward()で呼び出されているAutogradがすでに使用されている状態でfork()されたことに起因するエラーとなります。 Autograd engine relies on threads pool, which makes it vulnerable to fork. We detect such situations and warn users to use spawn method of multiprocessing. 指示通りspawnでやってみましょう。 if __name__ == '__main__': x = torch.rand(1, 5, 10) t = torch.Tensor([0, 1, 1, 0, 1]).reshape(1, 5, 1) train(x, t) print('-'*54) ctx = mp.get_context('spawn') processes = [] for i in range(5): p = ctx.Process(target=train, args=(x, t)) p.start() processes.append(p) for p in processes: p.join() >>> trainメソッドが終了したよ------------------------------------------------------trainメソッドが終了したよtrainメソッドが終了したよtrainメソッドが終了したよtrainメソッドが終了したよtrainメソッドが終了したよ 無事エラーなく実行することができました。 この結果から spawn を明示的に指定することでbackward()処理のエラーを回避できることがわかります。 実際の実装は下記のような形となっています。 ctx = mp.get_context('spawn') # mp.set_start_method('spawn', force=True) # エピソードループ for episode in tqdm(range(num_episodes)): clients: List[BombermanClient] = self.create_clients() process_pool = [] for c in clients: p = ctx.Process(target=c.run) p.start() process_pool.append(p) # エピソードの学習 loop = asyncio.get_event_loop() loop.run_until_complete(self.train_episode(max_steps)) print("ロールアウト終了") # 子プロセスの終了を待つ for p in process_pool: if p.is_alive(): p.terminate() p.join(timeout=3) # 最大3秒待つ if p.is_alive(): # それでも終了しない場合は強制終了 print(f"Warning: Process {p.pid} did not terminate gracefully, forcing...") p.kill() p.join() そもそもMacOSだとデフォルトで spawn だそうです。つまりわざわざ明示的に fork を使ったところから全ては始まったのです。Cursorくん、君は頑なにforkを推していたよね・・・。 これで 戦 に臨めます! 終わりに 元々のスタート地点として、マイナビグループの中の一つである Mynavi Techtus Vietnam さんから挑戦状を叩きつけられたところからこの沼は始まりました。 Techtusさん「ボン◯ーマンAIを作成してPvPをやろう!それでどこのチームが強いか勝負だ!」 結果は・・・ 全敗 でした。 そもそもこの問題を解決したのは戦いが終わった3週間後ぐらいなんです! つまり、戦には裸同然で挑んだことになる・・・?すでに次の戦は始まっているという決意を胸に日々沼の中で生きたいと思います! イベント告知 12月23日にイベントを開催します!申し込みはこちらから▼ https://mynaviit.connpass.com/event/376769
動画
該当するコンテンツが見つかりませんでした









