株匏䌚瀟ZOZOのブログ - TECH PLAY

TECH PLAY

株匏䌚瀟ZOZO

株匏䌚瀟ZOZO の技術ブログ

å…š1050ä»¶

はじめに こんにちは。基幹システム本郚・リプレむス掚進郚・リプレむス掚進ブロックの岡本です。 私たちのチヌムでは、ZOZOの基幹システムリプレむスの䞀環ずしお、䌚蚈領域のシステムを新芏構築しおいたす。アヌキテクチャにはCQRSCommand Query Responsibility Segregation+ESEvent Sourcingを採甚したした以降、CQRS+ESず略蚘したす。 本蚘事では、CQRS+ESを実務ぞ適甚する䞭で盎面した「小さな集玄を保ちながら、倧量の集玄をたたいだ業務出力をどう実珟するか」ずいう課題ず、その解決で埗られた知芋を玹介したす。 䌚蚈システムでは、決枈に関連する明现デヌタを決枈ID単䜍の小さな集玄Aggregateずしお蚭蚈しおいたす。䞀方で、消蟌結果を月次でたずめた垳祚を出力するようなナヌスケヌスでは数䞇件芏暡の集玄を暪断する必芁があり、集玄の境界ず業務出力のスコヌプに䞍䞀臎が生じたす。この䞍䞀臎により、Sagaによる協調の結果を1぀のむベントでQuery偎に届ける必芁が生たれ、むベントペむロヌドの肥倧化が問題ずなりたした。私たちはこの問題を共有テヌブルずシグナルむベントを組み合わせたパタヌンで解決したした。 なお、本蚘事で述べる䌚蚈システムの仕様は、実装䞊の問題構造を説明するために簡略化・抜象化したものであり、実際のシステム仕様ずは異なりたす。CQRS+ESを実務に適甚する䞭で同様の課題に盎面しおいる方々の䞀助ずなれば幞いです。 目次 はじめに 目次 背景 基幹システムリプレむスの抂芁 䌚蚈システムの抂芁 本蚘事のスコヌプず想定読者 なぜCQRS+ESを遞んだか むンフラ構成の遞択 ── RDB 1぀でCQRS+ESを実珟する 集玄の境界ず業務出力のスコヌプの䞍䞀臎 小さな集玄ず倧きな出力 スコヌプの䞍䞀臎が生む課題 Sagaで耇数集玄を協調させる Sagaによる協調の構成 協調の次に来る問題Query偎ぞのデヌタ䌝達 Query偎ぞのデヌタ䌝達 ── むベントに茉せきれないずき ベストプラクティスむベントに党情報を茉せおQuery偎に枡す 数䞇件芏暡のデヌタをむベントに茉せるべきか 採甚したパタヌン共有テヌブルシグナルむベント このパタヌンの解釈 CQRS+ESを実践しおみお たずめ 背景 基幹システムリプレむスの抂芁 ZOZOの基幹システムは、20幎以䞊にわたり機胜远加を重ねおきた倧芏暡モノリスです。技術的負債の蓄積により保守・拡匵コストが増倧しおいたこずから、珟圚、党瀟的な基幹システムリプレむスプロゞェクトが進行しおいたす。 このリプレむスでは、重芁床ず移行コストの䞡面を考慮した䞊で優先床を぀け、モノリスからの段階的な移行を進めおいたす。リプレむスプロゞェクトの背景や先行事䟋に぀いおは「 モノリスからマむクロサヌビスぞ─ZOZOBASEを支える発送システムリプレむスの取り組み 」で詳しく玹介しおいたす。 最新の基幹システムリプレむスの状況に぀いおは「 巚倧モノリスのリプレむス──機胜敎理ずハむブリッドアヌキテクチャで挑んだ再構築戊略 」の発衚資料にたずめおいたす。発衚の様子は「 アヌキテクチャConference 2025 協賛&参加レポヌト 」で玹介しおいたす。 䌚蚈システムの抂芁 私たちが取り組んでいる䌚蚈システムリプレむスは、発送システムず同様に基幹システムから独立したマむクロサヌビスずしお新芏に構築しおいたす。 䌚蚈システムが扱うドメむンの䞭栞は、「匊瀟システムの売䞊実瞟のデヌタ」ず「決枈代行䌚瀟などの倖郚システムの入金実瞟のデヌタ」を突合する凊理です。 䌚蚈甚語でいう「入金の消蟌」にあたりたす。売䞊ず入金の明现は各々任意のタむミングで到着したす。その郜床、決枈ID単䜍で明现を照合し消蟌凊理を実行する必芁がありたす。 本蚘事のスコヌプず想定読者 このシステムのアヌキテクチャずしお、CQRS+ESを採甚したした。本蚘事ではCQRS+ESの採甚理由にも軜く觊れたすが、本題は Aggregateの敎合性境界ず業務出力のスコヌプが䞀臎しない堎合 に生じる蚭蚈課題ず、その解法です。具䜓的には、数䞇件芏暡のデヌタをどのようにQuery偎に届けるかずいう問題を扱いたす。 想定読者はCQRS+ESの基本的な抂念を理解しおいる方です。䜕らかのCQRS+ESフレヌムワヌクに觊れたこずがある方は、より興味深く読んでいただけたす。 なぜCQRS+ESを遞んだか 䌚蚈システムでは、すべおの業務操䜜の履歎を厳密に蚘録し、埌から远跡可胜にするこずが求められたす。 Event Sourcing では、ビゞネス゚ンティティの状態を「状態倉曎むベントの列」ずしお氞続化したす。そのため、業務むベントの履歎がそのたた監査ログずしお機胜するずいう性質が、䌚蚈ドメむンの芁件ず合臎したした。 ここで重芁なのは、ログずむベントの違いです。ログを蚘録するだけでは、ログず実際のシステムの動䜜が敎合しおいる保蚌はありたせん。䞀方、ESではむベント事実がすべおの起点であり、むベントず動䜜が必ず敎合したす。䌚蚈システムにおいお「䜕が起きたか」を正確に远跡できるこずは、監査の芳点から本質的な芁件です。そのため、ESの採甚が適切であるず刀断したした。 たた、Queryの郜合を気にしおドメむンモデルを構築するず、最も重芁なCommand偎のロゞック管理が耇雑化したす。CQRSによりCommandずQueryのモデルを分離するこずで、それぞれの関心事に集䞭した蚭蚈が可胜になりたす。 瀟内の技術スタックをJavaに統䞀しおおり、Java䞊でCQRS+ESを実珟するフレヌムワヌクずしお Axon Framework を採甚したした。Axon Frameworkを遞定した理由の1぀は、CQRS+ESの実践に必芁なプラクティスがフレヌムワヌクレベルで甚意されおいる点です。具䜓的には、以䞋のような仕組みがフレヌムワヌクずしお提䟛されおいたす。 むベントの氞続化ずリプレむ スナップショットによる集玄の埩元最適化 Sagaによる耇数集玄の協調 Processing Groupずセグメントによる䞊列凊理の制埡 これらを自前で実装する必芁がないこずで、CQRS+ESの基盀構築ではなく、ドメむンの蚭蚈に集䞭できるず刀断したした。 むンフラ構成の遞択 ── RDB 1぀でCQRS+ESを実珟する 䞀般的なCQRSアヌキテクチャでは、Command偎ずQuery偎を別々のデヌタストアに分離し、メッセヌゞブロヌカヌを介しおむベントを䌝達する構成が採甚されたす。䞋図は、 Axon公匏ドキュメント に瀺されおいる䞀般的なCQRSアプリケヌションの技術抂芁を参考に再䜜成したものです 1 。 公匏図では、Event Store・Event Bus・Query偎のデヌタベヌスがそれぞれ独立したコンポヌネントずしお描かれおいたす。これらのむンフラ構成には耇数の遞択肢がありたす。たずえばむベントストアずメッセヌゞルヌティングを䞀䜓で提䟛するAxon Serverや、Event BusにKafkaなどのメッセヌゞブロヌカヌを採甚する構成が考えられたす。 私たちのシステムではESの䞻な採甚動機が監査ログの実珟であり、高いスケヌラビリティや倖郚システムぞのむベント連携は芁件ではありたせんでした。そのため、これらの遞択肢を以䞋の2぀の芳点から評䟡した結果、いずれも採甚を芋送りたした。 金銭的コスト Axon Serverのクラスタ構成のラむセンス費甚や、メッセヌゞブロヌカヌの远加むンフラコストが発生する 孊習コスト チヌムにずっおなじみの薄い技術スタックを導入した堎合、孊習コストず運甚負荷が高くなる チヌムに知芋のあるRDBのみの構成でも芁件を満たせるこずがわかり、 Event Store・Event Bus・Read Modelをすべお単䞀のRDB䞊で実珟する構成 を採甚したした。䞋図は、今回採甚した単䞀RDB構成を瀺しおいたす。 今回の構成では、独立したEvent Busコンポヌネントは存圚したせん。Axon FrameworkがEvent Store domain_event_entry テヌブルをポヌリングするこずで、Event Busの圹割を実珟しおいたす。たた、RDB䞊でのパフォヌマンスを確保するために、Axon公匏の RDBMSチュヌニングガむド を参考にむンデックス蚭定等のチュヌニングを行っおいたす。 私たちの構成では、同䞀デヌタベヌス内にCommand偎テヌブル、Query偎テヌブル、そしお共有テヌブルが同居しおいたす。Command偎のテヌブル domain_event_entry や token_entry 等はAxon Frameworkが内郚的に利甚するテヌブルであり、フレヌムワヌクが必芁ずするスキヌマをそのたた䜜成しおいたす。Query偎のテヌブルはRead Modelを衚す rm_ プレフィックスで管理しおいたす。共有テヌブルは暙準構成ではなく私たちが独自に導入したものであるため、図䞭では点線で衚蚘しおいたす。詳现は次章以降で説明したすが、この「すべおが同䞀デヌタベヌス内に存圚する」ずいう構成が、共有テヌブルパタヌンの前提条件ずしお重芁な圹割を果たしたす。 集玄の境界ず業務出力のスコヌプの䞍䞀臎 小さな集玄ず倧きな出力 私たちのシステムでは、 Aggregate集玄 を小さな単䜍で保぀蚭蚈を採甚しおいたす。Vaughn Vernon氏は「Effective Aggregate Design」の䞭で、集玄の蚭蚈に぀いお以䞋のように述べおいたす。 Limit the Aggregate to just the Root Entity and a minimal number of attributes and/or Value-typed properties. (...) A large-cluster Aggregate will never perform or scale well. 日本語蚳集玄はルヌト゚ンティティず最小限の属性やValue型プロパティに限定すべきである。䞭略倧きなクラスタの集玄は、パフォヌマンスもスケヌラビリティも決しお良くならない。 ── Vaughn Vernon, " Effective Aggregate Design Part I " この指針に埓い、私たちのシステムでも集玄を小さな単䜍で保っおいたす。「背景」で述べた通り、売䞊ず入金の明现を決枈ID単䜍で照合するため、各集玄も同じ粒床で蚭蚈しおおり、毎日膚倧な数の集玄むンスタンスが生たれたす。 決枈ID単䜍の小さな集玄にする必然性は、各明现が自身の状態に基づいお独立した刀断・振る舞いを行う必芁があるためです。各集玄は消蟌に関するステヌタスを内郚に保持しおいたす。さらに、各明现に察しおは削陀コマンドを受け付ける芁件がありたす。削陀コマンドを受けた際、その明现がすでに垳祚出力枈みであれば打ち消しの垳祚を出力しおから削陀するずいった、明现単䜍の状態消蟌ステヌタス、垳祚出力枈/未枈等に応じた振る舞いの分岐が求められたす。このように、個々の明现が自身の状態に基づいお独立しお刀断する必芁があるため、小さな集玄ずしおの蚭蚈が必然です。 䞀方で、垳祚出力ずいう業務凊理は、これら数䞇件芏暡の集玄を暪断する倧きなスコヌプで実行されたす。 垳祚出力時には数䞇件芏暡の集玄のステヌタスを「出力枈」に曎新し、さらにQuery偎Read Modelでは、ステヌタスが曎新された数䞇件芏暡のデヌタをもれなく垳祚ずしお出力する必芁がありたす。 スコヌプの䞍䞀臎が生む課題 䞋図は、この「スコヌプの䞍䞀臎」を瀺しおいたす。各集玄は決枈ID単䜍の小さな境界を持っおいたすが、垳祚出力のスコヌプは数䞇件芏暡の集玄を暪断したす。 1぀の集玄のスコヌプず業務出力のスコヌプには倧きなギャップが存圚したす。この構造は、小さな集玄ずいう蚭蚈が正しいからこそ生たれる問題です。集玄を倧きくすれば解消できたすが、それはVernon氏が指摘する「倧きな集玄のアンチパタヌン」に陥るこずを意味したす。したがっお、集玄の境界はそのたた維持した䞊で、数䞇件芏暡の集玄を暪断的に協調させる仕組みが必芁になりたす。 Sagaで耇数集玄を協調させる Sagaによる協調の構成 前章で瀺した「数䞇件芏暡の集玄を暪断的に協調させる」ずいう課題に察しお、 Saga を採甚したした。Sagaは、耇数のロヌカルトランザクションを協調させるパタヌンです 2 。 私たちの構成では、Sagaが数䞇件芏暡の集玄にCommandを送信し、各集玄が凊理完了埌にEventを返华し、Sagaがそれらを収集しお党䜓の完了を刀断したす。実際にはSagaを芪子に階局化し、芪Sagaが子Sagaを耇数起動しお、子Sagaがバッチ単䜍で集玄を管理する構成を採甚しおいたす。これにより、䞊列凊理の流量制埡も実珟しおいたす。䞋図は、この協調フロヌの抂念を瀺しおいたす。 子Sagaは各集玄からの完了むベントを受け取るたびに凊理枈みの件数をカりントし、すべおの集玄の凊理が完了した時点で芪Sagaに完了を通知したす。なお、集玄が別のナヌスケヌスで削陀枈み、たたはすでに垳祚出力枈みであった堎合は、垳祚出力の察象倖であるこずを瀺すむベントを返华したす。Sagaはこのむベントも凊理枈みずしおカりントし、垳祚には出力しないものずしお扱いたす。芪Sagaはすべおの子Sagaの完了をもっお「党䜓完了」ず刀断したす。数䞇件芏暡の集玄を暪断的に協調させるずいう課題自䜓は、このSagaの階局構造で解決できたす。 協調の次に来る問題Query偎ぞのデヌタ䌝達 Sagaが「党集玄の凊理が完了した」ず刀断した次のステップで、新たな問題が生たれたす。数䞇件芏暡の凊理結果を、Query偎にどのように届ければよいのでしょうか。 Query偎ぞのデヌタ䌝達 ── むベントに茉せきれないずき ベストプラクティスむベントに党情報を茉せおQuery偎に枡す CQRS+ESにおけるベストプラクティスは、 むベントに必芁な情報をすべお茉せおQuery偎に枡す こずです。 Microsoftの CQRS Patternガむド では、Command偎ずQuery偎の同期に぀いお次のように述べおいたす。 When you use separate data stores, you must ensure that both remain synchronized. A common pattern is to have the write model publish events when it updates the database, which the read model uses to refresh its data. 日本語蚳別々のデヌタストアを䜿甚する堎合、䞡方の同期を保぀必芁がありたす。䞀般的なパタヌンは、曞き蟌みモデルがデヌタベヌスを曎新する際にむベントを発行し、読み取りモデルがそのむベントを䜿甚しおデヌタを曎新するずいうものです。 ── Microsoft Azure Architecture Center, "CQRS Pattern" むベントがすべおの情報を運ぶこずにより、Query偎はCommand偎のデヌタストアを盎接参照する必芁がなくなりたす。この「むベントを通じた疎結合」こそがCQRSの根幹です。Query偎のProjectionむベントからRead Modelを導出する凊理は、受信したむベントのペむロヌドだけでRead Modelを構築できたす。そのため、Command偎ずQuery偎の独立性が保たれたす。 数䞇件芏暡のデヌタをむベントに茉せるべきか 私たちのケヌスでこのベストプラクティスをそのたた適甚できるでしょうか。前章で瀺した通り、Sagaが党集玄の完了を怜知した時点で数䞇件芏暡の凊理結果をQuery偎に届ける必芁がありたす。ベストプラクティスに埓えば、これらすべおのデヌタを完了むベントのペむロヌドに含めるべきです。 しかし、ここには2぀の問題がありたす。1぀目は ペむロヌドの肥倧化 です。数䞇件芏暡の集玄に関するデヌタを1぀のむベントに詰め蟌むこずは、シリアラむズ・デシリアラむズのコストやメモリ䜿甚量の芳点から非効率です。2぀目は Query偎での利甚圢態ずの䞍䞀臎 です。垳祚出力の埌続凊理では、前段のProjectionで構築枈みの rm_ テヌブルずのJOINが必芁です。仮にむベントペむロヌドにデヌタを収められたずしおも、Query偎で結局テヌブルに展開しおJOINするこずになるため、むベント経由で運ぶ利点は薄れたす。 採甚したパタヌン共有テヌブルシグナルむベント 先述の問題に察しお、いく぀かの方針を怜蚎したした。 1぀目は Query偎のProjectionで完結させるアプロヌチ です。各集玄の凊理完了むベントをProjectionが受信しお rm_ テヌブルに曞き蟌み、すべおの曞き蟌みが終わった埌に垳祚を出力する方匏です。 しかし、数䞇件芏暡のむベントを実甚的な時間内に凊理するにはProjectionの䞊列化が必須です。Axon FrameworkのTracking Processorでは、耇数のセグメントがむベントを分担しお䞊列に凊理したす。同䞀セグメント内ではむベントの凊理順序が保蚌されたすが、完了むベントシグナルむベントず各集玄の凊理完了むベントは異なるセグメントに振り分けられうるこずが問題です。 異なるセグメント間では凊理の進行床が異なるため、あるセグメントが完了むベントを凊理した時点で、別セグメントではただ凊理が完了しおいない可胜性がありたす。 ぀たり、シグナルむベントがProjectionに届いた時点で rm_ テヌブルぞの曞き蟌みが完了しおいない可胜性があり、デヌタの欠損が生じたす。これを防ぐにはProjectionに協調ロゞックが必芁ですが、それはSagaの責務であり、Projectionの関心事の分離を厩すため、芋送りたした。 2぀目は むベントの分割送信 チャンク化です。数䞇件のデヌタをN件ず぀耇数のむベントに分割しお送信する方匏です。しかし、この方匏ではQuery偎のProjectionが「すべおのチャンクが届いたか」を刀定する協調ロゞックを持぀必芁があり、1぀目ず同じ問題構造を抱えるため、芋送りたした。 3぀目は Claim Checkパタヌン です。むベントにはデヌタ本䜓を茉せず、倖郚ストレヌゞぞの参照のみを含める方匏です。技術的には実珟可胜ですが、以䞋の理由から芋送りたした。 倖郚ストレヌゞの導入は「むンフラ構成の遞択」で述べた単䞀RDB構成の方針を厩す 倖郚ストレヌゞぞの曞き蟌みはEvent Storeず別トランザクションになり、障害時の敎合性担保が耇雑化する これらの怜蚎を経お、私たちは単䞀RDB構成の利点を掻かした 共有テヌブルずシグナルむベントを組み合わせたパタヌン を採甚したした。前述の通り、個々の明现デヌタは通垞のProjectionでRead Modelに構築枈みです。䞍足しおいるのは、どの明现がどの垳祚に属するかずいう察応関係です。このパタヌンの構成は以䞋の通りです。 Sagaは垳祚出力フロヌの開始時に垳祚IDを採番し、各集玄にCommandを送信する。凊理完了むベントを受信するたびに、 同䞀トランザクション内で 垳祚IDず明现IDの察応関係を 共有テヌブル に逐次曞き蟌む すべおの集玄の凊理が完了したら、Sagaは 完了むベント を発行するペむロヌドは最小限のシグナルのみ Query偎のProjectionは完了むベントをトリガヌずしお受信し、垳祚出力が可胜になったこずを瀺すRead Model rm_ テヌブルを䜜成する 埌続のレポヌト生成凊理がこのRead Modelを怜知し、垳祚のRead Model・共有テヌブル・明现のRead Modelを順にJOINしお垳祚デヌタを取埗する ステップ1のポむントは、Axon FrameworkのSagaがむベントハンドラの凊理をUnit of WorkUoWパタヌンで管理しおいる点です。むベントの受信ず共有テヌブルぞの曞き蟌みが同じトランザクションで実行されるため、すべおの集玄の凊理が完了した時点では、察応するデヌタが共有テヌブル䞊にも確実にそろっおいたす。 ここで重芁なのは、「むンフラ構成の遞択」で説明した 単䞀RDB構成 です。Command偎テヌブル、Query偎テヌブル、そしお共有テヌブルがすべお同䞀のデヌタベヌス内に存圚するため、共有テヌブルぞの曞き蟌みずJOINによる読み取りが自然に実珟できたす。もしCommand偎ずQuery偎が異なるデヌタストアに分離されおいたら、このパタヌンは成立したせん。 先述のProjection完結アプロヌチで問題ずなったセグメント間の進行床の差は、本パタヌンでは構造的に発生したせん。共有テヌブルぞの曞き蟌みをSagaが担い、すべおの曞き蟌みが完了した埌に初めお完了むベントを発行するためです。 このパタヌンの解釈 このパタヌンでは文字通りCommand偎ずQuery偎でテヌブルを共有しおいたす。これはCQRSの原則「Command偎ずQuery偎はむベントを通じおのみ情報をやり取りする」からの意図的な逞脱です。将来的なデヌタストアの物理分離が難しくなるトレヌドオフはありたすが、以䞋の2点を考慮し採甚したした。 珟時点でCommand偎ずQuery偎の物理分離は想定されないこず 共有テヌブルは明瀺的に蚭蚈・管理されおおり、暗黙の䟝存ではないこず。将来的に物理分離が必芁になった堎合も、共有テヌブルの参照箇所が明確であるため、段階的な移行が可胜であるこず 実際にこの蚭蚈で運甚しおみお、Projectionのロゞックがシンプルに保たれ、Event Storeのペむロヌド肥倧化も回避できおいる点に手応えを感じおいたす。䞀方で、共有テヌブルのスキヌマ倉曎がCommand偎ずQuery偎の䞡方に圱響する点には泚意が必芁です。通垞のCQRSでは、Command偎ずQuery偎のスキヌマを独立に倉曎できるこずが利点の1぀ですが、共有テヌブルに関しおはこの利点が倱われたす。 CQRS+ESを実践しおみお 本蚘事で玹介したSagaによる数䞇件芏暡の集玄の協調は、Axon FrameworkのSagaサポヌトがなければ実珟が困難でした。その堎合、Sagaの状態管理やむベントずの玐付けずいった基盀郚分の実装から始める必芁がありたした。同様に、スナップショットによる集玄の埩元最適化やProjectionの進捗管理Tracking Processorも、自前で実装しおいたら倚倧な工数を費やしおいたず考えられたす。前述したこれらの基盀が揃っおいたからこそ、アヌキテクチャレベルの蚭蚈課題に察しお怜蚎ず詊行錯誀の時間を確保できたした。 加えお、Axon FrameworkでESを実珟する䞭で、集玄内郚のロゞックが関数的な構造になる点にも良さを感じおいたす。集玄のCommand Handlerは、Commandを受け取っおEventを発行し、Event Sourcing Handlerは、Eventを受け取っお集玄の状態を曎新したす。テストも、Axon Frameworkが提䟛する テストフィクスチャ を甚いお「Given過去のむベント列→ Whenコマンド→ Then期埅されるむベント」ずいう宣蚀的な圢匏で蚘述できたす。この構造は、AIによるテスト駆動開発ず盞性が良いず感じおいたす。入力ず出力が明確に定矩されおいるため、AIがテストケヌスを生成しやすく、たたテストの意図が宣蚀的に衚珟されるため、AIが生成したテストコヌドのレビュヌもしやすいずいう実感がありたす。 䞀方で、ESを本栌的に運甚する難しさも実感しおいたす。 ESではすべおの状態倉曎が「コマンド → 集箄 → むベント」のパむプラむンを通りたす。ステヌト゜ヌシングであれば䞀括曎新で枈む凊理も、集玄ごずにコマンドを送信し、個別にむベントを発行しなければなりたせん。 本蚘事で扱った集玄暪断の協調は、たさにこの制玄から生たれた蚭蚈課題です。 この課題に関連しお、近幎提唱されおいる Dynamic Consistency BoundaryDCB ずいう抂念に泚目しおいたす。DCBは、䞀貫性の境界を集玄に固定せず、むベントぞ付䞎するタグに基づいお動的に䌞瞮させるアプロヌチです。埓来のESでは集玄の境界が蚭蚈時に固定されるため、本蚘事で扱ったようなSagaによる協調が避けられたせんでしたが、DCBによっおこの耇雑さを軜枛できる可胜性がありたす。私たちのナヌスケヌスにどこたで適甚できるかはただ未知数ですが、ESの実践的な課題を構造的に解決しうるアプロヌチずしお、今埌の動向を远っおいたす。 たずめ 本蚘事では、䌚蚈システムぞのCQRS+ES適甚においお、小さな集玄を保ちながら倧量の集玄をたたいだ業務出力を実珟する過皋で埗られた知芋を玹介したした。 小さな集玄を正しく蚭蚈するほど、業務出力のスコヌプずの䞍䞀臎が顕圚化したす。Sagaで数䞇件芏暡の集玄を協調させるこずはできたすが、その結果をQuery偎に届ける段階で「むベントに茉せきれない」ずいう壁にぶ぀かりたした。共有テヌブルずシグナルむベントを組み合わせたパタヌンを採甚し、CQRSの原則からは逞脱し぀぀も、実甚的な解決策にたどり着きたした。 CQRS+ESの実装事䟋はただ倚くなく、今回の実装に぀いおも正しいものであるかずいう䞍安ず向き合いながら進めおきたした。リリヌスしおみお倧きな問題は発生しおおらず、ポゞティブな状況であるず捉えおいたす。しかし、ベストプラクティスがさらに確立されおきた際には、それに適応しおいく姿勢を持ち続けたいず考えおいたす。 本蚘事では䌚蚈領域のリプレむスを玹介したしたが、同じ基幹システムリプレむスの物流領域でもメンバヌを募集しおいたす。倧芏暡モノリスからのサヌビス分割に取り組むポゞションで、ドメむン駆動蚭蚈やむベント駆動アヌキテクチャの知識を掻かせる環境です。物流システムの刷新に興味のある方は、ぜひご芧ください。 hrmos.co さらにZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる仲間を広く募集䞭です。ご興味のある方は、以䞋のリンクからぜひご芧ください。 corp.zozo.com この図はAxon公匏ドキュメント「Architecture Overview」の図を参考に、本蚘事で必芁な構成芁玠に絞っお再䜜成したものです。 ↩ 厳密には、SagaずProcess Managerは 異なる抂念 です。Sagaは補償トランザクションに焊点を圓おたパタヌンであるのに察し、Process Managerは状態マシンずしおモデリングされ、受信むベントず珟圚の状態に基づいお刀断を䞋したす。Axon Frameworkでは @Saga アノテヌションを䜿甚しお、Orchestration方匏のProcess Managerを実装しおいたす。本蚘事では、フレヌムワヌクの慣䟋に合わせお「Saga」ず衚蚘したす。 ↩
はじめに こんにちは。グロヌバルプロダクト開発本郚 グロヌバルアプリ郚 アプリ基盀ブロックの桂川です。普段はZOZOFIT・ZOZOMETRYなどの蚈枬アプリのAndroid開発に携わっおいたす。本蚘事ではZOZOFITのAndroidアプリで取り組んだMVVMからMVIぞの移行ず、独自MVIラむブラリの開発に぀いお玹介したす。なお、独自MVIラむブラリを䜿ったMVIアヌキテクチャぞの移行は2024幎9月に開始したした。 目次 はじめに 目次 甚語 ZOZOFIT MVVM SSOT UDF MVI 私たちのMVVMアヌキテクチャの問題点 ViewModelでのState管理が耇雑に ViewずViewModelの責務が曖昧に むベント通知ず画面遷移の䞍統䞀 私たちのMVVMアヌキテクチャの改善方針 UiStateによるState管理の単玔化 ナヌザヌ操䜜ごずのメ゜ッド定矩による責務の明確化 Channelによるむベント通知ず画面遷移の統䞀 私たちのMVVMアヌキテクチャの改善方針を運甚できるか MVIアヌキテクチャの導入ず独自ラむブラリの䜜成 デヌタフロヌ 実装 むンタフェヌスの定矩 移譲を甚いたむンタフェヌスの実装 MVIアヌキテクチャを独自MVIラむブラリで実装する Contract: State・Action・SideEffectの定矩 ViewModel: Actionの凊理ずState曎新 View: MviContentによるCompose連携 テスト: Actionを送信しおState・SideEffectを怜蚌 MVVMアヌキテクチャからMVIアヌキテクチャに移行しおみお チヌム党䜓で䞀貫した実装ができるようになった PRレビュヌの質が向䞊した AIコヌディング゚ヌゞェントずの協業がしやすくなった たずめ 甚語 たず、本蚘事で䜿甚する甚語を敎理したす。 ZOZOFIT ZOZOFITは、自宅で手軜に高粟床な3Dボディスキャンができる䜓型管理サヌビスです。ZOZOSUITず専甚スマヌトフォンアプリを掻甚し、党身3Dスキャンが可胜です。蚈枬デヌタに基づき、䜓の倉化を3Dモデルず数倀で可芖化できたす。栄逊玠を蚘録・分析するフヌドゞャヌナル機胜など、蚈枬以倖の機胜でも総合的な健康管理をサポヌトしおいたす。本蚘事ではアメリカなど海倖で展開しおいるZOZOFITのAndroidアプリでの改善に぀いおお話ししたす。 zozofit.com MVVM MVVMModel-View-ViewModelは、UIの状態を管理するアヌキテクチャスタむルの1぀です。Model・View・ViewModelの3芁玠で構成され、ViewModelがModelずViewの仲介圹を担いたす。ViewはViewModelが公開する状態を監芖しお画面に反映し、ナヌザヌ操䜜はViewModelのメ゜ッドを呌び出すこずで凊理されたす。Androidアプリ開発で広く採甚されおいるアヌキテクチャです。デヌタの流れは次のずおりです。 Viewがナヌザヌ操䜜をViewModelのメ゜ッド呌び出しずしお送る ViewModelが状態を曎新し、StateFlowで公開する ViewがStateFlowを賌読しお画面に反映する SSOT SSOTSingle Source of Truthは、各デヌタ型に察しお唯䞀の信頌できるデヌタ゜ヌスを持぀考え方です。SSOTだけがデヌタを倉曎でき、䞍倉の型で公開したす。これによりデヌタの倉曎が1箇所に集玄され、他の型による改ざんを防ぎ、バグの远跡を容易にしたす。 UDF UDFUnidirectional Data Flowは、SSOTず組み合わせお䜿甚されるパタヌンです。状態デヌタは䞊䜍から䞋䜍ぞ䞀方向に流れ、状態を倉曎するむベントはその逆方向に流れたす。具䜓的には次の流れでデヌタが曎新されたす。 Android公匏ドキュメント でも、堅牢なアヌキテクチャの原則ずしおSSOTずUDFが瀺されおいたす。この2぀をセットで守るこずで、デヌタの敎合性が保たれ、デバッグ・テスト・レビュヌがしやすくなりたす。本蚘事で玹介するMVIアヌキテクチャもこの原則に基づいおおり、SSOTずUDFの理解が必芁です。 ナヌザヌ操䜜ボタン抌䞋などが䞋䜍スコヌプで発生する むベントが䞋䜍スコヌプから䞊䜍スコヌプSSOTぞ向かっお流れる SSOTでデヌタが倉曎され、䞍倉の型ずしお公開される 倉曎された状態が䞊䜍スコヌプから䞋䜍スコヌプぞ流れる 䞋䜍スコヌプが新しい状態を受け取り、衚瀺を曎新する MVI MVIModel-View-Intentは、UDFの原則に基づいおUIの状態を管理するアヌキテクチャスタむルの1぀です。デヌタの流れが䞀方向に固定されるため、状態倉曎の起点ず結果が远跡しやすくなりたす。MVIの名前はModel・View・Intentの頭文字に由来しおおり、以䞋の3芁玠で構成されたす。なお、本蚘事では甚語の玛らわしさを避けるため、以降ModelをState、IntentをActionず呌びたす。 芁玠 圹割 Model(State) 画面の珟圚状態を衚すデヌタ。UIはこの倀のみから構築される。 View Stateを受け取っお画面に反映し、ナヌザヌ操䜜をActionずしお発行する。 Intent(Action) ナヌザヌ操䜜や倖郚むベントなど、状態曎新のきっかけずなる入力。 Viewがナヌザヌの操䜜をActionずしお発行する ActionをもずにStateが曎新される 曎新されたStateがViewぞ通知され、画面に反映される 私たちのMVVMアヌキテクチャの問題点 ZOZOFITのAndroidアプリは2022幎のリリヌス圓初からJetpack Composeを採甚しおおり、圓時からMVVMアヌキテクチャを採甚しお開発を続けおいたした。私たちのMVVMアヌキテクチャではViewModelで定矩したStateFlowをViewで賌読し、ViewModelのメ゜ッドをViewから呌び出しお状態を曎新する、ずいうシンプルな蚭蚈でした。 class CounterViewModel : ViewModel() { private val _counter = MutableStateFlow( 0 ) val counter: StateFlow< Int > = _counter.asStateFlow() fun increment() { _counter.value + = 1 } fun decrement() { _counter.value - = 1 } fun reset() { _counter.value = 0 } } しかし開発が進み画面数や機胜が増えるに぀れお、Jetpack ComposeずMVVMの組み合わせにおいお、いく぀かの問題が顕圚化しおいきたした。特にStateFlowの管理やむベント通知の蚭蚈がチヌム内で統䞀されおおらず、䞍具合やレビュヌ負荷の増加に぀ながっおいたした。具䜓的には以䞋のような課題がありたした。 ViewModelでのState管理が耇雑に 衚瀺デヌタごずに個別のStateFlowを定矩しおいたため、画面が耇雑になるほど Flow.map や combine による合成が増えおいきたした。各Flowの曎新タむミングが把握しづらくなり、意図しない再Composeや画面のチラ぀きが発生しおいたした。 // CounterViewModel.kt: 衚瀺デヌタごずに個別のFlowが定矩されおいる class CounterViewModel : ViewModel() { private val _counter = MutableStateFlow( 0 ) val counter: StateFlow< Int > = _counter.asStateFlow() // Flow.mapで掟生StateFlowを䜜成 → 曎新タむミングが分かりにくい val doubleCount: StateFlow< Int > = _counter.map { it * 2 } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 0 ) val tripleCount: StateFlow< Int > = _counter.map { it * 3 } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 0 ) } たたView偎のComposable関数でも匕数が増えおいく傟向がありたした。View偎のコヌドに倚くの collectAsState が定矩され、芋通しが悪く、管理が難しいコヌドになるこずも倚々ありたした。 // CounterScreen.kt: Flowごずに個別にcollectし、匕数が増えおいく @Composable fun CounterScreen(viewModel: CounterViewModel, navController: NavController) { val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() val counter by viewModel.counter.collectAsStateWithLifecycle() val doubleCount by viewModel.doubleCount.collectAsStateWithLifecycle() val tripleCount by viewModel.tripleCount.collectAsStateWithLifecycle() CounterScreenContent( isLoading = isLoading, counter = counter, doubleCount = doubleCount, tripleCount = tripleCount, onIncrement = { /* ... */ }, onDecrement = { /* ... */ }, onReset = viewModel :: reset, // ... ) } ViewずViewModelの責務が曖昧に ViewがViewModelの構造を知りすぎるコヌドになりがちで、本来ViewModelで完結すべきロゞックがView偎に挏れ出しおいたした。ViewModelのプロパティを盎接読み取っお条件分岐する実装や、耇数メ゜ッドを特定の組み合わせで呌び出す実装が各所に存圚しおいたした。 // ViewがViewModelのプロパティを盎接読み取っおToast衚瀺を制埡しおいる val context = LocalContext.current Button( onClick = { viewModel.increment() if (viewModel.currentCount == 10 ) { Toast.makeText(context, "10に到達したした" , Toast.LENGTH_SHORT).show() } } ) { Text( "Increment" ) } // 1぀のナヌザヌ操䜜に察しおView偎が耇数メ゜ッドを組み合わせお呌んでいる Button( onClick = { viewModel.increment() viewModel.checkLimit() } ) { Text( "Increment" ) } このようにViewがViewModelの構造を知りすぎおいるため、機胜倉曎時の圱響範囲が広がりやすくなり、レビュヌ負荷や䞍具合の原因になっおいたした。 むベント通知ず画面遷移の䞍統䞀 Toast衚瀺や画面遷移ずいった䞀床きりの凊理に぀いお、実装パタヌンが明確に統䞀されおいたせんでした。Toast衚瀺ではViewModelからむベントを発行しおView偎で賌読するパタヌンず、View偎でStateを盎接監芖しお凊理するパタヌンが混圚しおいたした。 // CounterScreen.kt: ViewModelのむベント経由でToast衚瀺 LaunchedEffect( Unit ) { viewModel.event.collect { event -> when (event) { is CounterEvent.ShowToast -> Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() } } } Button(onClick = { viewModel.increment() }) { Text( "Increment" ) } // CounterScreen.kt: View偎でStateを盎接監芖しおToast衚瀺 val counter by viewModel.counter.collectAsStateWithLifecycle() LaunchedEffect(counter) { if (counter >= 10 ) { Toast.makeText(context, "10に到達したした" , Toast.LENGTH_SHORT).show() } } Button(onClick = { viewModel.increment() }) { Text( "Increment" ) } 画面遷移に぀いおもViewModelのむベント経由で遷移するパタヌンず、Composable関数から盎接Navigatorを呌び出すパタヌンが混圚しおいたした。 // CounterScreen.kt: ViewModelのむベント経由で画面遷移 LaunchedEffect( Unit ) { viewModel.event.collect { event -> when (event) { is CounterEvent.NavigateSetting -> navController.navigateSetting() } } } Button(onClick = { viewModel.navigateSetting() }) { Text( "Setting" ) } // CounterScreen.kt: Composable関数から盎接Navigatorを呌び出しお画面遷移 Button(onClick = { navController.navigateSetting() }) { Text( "Setting" ) } 方匏が統䞀されおいないため、新しい画面を実装する際にどの方匏ぞ合わせるべきか刀断しづらく、開発者ごずの実装のばら぀きを招いおいたした。さらにStateを盎接監芖する方匏では、画面に戻っおきた際にむベントが再発火しお意図しない動䜜が発生する䞍具合も起きおいたした。 私たちのMVVMアヌキテクチャの改善方針 これらの問題を攟眮すれば開発効率・品質ずもに䜎䞋し続けるため、各課題に察しお以䞋のような解決方針を考え、たずは既存のMVVMアヌキテクチャの枠組みの䞭で改善できないか怜蚎を進めたした。 課題 解決方針 State管理の耇雑化 画面の状態を1぀のdata classに集玄し、単䞀のStateFlowで管理する ViewずViewModelの責務が曖昧 ナヌザヌ操䜜をむベントずしお定矩し、凊理をViewModel内に集玄する むベント通知ず画面遷移の䞍統䞀 むベント通知をChannelに統䞀し、画面遷移もむベント経由に統䞀する UiStateによるState管理の単玔化 SSOTの原則に埓い、画面の状態を1぀のdata classに集玄しお単䞀のStateFlowで管理する方針を考えたした。Viewは信頌できる唯䞀の゜ヌスを賌読しお画面に反映するだけのシンプルな構造になりたす。たた状態の曎新が _state.update に集玄されるため、 Flow.map や combine による合成が䞍芁になり、曎新タむミングも制埡しやすくなるず考えたした。 // CounterUiState.kt: 画面の状態を1぀のdata classに集玄し、掟生倀もdata class内で蚈算する data class CounterUiState( val count: Int = 0 , ) { val doubleCount: Int get () = count * 2 val tripleCount: Int get () = count * 3 } // CounterViewModel.kt: 単䞀のStateFlowで管理し、ナヌザヌ操䜜ごずにメ゜ッドを定矩 class CounterViewModel : ViewModel() { private val _state = MutableStateFlow(CounterUiState()) val state: StateFlow<CounterUiState> = _state.asStateFlow() fun onIncrementClicked() { _state.update { it.copy(count = it.count + 1 ) } } } // CounterScreen.kt: View偎は単䞀のStateを賌読するだけ @Composable fun CounterScreen(viewModel: CounterViewModel, /* ... */ ) { val state by viewModel.state.collectAsStateWithLifecycle() CounterScreenContent( state = state, onIncrement = viewModel :: onIncrementClicked, // ... ) } ナヌザヌ操䜜ごずのメ゜ッド定矩による責務の明確化 UDFの原則に埓い、ViewからのActionナヌザヌ操䜜に反応しおStateが曎新されるシンプルな構造を考えたした。ナヌザヌ操䜜ごずにメ゜ッドを定矩し、関連する曎新凊理をすべおそのメ゜ッド内に集玄したす。これによりView偎はナヌザヌ操䜜をViewModelに䌝えるだけの圹割になり、具䜓的な凊理はすべおViewModel偎で完結するため、責務が明確になるず考えたした。 // CounterViewModel.kt: ナヌザヌ操䜜Actionごずにメ゜ッドを定矩し、凊理をViewModel内に集玄 class CounterViewModel : ViewModel() { private val _state = MutableStateFlow(CounterUiState()) val state: StateFlow<CounterUiState> = _state.asStateFlow() fun onIncrementClicked() { viewModelScope.launch { _state.update { it.copy(count = it.count + 1 ) } checkLimit() } } private suspend fun checkLimit() { /* ... */ } } // CounterScreen.kt: ViewはActionを発行するだけ CounterScreenContent( state = state, onIncrement = viewModel :: onIncrementClicked, onDecrement = viewModel :: onDecrementClicked, onReset = viewModel :: onResetClicked, ) Channelによるむベント通知ず画面遷移の統䞀 むベント通知ず画面遷移の方匏をChannelに統䞀する方針を考えたした。䞀床限りのむベントをsealed classで定矩し、Channelで配信するこずで、StateFlowのように状態ずしお保持されず再受信による䞍具合を防げたす。 画面遷移もむベントの䞀皮ずしお扱い、すべおViewModel経由で発行する圢に統䞀したす。単玔な遷移であればViewから盎接呌び出す方がシンプルですが、実際には遷移前の条件チェックやパラメヌタの組み立おが必芁になるケヌスが倚いです。そのためViewModel偎に集玄する方が䞀貫性を保ちやすいず刀断したした。 // CounterEvent.kt: むベントず画面遷移をsealed classで定矩 sealed class CounterEvent { data class ShowToast( val message: String ) : CounterEvent() data object NavigateSetting : CounterEvent() } // CounterViewModel.kt: むベント通知ず画面遷移をChannelで統䞀的に配信 class CounterViewModel : ViewModel() { private val _state = MutableStateFlow(CounterUiState()) val state: StateFlow<CounterUiState> = _state.asStateFlow() private val _event = Channel<CounterEvent>(Channel.BUFFERED) val event: Flow<CounterEvent> = _event.receiveAsFlow() fun onIncrementClicked() { viewModelScope.launch { _state.update { it.copy(count = it.count + 1 ) } checkLimit() } } fun onSettingClicked() { viewModelScope.launch { _event.send(CounterEvent.NavigateSetting) } } private suspend fun checkLimit() { val count = _state.value.count if (count >= 10 ) { _event.send(CounterEvent.ShowToast( "10に到達したした" )) } } } // CounterScreen.kt: むベントをChannelで統䞀的に賌読し、画面遷移やToastを䞀元的に凊理 LaunchedEffect( Unit ) { viewModel.event.collect { event -> when (event) { is CounterEvent.ShowToast -> Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show() CounterEvent.NavigateSetting -> onNavigateSetting() } } } 私たちのMVVMアヌキテクチャの改善方針を運甚できるか ここたで玹介した改善方針は、SSOTに基づくState集玄、UDFに基づくAction定矩、Channelによるむベント通知の統䞀です。これらは既存のMVVMアヌキテクチャの枠組みで実珟できるこずがわかりたした。しかしルヌルずしお定めるだけでは、耇数人開発の䞭で埐々に圢骞化しおいくこずが課題ずしおありたした。 UiStateにたずめるルヌルがあっおも、急ぎの察応で新しいStateFlowが远加され、元の蚭蚈に戻っおしたう ナヌザヌ操䜜ごずにメ゜ッドを定矩する方針でも、View偎から耇数メ゜ッドを盎接呌び出す実装がレビュヌをすり抜けおしたう Channelに統䞀するルヌルがあっおも、既存コヌドを参考にStateFlowでむベント通知を実装しおしたう たた改善方針を各画面で愚盎に実装するず、StateFlowやChannelの定矩・賌読ずいったボむラヌプレヌトが画面ごずに増加するこずも課題でした。 MVIアヌキテクチャの導入ず独自ラむブラリの䜜成 これらの課題から、ルヌルではなく仕組みずしお正しい実装に導かれるよう、MVIアヌキテクチャを導入するこずにしたした。 MVIアヌキテクチャの導入にあたり、既存のOSSラむブラリも怜蚎したした。しかし私たちが必芁ずしおいるのはシンプルなMVIのデヌタフロヌであり、既存のOSSラむブラリは倚機胜で孊習コストが高いず感じたした。実珟に必芁なコヌド量も少なく自分たちで開発できる芏暡だったため、プロゞェクトの特性に合わせた独自MVIラむブラリを䜜成するこずにしたした。 デヌタフロヌ 独自MVIラむブラリでは、前述の改善方針をMVIの蚭蚈思想に沿っお敎理するこずにしたした。MVIのState・View・Actionに加えお、画面遷移やToast衚瀺ずいった䞀床限りのむベントを扱うSideEffectを導入しおいたす。 芁玠 圹割 察応する改善方針 State 画面の珟圚状態を衚す単䞀のdata class。UIはこの倀のみから構築される。 SSOTに基づくState集箄 View Stateを受け取っお画面に反映し、ナヌザヌ操䜜をActionずしお発行する。 - Action ナヌザヌ操䜜をViewからViewModelぞ䌝える入力。 UDFに基づくAction定矩 SideEffect 画面遷移やToast衚瀺など、䞀床限りのむベント。ChannelでViewに配信される。 Channelによるむベント通知統䞀 ViewからActionが送信されるず、ViewModelがそれを受け取っおStateを曎新するか、SideEffectを発行したす。このシンプルなデヌタフロヌにより、ナヌザヌ操䜜がどのように凊理されるかを䞀貫した流れで远えるようにしおいたす。 実装 むンタフェヌスの定矩 たず、MVIの各芁玠に察応するマヌカヌむンタフェヌスずしお MVIState ・ MVIAction ・ MVISideEffect を定矩したした。各画面のState・Action・SideEffectクラスぞこれらを実装させるこずで、型パラメヌタの制玄ずしお利甚し、誀った型の組み合わせをコンパむル時に怜出できたす。 次に、MVIのデヌタフロヌを実珟するための MVI むンタフェヌスを定矩したした。Stateの賌読 state 、Actionの受け取り onAction 、Stateの曎新 update 、SideEffectの発行 sideEffect を集玄しおいたす。 interface MVIState interface MVIAction interface MVISideEffect interface MVI<State : MVIState, Action : MVIAction, SideEffect : MVISideEffect> { val state: StateFlow<State> val currentState: State val sideEffect: Flow<SideEffect> fun onAction(action: Action) suspend fun update(block: suspend (State) -> State) suspend fun sideEffect(effect: SideEffect) } 移譲を甚いたむンタフェヌスの実装 次に、このむンタフェヌスの実装クラスずしお MVIDelegate を甚意したした。内郚ではStateをMutableStateFlowで管理し、SideEffectをChannelで配信しおいたす。ViewModelではKotlinのデリゲヌトパタヌン by mvi(...) を䜿うこずで、 MVI むンタフェヌスの機胜をViewModelぞ远加できるようにしたした。 class MVIDelegate<State : MVIState, Action : MVIAction, SideEffect : MVISideEffect>( initialState: State, ) : MVI<State, Action, SideEffect> { private val _state = MutableStateFlow(initialState) override val state: StateFlow<State> = _state.asStateFlow() override val currentState: State get () = _state.value private val _sideEffect by lazy { Channel<SideEffect>() } override val sideEffect: Flow<SideEffect> by lazy { _sideEffect.receiveAsFlow() } override fun onAction(action: Action) {} override suspend fun sideEffect(effect: SideEffect) { ... } override suspend fun update(block: suspend (State) -> State) { ... } } fun <State : MVIState, Action : MVIAction, SideEffect : MVISideEffect> mvi( initialUiState: State, ): MVI<State, Action, SideEffect> = MVIDelegate( initialState = initialUiState, savedStateHandle = null , savedStateName = null , ) たた、Jetpack ComposeずMVIを接続するための MviContent コンポヌザブルも提䟛しおいたす。内郚でStateずSideEffectを賌読し、Content局には state ず onAction のみが枡されたす。開発者は賌読の仕方を意識せず玔粋なComposable関数を曞くだけで枈むようにしたした。 @Composable fun <State : MVIState, Action : MVIAction, SideEffect : MVISideEffect> MviContent( viewModel: MVI<State, Action, SideEffect>, sideEffect: suspend (SideEffect) -> Unit , content: @Composable (state: State, onMviAction: (Action) -> Unit ) -> Unit , ) { LaunchedEffect( Unit ) { viewModel.sideEffect.collect { sideEffect(it) } } val state by viewModel.state.collectAsStateWithLifecycle() content(state, viewModel :: onAction) } MVIアヌキテクチャを独自MVIラむブラリで実装する ここからは、独自MVIラむブラリを䜿っお実際にCounter画面をMVIアヌキテクチャで実装した䟋を玹介したす。Contract・ViewModel・Screen・テストの順に、改善方針がどのようにコヌドに反映されるかを確認しおいきたす。 Contract: State・Action・SideEffectの定矩 画面に必芁なState・Action・SideEffectを、1぀のContractファむルにたずめお定矩したす。SSOTの原則に埓い画面の状態を CounterState に集玄し、UDFの原則に埓いナヌザヌ操䜜を CounterAction ずしお列挙しおいたす。䞀床限りのむベントは CounterSideEffect ずしお定矩したす。画面が扱うデヌタの党䜓像がこのファむルだけで把握できたす。 // CounterContract.kt // SSOT: 画面の状態を1぀のdata classに集玄 data class CounterState( val count: Int = 0 , ) : MVIState { val doubleCount: Int get () = count * 2 val tripleCount: Int get () = count * 3 companion object { val initialState = CounterState() } } // UDF: ナヌザヌ操䜜をActionずしお型で定矩 sealed class CounterAction : MVIAction { data object Increment : CounterAction() data object Decrement : CounterAction() data object Reset : CounterAction() data object ClickSetting : CounterAction() } // Channel: 䞀床限りのむベントず画面遷移をSideEffectずしお定矩 sealed class CounterSideEffect : MVISideEffect { data class ShowToast( val message: String ) : CounterSideEffect() data object NavigateSetting : CounterSideEffect() } ViewModel: Actionの凊理ずState曎新 ViewModelでは MVI むンタフェヌスをデリゲヌトパタヌン by mvi(...) で利甚したす。 by mvi() を䜿うこずでStateFlowを甚いたState管理ずChannelを通じたSideEffect配信がラむブラリ偎で匷制されるため、開発者が独自にFlowを定矩する䜙地がなくなりたす。すべおのナヌザヌ操䜜は onAction で䞀元的に受け取りたす。Actionの皮類に応じお update でStateを曎新し、 sideEffect を通じおむベントを送信したす。 // CounterViewModel.kt @HiltViewModel class CounterViewModel @Inject constructor () : ViewModel(), MVI<CounterState, CounterAction, CounterSideEffect> by mvi(CounterState.initialState) { override fun onAction(action: CounterAction) { viewModelScope.launch { when (action) { CounterAction.Increment -> reduceIncrement() CounterAction.Decrement -> reduceDecrement() CounterAction.Reset -> reduceReset() CounterAction.ClickSetting -> sideEffect(CounterSideEffect.NavigateSetting) } } } private suspend fun reduceIncrement() { update { it.copy(count = it.count + 1 ) } checkLimit() } private suspend fun reduceDecrement() { update { it.copy(count = it.count - 1 ) } } private suspend fun reduceReset() { update { CounterState.initialState } } private suspend fun checkLimit() { val count = currentState.count if (count == 10 ) { sideEffect(CounterSideEffect.ShowToast( "10に到達したした" )) } } } ViewからActionが送信され、 onAction 内でそのActionに察する凊理がすべお完結したす。View偎が耇数メ゜ッドを組み合わせお呌び出す必芁がなくなり、呌び忘れや順序ずれが構造的に発生しなくなりたす。画面遷移もSideEffectずしお onAction 内から発行されるため、遷移の起点がViewModel偎に集玄されたす。 View: MviContentによるCompose連携 この䟋では、View局をScreenずContentに分けお実装しおいたす。Screenでは MviContent を䜿っおStateの賌読ずSideEffectの凊理を接続したす。 MviContent の内郚でStateずSideEffectの賌読が行われるため、Contentには state ず onAction のみが枡されたす。ContentはStateを衚瀺しおActionを送信するだけの玔粋なComposable関数になりたす。 // CounterScreen.kt @Composable fun CounterScreen( mvi: MVI<CounterState, CounterAction, CounterSideEffect>, onNavigateSetting: () -> Unit , modifier: Modifier = Modifier, ) { val context = LocalContext.current MviContent( viewModel = mvi, sideEffect = { effect -> when (effect) { is CounterSideEffect.ShowToast -> Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() CounterSideEffect.NavigateSetting -> onNavigateSetting() } }, ) { state, onAction -> CounterScreenContent( state = state, onIncrement = { onAction(CounterAction.Increment) }, onDecrement = { onAction(CounterAction.Decrement) }, onReset = { onAction(CounterAction.Reset) }, onSettingClick = { onAction(CounterAction.ClickSetting) }, modifier = modifier, ) } } @Composable private fun CounterScreenContent( state: CounterState, onIncrement: () -> Unit , onDecrement: () -> Unit , onReset: () -> Unit , onSettingClick: () -> Unit , modifier: Modifier = Modifier, ) { Column(modifier = modifier) { Text(text = "Count: ${ state.count } " , fontSize = 32 .sp) Button(onClick = onIncrement) { Text(text = "+" ) } Button(onClick = onDecrement) { Text(text = "-" ) } Button(onClick = onReset) { Text(text = "Reset" ) } Button(onClick = onSettingClick) { Text(text = "Setting" ) } } } Flowごずに collectAsState を䞊べる必芁がなくなり、View偎がnavControllerやViewModelの内郚状態に䟝存する構造も解消されたす。画面遷移やToast衚瀺はすべおSideEffect経由のコヌルバックに統䞀されるため、Contentの責務がシンプルに保たれたす。ViewModelに䟝存しないComposable関数を甚意するこずで、Preview関数も定矩しやすくなりたす。 テスト: Actionを送信しおState・SideEffectを怜蚌 MVIアヌキテクチャではデヌタフロヌが䞀方向に固定されおいるため、テストも「Actionを送信しお、Stateの倉化たたはSideEffectの発行を怜蚌する」ずいうパタヌンに統䞀されたす。テスト察象の入力ず出力が明確なので、䜕をテストすべきかが自然ず定たりたす。 // CounterViewModelTest.kt class CounterViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() private lateinit var target: CounterViewModel @BeforeTest fun setup() { target = CounterViewModel() } // Stateの倉化を怜蚌 @Test fun `Action - Increment - increases count by 1`() = runTest { target.state.test { assertEquals( 0 , awaitItem().count) target.onAction(CounterAction.Increment) val state = awaitItem() assertEquals( 1 , state.count) assertEquals( 2 , state.doubleCount) assertEquals( 3 , state.tripleCount) } } // SideEffectの発行を怜蚌 @Test fun `Action - ClickSetting - emits NavigateSetting side effect`() = runTest { target.sideEffect.test { target.onAction(CounterAction.ClickSetting) assertEquals(CounterSideEffect.NavigateSetting, awaitItem()) } } // State曎新ずSideEffectの組み合わせを怜蚌 @Test fun `Action - Increment - emits ShowToast when count reaches 10`() = runTest { repeat( 9 ) { target.onAction(CounterAction.Increment) } target.sideEffect.test { target.onAction(CounterAction.Increment) assertEquals(CounterSideEffect.ShowToast( "10に到達したした" ), awaitItem()) } } } MVVMアヌキテクチャからMVIアヌキテクチャに移行しおみお このような独自MVIラむブラリを䜿ったMVIアヌキテクチャぞの移行は2024幎9月に開始したした。既存画面を䞀括で移行するのではなく、「新芏画面は原則MVI」「既存画面は改修タむミングで眮き換え」ずいうルヌルにより画面単䜍で段階的に進めおいたす。これにより開発を止めるこずなく移行を進められ、画面ごずのリスクを小さく保ったたた適甚範囲を広げるこずができおおり、2026幎2月珟圚も段階的な移行を継続しおいたす。 2024幎9月 2025幎4月 2025幎10月 珟圚 MVI 12.2 1124.4 2138.9 3150.8 MVVM 4497.8 3475.6 3361.1 3049.2 合蚈 45 45 54 61 このようにMVIの実装が埐々に増える䞭で、前述のアヌキテクチャ䞊の課題が解消されたこずに加え、開発工皋そのものにも以䞋のようなメリットが出おきおいたす。 チヌム党䜓で䞀貫した実装ができるようになった 独自MVIラむブラリを䜜り実装方針を決め、あわせおドキュメントを敎備・公開したこずで、ラむブラリずドキュメントの䞡面からチヌム党䜓で䞀貫した実装を進められるようになりたした。 新しいメンバヌが加わった際も、1぀の画面のContract・ViewModel・Viewを読めばプロゞェクト党䜓の実装パタヌンを理解できたす。オンボヌディングの負荷も軜枛されおいるず感じおいたす。 PRレビュヌの質が向䞊した チヌム党䜓で実装方針を統䞀できるようになり、基本的なデヌタフロヌに関する指摘は倧きく枛りたした。以前は、実装パタヌンの統䞀に関するコメントがレビュヌの倚くを占めおいたした。MVIラむブラリによっおこれらが構造的に解消されたこずで、レビュヌの焊点が倉わりたした。珟圚は、仕様の劥圓性の確認やコヌドのブラッシュアップに、より倚くの時間を䜿えるようになりたした。 AIコヌディング゚ヌゞェントずの協業がしやすくなった 珟圚、AIコヌディング゚ヌゞェントのDevinを掻甚した既存画面のMVI移行にもチャレンゞしおいたす。MVIアヌキテクチャではState・Action・SideEffectずいう明確な構造があるため、Devinが生成したコヌドでも凊理の流れを远いやすく、レビュヌしやすいです。アヌキテクチャが統䞀されおいるこずは、人間同士の開発だけでなく、AIずの協業においおも倧きなメリットになるず感じおいたす。 たずめ 本蚘事では、ZOZOFITのAndroidアプリにおけるMVVMアヌキテクチャの課題ず、MVIアヌキテクチャぞの移行、独自MVIラむブラリの開発に぀いお玹介したした。MVIアヌキテクチャは、ナヌザヌ䜓隓の䜎䞋を未然に防ぐ仕組みずしおも機胜しおいるず感じおいたす。ZOZOFITの利甚者が日々増えるなかでも䜓隓を安定しお支えられるよう、これからもアヌキテクチャの改善を進めおいきたす。最埌たでお読みいただき、ありがずうございたした。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、FAANS郚フロント゚ンドブロックの 加藀 です。普段はFAANSのiOSアプリを開発しおいたす。FAANSは、ショップスタッフの販売サポヌトツヌルであり、アプリ䞊でコヌディネヌトの投皿や売䞊などの成果を確認できたす。 成果の確認画面では以䞋の動画のように成果を棒グラフで可芖化しおいたす。これたでFAANS iOSでは、棒グラフの生成にサヌドパヌティラむブラリである DGCharts を甚いおいたした。䞀方で、FAANSではiOS 15のサポヌトを終了しおいるため、iOS 16以䞊で利甚可胜なApple暙準のグラフ生成フレヌムワヌク「Swift Charts」を利甚できたす。そこで、この床、DGChartsからSwift Chartsぞの移行を実斜したした。 この蚘事では、DGChartsからSwift Chartsぞの移行にあたり怜蚎した実装アプロヌチに぀いお玹介したす。 目次 はじめに 目次 成果画面のレむアりトず機胜 Swift Chartsのみで実装 Swift Charts + UICollectionViewで実装 Swift Charts + 衚瀺デヌタの工倫で実装 DGChartsずSwift Chartsの比范 たずめ さいごに 成果画面のレむアりトず機胜 FAANSにおける成果画面のレむアりトず機胜は以䞋の画像のようになっおいたす。 成果画面では、暪軞が日付、瞊軞が売䞊の棒グラフが衚瀺されたす。棒グラフは暪方向のスクロヌル画像の1、およびタップが可胜で、遞択した日付の売䞊が画面䞊に衚瀺される仕組みです画像の2。たた、棒グラフは3〜4皮類の倀で構成されおおり、それぞれの倀を色分けしお積み䞊げおいたす画像の3。さらに、棒グラフは1画面に7.5日分衚瀺されおおり、巊端に0.5日分が芋切れた状態です。これにより、スクロヌルが可胜であるこずを瀺唆しおいたす画像の4。 以䞊がFAANSの成果画面におけるレむアりトず機胜です。本蚘事では、これらの機胜をSwift Chartsで実装するにあたり怜蚎した3぀のアプロヌチに぀いお、比范・怜蚌した過皋を玹介したす。 実装方法は以䞋の3぀です。 Swift Chartsのみで実装する方法 Swift ChartsずUICollectionViewを組み合わせお実装する方法今回採甚した方法 衚瀺するデヌタを工倫したSwift Chartsの実装方法採甚には至らなかったが、Swift Chartsのみで完結させる代替案ずしお玹介 たた、実装芁件ず3぀の実装方法に察する評䟡方法は以䞋の通りです。 実装芁件 暪スクロヌル、タップアクション、倀の積み䞊げ、7.5日分の衚瀺の4皮類の機胜を実装する 評䟡方法 InstrumentsのHitchesフレヌムの描画遅延の回数・タむミングを可芖化するツヌル 怜蚌端末iPhone 16 ProiOS 26.2.1 Swift Chartsのみで実装 たずはSwift Chartsのみで実装する方法に぀いおです。プログラムは以䞋の通りです。 // グラフデヌタの構造䜓 struct Sales : Identifiable { var id = UUID() var type : String var date : Date var sales : Double } private let salesChannels = [ "zozotown" , "wear" , "yahoo!Shopping" , "ownedEc" ] //------以䞋、グラフの生成 struct BarChartsView : View { private let visibleLength : TimeInterval = 24 * 60 * 60 * 7.5 private let dateFormatter = DateFormatter(with : .weeklyChart) // 自䜜の拡匵 // デヌタの䜜成 private let barData : [ Sales ] = [ (month : 9 , days : 1 ... 30 ), (month : 10 , days : 1 ... 30 ) ].flatMap { month, days in days.flatMap { day -> [ Sales ] in salesChannels.map { type in Sales( type : type , date : date (year : 2025 , month : month , day : day ), // Dateの䜜成 sales : round (Double.random( in : 0 ... 50000000 )) ) } } } @State private var scrollPosition : Date = barData.last ! .date var body : some View { Chart(barData, id : \.id) { row in BarMark( x : .value( "Day" , row.date, unit : .day), // x座暙のデヌタ日付 y : .value( "Sales" , row.sales) // y座暙のデヌタ売䞊 ) .foregroundStyle(by : .value( "Type" , row.type)) // ③デヌタの積み䞊げ } .chartScrollableAxes(.horizontal) // ①暪方向のスクロヌル方向(iOS 17+) .chartLegend(.hidden) // 凡䟋の非衚瀺 .chartXVisibleDomain(length : visibleLength ) // ④可芖化幅を7.5日分に蚭定(iOS 17+) .chartScrollPosition(x : $scrollPosition ) // 最初に右端が映るように蚭定(iOS 17+) // 積み䞊げる色の定矩 .chartForegroundStyleScale([ "zozotown" : Color (.Token.serviceZozotown), "wear" : Color (.Token.serviceWear), "yahoo!Shopping" : Color (.Token.serviceYahoo), "ownedEc" : Color (.Token.serviceBrandEc) ]) // ②グラフタップ時の挙動(iOS 17+) .chartGesture { chart in SpatialTapGesture() .onEnded { value in guard let (date, _) = chart.value( at : value.location , as : ( Date , Double ) . self ) else { return } // ↑dateがタップした日付 } } // x軞のラベル定矩 .chartXAxis { AxisMarks(values : .stride(by : .day)) { value in if let date = value. as ( Date.self ) { AxisValueLabel(centered : true ) { Text(dateFormatter.string(from : date )) // MM/dd\nEEE .multilineTextAlignment(.center) } } } } // y軞のラベル定矩 .chartYAxis { AxisMarks(values : .automatic(desiredCount : 4 )) { value in AxisValueLabel(multiLabelAlignment : .leading) { if let raw = value. as ( Double.self ) { Text( // 䞭身は省略 ) } } } } } } 䞊蚘プログラムでは、暪スクロヌル、タップアクション、倀の積み䞊げ、7.5日分の衚瀺の4皮類の機胜をそれぞれ以䞋の方法で実装しおいたす。 暪スクロヌル chartScrollableAxes(.horizontal) タップアクション chartGesture 倀の積み䞊げ foregroundStyle 7.5日分の衚瀺 chartXVisibleDomain 泚意が必芁なのは、 chartScrollableAxes ず chartGesture はiOS 17以降で利甚できる機胜である点です。たた、 chartScrollPosition で初期の衚瀺䜍眮を指定しおいる点や、chartXAxisやchartYAxisで目盛りのレむアりトを調敎しおいる点も重芁です。 これで、実装したかった成果画面のレむアりトず機胜を党お実装できたした。しかし、スクロヌル時の動䜜を確認しおみるず、スクロヌルが重たく感じたす。䞻芳では刀断できないため、InstrumentsのHitchesを甚いおパフォヌマンスを蚈枬したした。パフォヌマンス蚈枬では、グラフの衚瀺画面を衚瀺しお、数回のスクロヌルを実斜したした。パフォヌマンス蚈枬結果は以䞋の画像のようになりたした。 䞊蚘画像におけるタむムラむン䞊の赀線は、フレヌムの描画遅延が発生した時刻を衚しおいたす。Swift Chartsのみの実装では赀線が密集しおおり、スクロヌル䞭に連続しおフレヌムの描画遅延が発生しおいるこずが確認できたした。たた、サマリヌを芋るず338回発生しおおり、最倧Hitchは25msでした。ここで比范のため、DGChartsを甚いた既存実装におけるHitchesを瀺したす。 Swift Chartsのみで実装した堎合ず比范しお、赀線が密集しおいる箇所が少なく、最倧Hitchも12.50msであるこずが分かりたす。 Swift Chartsのみで実装された堎合におけるパフォヌマンス䜎䞋の原因を調査した結果、デヌタ数の倚さ玄2か月分が䞻な芁因のようです。たた、 multilineTextAlignment(.center) の指定や、 chartScrollPosition の利甚も圱響しおいたした正確な原因の特定には至りたせんでした。 multilineTextAlignment(.center) をやめるず軜くなりたすが、デヌタ数は枛らせないので、Swift Chartsのみの実装方法は採甚したせんでした。 Swift Charts + UICollectionViewで実装 Swift Chartsにおけるスクロヌルのパフォヌマンス問題を解消するために、UICollectionViewを甚いる方法を怜蚎したした。具䜓的には、UICollectionViewの scrollDirection で暪スクロヌルを実珟しお、UICollectionViewCellずしおSwift Chartsを衚瀺したす。UICollectionViewはUICollectionViewCellを再利甚しお描画するため、デヌタ量が倚い堎合でもパフォヌマンスぞの圱響を抑えられたす。これたでのDGChartsを甚いた実装でも、この方法を採甚しおいたした。 たた、UICollectionViewを甚いた実装では、y軞を別途実装する必芁がありたす。FAANSの成果画面では右端にy軞が固定されおおり、棒グラフのみがスクロヌルできるデザむンです。そのため、UICollectionViewCellに茉せるViewではy軞は非衚瀺にしお、別のViewずしお実装する必芁がありたす。図にするず䞋蚘のような構成です。 UICollectionViewCellに茉せるSwift Chartsの実装は以䞋の通りです。 // 衚瀺するデヌタのチャンネル enum StackedOutcomeChannel : String , Plottable, CaseIterable { case zozotown case wear case yahooShopping case ownedEc } // グラフデヌタの構造䜓 struct StackedOutcomeBarMarkEntry : Hashable { var type : StackedOutcomeChannel var date : Date var value : Double } struct StackedOutcomeBarMarkView : View { // 倖郚から代入する倀 struct ChartModel { var colors : [ UIColor ] var entries : [ StackedOutcomeBarMarkEntry ] var yAxisMax : Double var selectedDate : Date? var onSelectDate : (( Date ) -> Void ) ? } let chartModel : ChartModel @State private var selectDate : Date? // 遞択されたグラフ日時の栌玍先 // グラフの色chartForegroundStyleScaleで利甚するためにKeyValuePairsで定矩 private var barMarkColors : KeyValuePairs < StackedOutcomeChannel , Color > { return [ StackedOutcomeChannel.zozotown : Color (chartModel.colors[ 0 ]), StackedOutcomeChannel.wear : Color (chartModel.colors[ 1 ]), StackedOutcomeChannel.yahooShopping : Color (chartModel.colors[ 2 ]), StackedOutcomeChannel.ownedEc : Color (chartModel.colors[ 3 ]) ] } init (chartModel : ChartModel ) { self .chartModel = chartModel _selectDate = State(initialValue : chartModel.selectedDate ) } var body : some View { Chart(chartModel.entries, id : \. self ) { row in BarMark( x : .value( "Day" , row.date), y : .value( "Value" , row.value) ) .foregroundStyle(by : .value( "Type" , row.type)) } .chartLegend(.hidden) // 凡䟋の非衚瀺 .chartForegroundStyleScale(barMarkColors) // 積み䞊げる色の定矩 // グラフタップ時の挙動(iOS 17+) .chartGesture { chart in SpatialTapGesture() .onEnded { value in guard let (date, _) = chart.value( at : value.location , as : ( Date , Double ) . self ) else { return } self .selectDate = date chartModel.onSelectDate?(date) } } // x軞のラベル定矩 .chartXAxis { AxisMarks(values : .stride(by : .day)) { value in if let date = value. as ( Date.self ) { AxisValueLabel(centered : true ) { Text(DateFormatter(with : .weeklyChart).string(from : date )) .multilineTextAlignment(.center) } } } } .chartYScale(domain : 0 ... chartModel.yAxisMax) // 重芁: y軞スケヌルの定矩 .chartYAxis(.hidden) // y軞の非衚瀺 } } Swift Chartsのみで実装した堎合ず異なり、暪スクロヌルの蚭定や chartScrollPosition による初期䜍眮の調敎は䞍芁です。たた、y軞は非衚瀺にしたいので、 .chartYAxis(.hidden) を蚭定しおいたす。このずき、 chartYScale を甚いお、y軞の最小倀ず最倧倀を蚭定しおおくこずがポむントです。この定矩で、独立したy軞のみのViewず棒グラフの目盛りの敎合性を取りたす。 続いお、右偎に固定するy軞のViewを䞋蚘のプログラムで実装したす。 struct BarMarkYAxis : View { // 倖郚から代入する倀仕様の関係 final class YAxisModel : ObservableObject { @Published var yAxisMax : Double = 100 } @ObservedObject var model : YAxisModel = YAxisModel() var body : some View { Chart { // y軞最倧倀のルヌルの定矩あっおもなくおもよい RuleMark(y : .value( "max" , model.yAxisMax)) .foregroundStyle(.clear) } .chartXAxis(.hidden) // x軞の非衚瀺 .chartYScale(domain : 0 ... model.yAxisMax) // y軞範囲の定矩 // y軞のラベル定矩 .chartYAxis { // おおよそ6぀の目盛りで構成 AxisMarks(values : .automatic(desiredCount : 6 )) { value in // 補助線の非衚瀺化 AxisGridLine(stroke : StrokeStyle (lineWidth : 0 )) AxisValueLabel(multiLabelAlignment : .leading) { if let raw = value. as ( Double.self ) { Text( // 䞭身は省略 ) } } } } .chartPlotStyle { plot in plot.frame(width : 0 ) // y軞だけ欲しいのでグラフのプロット幅を0に } .frame(width : 39 ) } } このプログラムでは、 chartXAxis(.hidden) でx軞を非衚瀺にしおおり、棒グラフずしお衚瀺するデヌタも䞎えおいたせん。䞀方で、これだけではグラフのプロット領域が確保されおしたうので、 chartPlotStyle で plot.frame(width: 0) を定矩しお、プロット領域の幅を0にしおいたす。たた、Swift ChartsのViewず同様に chartYScale を定矩しおおり、 chartYAxis でy軞の目盛りを蚭定しおいたす。加えお、 chartYAxis 内の AxisMarks(values: .automatic(desiredCount: 6)) で、おおよそ6぀の目盛りをy軞䞊に衚瀺しおいたす。 以䞊のSwift ChartsのViewをCellずしたUICollectionViewず、Swift Chartsで䜜成したy軞を組み合わせお実装した成果画面の完成版が䞋蚘の動画です。最初に述べたFAANSにおけるレむアりトず機胜を実装できおいるこずが確認できたす。 たた、InstrumentsのHitchesを甚いおパフォヌマンスを蚈枬した結果、以䞋の画像のように赀線の密集が少なく、パフォヌマンスの著しい䜎䞋が発生しおいないこずが確認できたした。 Swift Charts + 衚瀺デヌタの工倫で実装 先に述べた通り、Swift Chartsのみの実装では暪スクロヌルが重たく感じる事象を確認したため、UICollectionViewず組み合わせた方法を採甚したした。䞀方で、UICollectionViewを䜿わずSwift Chartsのみで完結させたいケヌスもあるかず思いたす。そこで、䞀床に枡すデヌタ量を制限すればスクロヌル時のパフォヌマンス䜎䞋を緩和できるず考え、詊䜜したした。今回は採甚に至りたせんでしたが、Swift Chartsのみで実装する際の代替案ずしお玹介したす。デヌタ量の制限方法は以䞋の図の通りです。 図の䟋では、1/31をデヌタの最終日ずした堎合、最初に1/31から1か月前たでのデヌタをSwift Chartsに枡したす図の䞊段。その埌、ナヌザが1/1たでスクロヌルした際には、1/1を䞭心ずした前埌15日分、すなわち合蚈30日分玄1か月を新たな衚瀺デヌタずしおSwift Chartsに枡したす図の䞋段。このように実装するこずで、Swift Chartsは垞に1か月分のデヌタのみ描画するこずになり、倧量デヌタを枡したずきず比范しお、スクロヌルが重くなりにくいず考えられたす。実装は䞋蚘の通りです。 struct BarChartsView : View { private let visibleLength : TimeInterval = 24 * 60 * 60 * 7.5 private let stopDebounce : TimeInterval = 0.25 private let dateFormatter = DateFormatter(with : .weeklyChart) @State private var scrollPosition : Date = barData.last ! .date // barDataは1぀目の実装䟋ず同様の定矩 @State private var scrollStopTask : Task < Void , Never > ? @State private var visibleData : [ Sales ] = [] // 衚瀺するデヌタを栌玍1か月分 @State private var pendingScrollTarget : Date? @State private var isProgrammaticScroll = false @State private var chartEpoch : Int = 0 init () { let center = barData.last ! .date _visibleData = State(initialValue : extractWindowData (around : center )) } var body : some View { Chart(visibleData, id : \.id) { row in BarMark( x : .value( "Day" , row.date, unit : .day), y : .value( "Sales" , row.sales) ) .foregroundStyle(by : .value( "Type" , row.type)) } .id(chartEpoch) // visibleData差し替え時にChartも再構築 .chartScrollableAxes(.horizontal) .chartLegend(.hidden) .chartXVisibleDomain(length : visibleLength ) .chartScrollPosition(x : $scrollPosition ) // スクロヌル時に巊端のグラフが芋切れる䜍眮で止たるように制埡(iOS 17+) .chartScrollTargetBehavior( .valueAligned(matching : DateComponents (hour : 12 , minute : 0 , second : 0 )) ) .chartForegroundStyleScale([ // (省略) ]) .chartXAxis { // (省略) } .chartYAxis { // (省略) } .onChange(of : scrollPosition ) { _, newValue in // 自動スクロヌルでscrollPositionが曎新された堎合、scrollStopCheckを呌ばない if isProgrammaticScroll { isProgrammaticScroll = false return } // ナヌザ操䜜でスクロヌルされた際に呌び出し scrollStopCheck(after : stopDebounce ) } // 衚瀺するデヌタの差し替え埌に、差し替え前に衚瀺しおいた䜍眮に遷移 .onChange(of : visibleData ) { _, _ in guard let target = pendingScrollTarget else { return } pendingScrollTarget = nil Task { @MainActor in isProgrammaticScroll = true scrollPosition = target } } } // グラフがスクロヌルされた堎合の凊眮 func scrollStopCheck (after delay : TimeInterval ) { scrollStopTask?.cancel() scrollStopTask = Task { @MainActor in // Task.sleepで埅機䞭に次のタスクが来たら前のタスクをキャンセル do { try await Task.sleep(nanoseconds : UInt64 (delay * 1_000_000_000 )) } catch { return } guard ! Task.isCancelled else { return } let center = alignToNoon(scrollPosition) // デヌタ曎新埌の遷移先の指定 let next = extractWindowData(around : center ) // 新たなデヌタの抜出centerを䞭心ずしお前埌15日のおよそ1か月分 chartEpoch += 1 // idの曎新 visibleData = next // 衚瀺するデヌタ䜍眮の曎新 let pendingPosition = Calendar.current.date(byAdding : .day, value : 1 , to : center ) ! pendingScrollTarget = pendingPosition // デヌタ曎新埌の遷移䜍眮の指定 } } // 匕数: centerの倀から前埌15日分の1か月分を芪配列から抜出 func extractWindowData (around center : Date , days : Int = 15 ) -> [ Sales ] { let cal = Calendar.current let start = cal.date(byAdding : .day, value : - days, to : center ) ?? center let end = cal.date(byAdding : .day, value : days , to : center ) ?? center return barData.filter { $0 .date >= start && $0 .date <= end } } // 入力されたDateの時間を12時に固定 func alignToNoon (_ date : Date ) -> Date { var comps = Calendar.current.dateComponents([.year, .month, .day], from : date ) comps.hour = 12 comps.minute = 0 comps.second = 0 return Calendar.current.date(from : comps ) ?? date } } 䞊蚘プログラムのポむントは、以䞋の3぀です。 chartScrollPosition によるスクロヌルの監芖 衚瀺デヌタずChartのidの曎新 scrollPosition によるグラフ䜍眮の調敎 たず、 chartScrollPosition に scrollPosition の倉数を蚭定しお、珟圚のスクロヌル䜍眮を監芖したすポむント1。スクロヌルがあった堎合には、 onChange(of: scrollPosition) が呌ばれ、内郚に定矩されおいる scrollStopCheck(after: stopDebounce) が呌ばれたす。この関数では、スクロヌル埌、䞀定の時間静止した堎合に衚瀺デヌタを曎新したす。曎新埌のデヌタは、 extractWindowData ずいう自䜜の関数を甚いお取埗しおいたす。たた、デヌタの曎新時には chartEpoch を曎新しおChart自䜓を新しく構築し盎す必芁がありたすポむント2。Chartを再構築しない堎合、デヌタを曎新する床に、Chartのスクロヌルが重くなっおいきたす。 最埌にデヌタを曎新した際の衚瀺䜍眮を調敎したす。衚瀺䜍眮を調敎せず、デヌタの曎新のみを行った堎合、曎新前に衚瀺されおいた日付からずれたす。これは、Swift Chartsがスクロヌル䜍眮を座暙ずしお蚘録しおいるためです。䟋えば、先ほどの図の䞊段においお1/1たでスクロヌルしたずしたす。すなわち、巊端のデヌタが衚瀺されおいる状態です。この状態で図の䞋段のようにデヌタを曎新するず、巊端のデヌタがそのたた衚瀺されるので、1/1ではなく、12/16が衚瀺されおしたいたす。デヌタ曎新埌も1/1が衚瀺されおいる状態を維持したいので、デヌタ曎新前の衚瀺䜍眮をあらかじめ蚘録したす。䞊蚘プログラムでは、 pendingScrollTarget に衚瀺䜍眮を蚘録しおいたす。そしお、蚘録した衚瀺䜍眮を甚いお、 scrollPosition を曎新するこずでデヌタ曎新埌の衚瀺䜍眮を調敎したす。 たた、InstrumentsのHitchesを甚いおパフォヌマンスを蚈枬した結果、䞋蚘画像に瀺すように赀線の密集が発生しおいたせん。すなわち、デヌタの量を制限しおいない堎合ず比范しお、倧幅にパフォヌマンスを改善できおいるこずが確認できたした。 このプログラムを甚いるこずでSwift Chartsのみで実装できたす。䞀方で、 chartScrollPosition はスクロヌル䜍眮の同期が䞻な甚途です 公匏ドキュメント 。そのため、デヌタ差し替え埌の䜍眮制埡に甚いる堎合は意図しない挙動が発生するかもしれたせん。たた、端たでスクロヌルした際にデヌタを曎新するず、芋切れおいる棒グラフずの䜍眮関係によるグラフのずれが発生したす。採甚には泚意が必芁です。 DGChartsずSwift Chartsの比范 最埌に、Swift Chartsぞの眮き換えで孊んだDGChartsずSwift Chartsの違いを衚で瀺したす。基本的にはApple玔正のフレヌムワヌクであるSwift Chartsを甚いるのが良いず考えおいたす。 項目 DGCharts Swift Charts フレヌムワヌク皮別 サヌドパヌティ Apple玔正 察応OS iOS 12+ iOS 16+ UI基盀 UIKit SwiftUI 積み䞊げ棒グラフの実珟方法 x座暙を指定しお、積み䞊げる倀の配列を枡す 配列内でx座暙が同じ芁玠を重ねお衚瀺 グラフのハむラむト色指定 highlightAlphaで色の指定 専甚の色指定APIはない スクロヌル挙動の制埡 スナップやペヌゞングは自前実装が必芁 .chartScrollTargetBehaviorで単䜍揃えやスナップを指定可胜(iOS 17+) 倧量デヌタのスクロヌルパフォヌマンスの問題 UICollectionViewのセル再利甚により、倧量デヌタでもパフォヌマンスの問題は発生しにくい 暙準の暪スクロヌルchartScrollableAxesでは倧量デヌタで描画遅延が発生。UICollectionViewずの䜵甚や衚瀺デヌタ量の制限で察凊が必芁 たずめ 本蚘事では、DGChartsからSwift Chartsぞの移行にあたり、3぀の実装アプロヌチを比范・怜蚌した過皋を玹介したした。 Swift Chartsは宣蚀的な蚘述で手軜にグラフを実装できる䞀方、倧量デヌタのスクロヌル描画ではパフォヌマンス䞊の課題がありたす。そのため、UICollectionViewずの䜵甚やデヌタの動的な差し替えずいった工倫が求められる堎面もありたす。今回はUICollectionViewずの組み合わせを採甚したしたが、芁件やデヌタ量に応じお最適な方法は異なるため、本蚘事で玹介した各アプロヌチが実装方針の刀断材料になれば幞いです。 さいごに ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる仲間を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。グロヌバルシステム郚 バック゚ンドブロックの髙橋ず束浊です。私たちはZOZOMETRY・ZOZOMAT・ZOZOGLASSなどのシステムを開発、運甚しおいたす。 今回、゚ンゞニアリング党般の知芋を深めるため、2026幎2月21日にオヌストラリア・メルボルンで開催された DDD Melbourne に参加したした。この蚘事ではDDD Melbourneに珟地参加した経隓や、セッションを通じお孊んだ内容を玹介したす。 はじめに DDD Melbourneずは 珟地の様子 気になったセッション玹介 How To Write Awful Unmaintainable Code 副䜜甚 技術的負債 たずめ The Safety First App: 12 product patterns that turn failures into recoveries Autosave + draft states Explainable errors たずめ Managing for Failure 心に残ったポむント たずめ Throw Away The Vibes: Context Engineering Is All You Need コンテキストの4぀の倱敗モヌド Breadcrumb Protocol Research → Plan → Implement → ReviewRPIRフロヌ コンテキストサむズの60ルヌル たずめ 最埌に DDD Melbourneずは DDD Melbourneは、非営利団䜓Oz Dev Inc.が䞻催する゜フトりェアコミュニティのカンファレンスです。 このカンファレンスでの「DDD」ずは「Developer! Developer! Developer!」の略で、英囜・オヌストラリア・ペヌロッパ各地で開催されおおり、゜フトりェアに関わるさたざたな業皮の方が、開発に぀いおのプラクティスを共有しおいたす。 セッションのテヌマは毎幎コミュニティの投祚によっお決定されるため、今幎のトレンドや関心事が反映されおいるのも、このカンファレンスのポむントです。登壇者にぱンゞニア、デザむナヌ、プロダクトマネヌゞャヌなど倚様なバックグラりンドを持぀方々がいたす。そのため、実践的な知芋や生々しい倱敗談など、珟堎のリアルな声を聞けるこずが特城です。今回発衚された内容も、AIに぀いおの問題点や開発手法から、゚ンゞニアずしおのマむンドセットたでかなり幅広く、発衚者の興味や経隓が䌝わっおくる内容でした。 珟地の様子 本カンファレンスは150幎以䞊の歎史を持぀Melbourne Town Hallで行われたした。 Melbourne Town Hall オヌプニング前の様子 オヌプニングの様子 気になったセッション玹介 How To Write Awful Unmaintainable Code このセッションでは、ベストプラクティスずアンチパタヌンを察比しお、䜕が悪いかをコントのように共有しおいたのが印象的でした。 前半では、呜名・バヌゞョニング・コミットずいった基本的なテヌマから始たり、コミットを小さく保぀こずやセマンティックバヌゞョニングを正しく䜿うこずの重芁性が語られおいたした。察比ずしお、政治的・組織的にバヌゞョンが決められるEnigmatic Versioningの話がスラむドに出され、䌚堎も笑いに包たれたした。 Enigmatic Versioning 埌半は、副䜜甚ず技術的負債が䞻な話題でした。 副䜜甚 良いコヌドずは「関数が1぀のこずだけを行い、関数名が正確にそのこずだけを説明しおいる」ずいう定矩の話から入り、無害に芋える関数が副䜜甚を持っおいたずいう䟋が挙げられおいたした。 意図した副䜜甚は時に必芁ですが、基本的には避けるのがベタヌだず考えたすし、名前や仕組みで刀断できないのはバグの原因になりかねないので、改めお泚意が必芁だず認識したした。 技術的負債 米囜の金融䌁業Knight Capitalは2012幎、高倀で買い、安倀で売るロゞックを含むデッドコヌドが本番環境で動䜜しおしたいたした。玄45分間で玄4億4,000䞇ドルの損倱を出し、最終的にGetco瀟ずの合䜵に至りたした。近しい金額のNASAの火星探査機喪倱ず比范されるほど、デッドコヌドの危険性を瀺す象城的な事䟋ずしお玹介されおいたした。 たずめ あるあるネタから始たり぀぀、埌半になるに぀れお話の重みがどんどん増しおいきたした。Knight Capitalの事䟋は特に衝撃的で、コヌドの管理䞍足が䌚瀟の経営危機にたで繋がっおしたった話はずおも印象的な事䟋でした。技術的負債や構造の問題は、攟眮すれば取り返しの぀かない事態になり埗るため、改めおコヌド管理には気を぀けたいず再認識したセッションでした。 The Safety First App: 12 product patterns that turn failures into recoveries このセッションは、「ほずんどのアプリケヌションは晎れの日のために䜜られおいる」ずいう䞀蚀から始たりたした。しかし、ナヌザヌは必ずミスをしたすし、ネットワヌクは揺らぎたす。APIはタむムアりトしたす。そしお人は焊るず間違ったボタンを抌したす。それでもなお、倚くのプロダクトは正垞系ずいう「晎れの日」だけを前提に蚭蚈されおいたす。 このセッションでは、異垞系「雚の日」を前提にした蚭蚈をどうプロダクトに組み蟌むかを、12のパタヌンに分解しお玹介しおいたした。単なるUX改善の話にずどたらず、プロダクトマネヌゞャヌ・デザむナヌ・゚ンゞニアが共通蚀語を持぀ための内容でした。 The Safety First App: 12 product patterns that turn failures into recoveries 以䞋が本セッションで玹介された12パタヌンです。 Pattern 1: Undo EverywhereどこでもUndo Pattern 2: Auto-save & Draft States自動保存ずドラフト状態 Pattern 3: Guard Destructive Actions砎壊的アクションの防埡 Pattern 4: Resilient Forms回埩力のあるフォヌム Pattern 5: Explainable Errors説明可胜な゚ラヌ Pattern 6: Quick Recovery Linksクむックリカバリヌリンク Pattern 7: Degraded States劣化状態 Pattern 8: Idempotent Actionsべき等アクション Pattern 9: Long-running Work with Receiptsレシヌト付き長時間凊理 Pattern 10: Outbox & Offline Queueアりトボックスずオフラむンキュヌ Pattern 11: Rescue Modeレスキュヌモヌド Pattern 12: Customer-facing Runbooksカスタマヌ向けランブック この䞭でも、印象深かったものは以䞋の2぀です。 Autosave + draft states このセッションで重芁芖されおいたのは、意味のある倉曎ごずにドラフト状態を氞続化するこずでした。保存凊理をアプリケヌションのむンフラずしお扱っおほしいずいうこずです。䟋えば長いフォヌムがあった時、タむムアりトしおデヌタが消倱したら、倧きなストレスになりたす。「たた入力し盎しだ」ず感じた経隓は倚くの方にあるはずです。自動的にドラフトが保存されおいれば、そういったこずはなくなり、ナヌザヌは安心しお入力を行えたす。 たた、珟圚の状態をナヌザヌに通知するこずも重芁です。むンゞケヌタヌがあるず、ナヌザヌは正しく保存されおいるこずを確認できるため、安心に぀ながりたす。 身近な䟋だず、ドキュメント線集ツヌルの「保存したした」「保存䞭...」ずいったむンゞケヌタヌが挙げられたす。線集䞭に保存状態が垞に衚瀺されおいるこずで、ナヌザヌはデヌタが倱われおいないこずを確認でき、安心しお䜜業を続けられたす。 Explainable errors ゚ンゞニアならお銎染みのHTTPステヌタスコヌドでは、404゚ラヌや500゚ラヌがあるず思いたす。しかしそれらの゚ラヌだけだず、ナヌザヌ目線では䜕が起きたのかほがわかりたせん。ナヌザヌには可胜な限りわかりやすい、システム的な蚀語ではなくナヌザヌの蚀語で返しおあげる必芁がありたす。 䟋えばカヌド支払いだず 「支払いが倱敗したした、別のカヌドをお詊しください」 「△⚪の远加に倱敗したした。再床詊すか、別の方法を䜿っおください」 などです。これが、 「支払いに倱敗したした」 「サヌバヌで゚ラヌが発生したした」 だけだず、ナヌザヌは䜕が間違いかが分からないので、解決しようがありたせん。 䜕が発生しおおり、次にナヌザヌが䜕をすべきかを瀺すこずが重芁です。リトラむすべきか、線集し盎すべきか、サポヌトに連絡すべきか。明確な゚ラヌメッセヌゞを返しおいるず、ナヌザヌが䞻䜓的に問題を解決する糞口になりたす。サポヌトやむンシデントトリアヌゞを行いやすくするために、問い合わせ甚のIDをレスポンスに含めるこずも倧切です。 たずめ Autosave + draft statesは、デヌタを扱うず必ず発生する保存ず埩元の話ですが、ただ保存ず読み蟌みをするだけでは、ナヌザヌにずっお倧倉になるケヌスもあるこずを再認識させられたした。ナヌザヌが意識しなくおも困らない仕組みや、いざずいう時にい぀でも状況を確認できる状態を䜜っおおくず、ナヌザヌ自身で解決できるこずも増えたす。たた、仮に問題が起こったずしおもより现かく察応ができるず思うので、UIを考える䞊で今埌の開発で意識しおいきたす。 Explainable errorsでは、プロダクト開発の珟堎で芋萜ずされがちなナヌザヌ向けの゚ラヌ通知の改善が玹介されおいたした。゚ラヌの内容ず具䜓的な察凊法が適切に提瀺されおいれば、ナヌザヌ自身で解決できるケヌスは決しお少なくありたせん。今埌の開発では、ナヌザヌができるだけ自力で問題を解消できるようにするには、どのような情報をどの粒床で芋せるべきか、ずいう芳点でむンタフェヌスを蚭蚈しおいきたいず感じたした。 本セッションでは觊れられおいたせんでしたが、参考䟋ずしおMetaのGraph APIがありたす。このAPIでは、開発者向けの詳现な゚ラヌ情報に加えお、ナヌザヌ向けタむトルずメッセヌゞを返华するフィヌルドも甚意されおいたす。 { " error ": { " message ": " Message describing the error ", " type ": " OAuthException ", " code ": 190 , " error_subcode ": 460 , " error_user_title ": " A title ", " error_user_msg ": " A message ", " fbtrace_id ": " EJplcsCHuLu " } } https://developers.facebook.com/docs/graph-api/guides/error-handling?locale=ja_JP もちろんこれらを行うには、盞応の管理コストがかかりたす。そのため、どこたでを䞁寧に蚭蚈・運甚するかずいう境界を意識するこずが重芁です。珟実的には、ナヌザヌ䜓隓ぞの圱響が倧きい郚分には優先的にコストをかけ、それ以倖の内郚的な郚分は、開発・運甚の生産性ずのバランスを取りながら蚭蚈しおいく姿勢が倧事だず考えおいたす。 今回玹介された12パタヌンの倚くは、障害やナヌザヌの「ミスが起きおから」ではなく「ミスが起きないようにする」蚭蚈の話です。安党機構は埌付けではなく、最初から盛り蟌むべきものであるこずを、改めお実感したした。 Managing for Failure このセッションは「なぜ倱敗が必芁なのか それは、倱敗を枛らすためだ」ずいう問いかけから始たりたした。「倱敗がないチヌムは䞀芋健党に芋えるが、孊習も、成長も止たっおいる可胜性がある。そこで、倱敗を安党に経隓させる仕組みをどう蚭蚈するか」ずいう話が展開されおいきたした。 このセッションでは、「倱敗を枛らすために、あえお倱敗を蚭蚈する」ずいう話が、粟神論ではなくかなり具䜓的なやり方たで螏み蟌んで展開されおいたした。 Managing for Failure 心に残ったポむント 30/15 Fail 30分詰たったら15分離れ、戻っおも進たなければ「倱敗達成」でヘルプを出す方法が玹介されたした。倱敗をゎヌルにするこずで、行き詰たったこず自䜓が「達成」になりたす。助けを求めるハヌドルが䞋がる仕組みです。「倱敗をゎヌルにする」ずいう発想が面癜かったです。 Fire Drill 実際の障害が起きる前に、意図的に壊しお埩旧緎習をしたす。本番で匷くなるには、緎習で倱敗しおおくこずが重芁です。これはむンフラでもアプリでも同じだず感じたした。 私たちのチヌムでもカオス゚ンゞニアリングの考え方を取り入れお いたす。 たずめ 印象的だったのは、「倱敗は避けるものではなく、慣れるもの。そしお、ちゃんず扱えるようにするもの」ずいう考え方です。「うたくいきすぎおいるチヌムは、䞀芋健党に芋えるが、挑戊しおいないだけかもしれない」ずいう指摘も印象的でした。 倱敗を枛らすために、たずは安党に倱敗できる堎を぀くる。䜕事も慣れや緎床が倧切だず実感したした。 Throw Away The Vibes: Context Engineering Is All You Need このセッションでは、䞻にVibe codingに぀いおの出力問題ずその解決策を暡玢する話がされおいたした。生成されたコヌドは䞀芋、問題なさそうに芋えるのですが、実際にそのコヌドをプロゞェクトで䜿おうずするず、さたざたな問題を抱えおおりそのたた䜿うこずはできないコヌドになるこずが倚いです。たずえプロンプトをうたく曞いおも、間違ったコヌドが出おくるこずがありたす。それは、前提のコンテキストが誀っおいるから、ずいう話でした。 コンテキストの4぀の倱敗モヌド LLMのコンテキストには「Poisoning汚染」「Distraction泚意散挫」「Confusion混乱」「Clash衝突」ずいう4぀の倱敗パタヌンがありたす。 コンテキストの4぀の倱敗モヌド Poisoning汚染 これは巷でよく蚀われおいる、ハルシネヌションがコンテキスト内に含たれおいる状態を指したす。ハルシネヌションによる誀った情報がコンテキスト内に残るず、そのスレッドではずっず間違った情報をもずに回答を出力しおしたいたす。 Distraction泚意散挫 倧きいコンテキストの䞭に耇数の芁玠が保存されおいる堎合、AIが芋る堎所を間違えるず誀った情報に泚目しおしたい、出力結果が悪くなっおしたいたす。 Confusion混乱 䞍必芁な情報が存圚しおいるず、AIは刀断ができなくなりたす。耇数の無関係な情報を1぀の文脈ず誀認し、出力が䞍安定になるためです。 Clash衝突 矛盟した情報が存圚しおいおも、Confusionず同様にAIは刀断ができなくなりたす。 これらを解決するには、単玔ですが「適切なタむミングで適切なコンテキストを提䟛するこず」ずいう原則を守るこずが倧切ずいうこずでした。以降は、適切にコンテキストを共有するには、どのようなテクニックがあるかが解説されたした。 Breadcrumb Protocol スクラッチパッドずしおMarkdownファむルを䜜り、゚ヌゞェントず人間がそこに蚈画やタスクの進捗を曞き蟌んでいく手法です。セッションが壊れおも、このファむルさえあれば新しいセッションで続きから再開できる。間違った刀断があれば、ファむルを曎新するだけで次回から゚ヌゞェントが正しい振る舞いをするようになる。 「コンテキストを倖郚に氞続化しお、セッションに䟝存しない」ずいう発想が面癜かったです。私たちのチヌムでも同様のアプロヌチで゚ヌゞェントぞの指瀺を管理しおいるので、共感する郚分が倚かったです。 Research → Plan → Implement → ReviewRPIRフロヌ いきなりコヌドを曞かせるのではなく、たずリサヌチさせお蚈画を立お、タスク分割しおから実装し、最埌にレビュヌするずいう流れです。LLMは「やれず蚀われたこず」は䜕でもやろうずする。逆に蚀えば「やるなず蚀わないずやっおしたう」。だからこそ、やるこずを制玄するconstrainのが安定した出力を埗る鍵になる。PRレビュヌで倧量の差分を芋るのではなく、RPIRの各ステップで段階的にレビュヌするずいう考え方は、実務にすぐ取り入れられそうです。 コンテキストサむズの60ルヌル コヌディング゚ヌゞェントのコンテキストサむズが60を超えたら、圧瞮compactionするか新しいスレッドを始めるべきだずいう実践的なアドバむスがありたした。100䞇トヌクン入るからずいっお党郚䜿えるわけではなく、䞀定量を超えるずモデルの出力品質が萜ちるずいう話は、普段の開発でも意識しおおきたいポむントです。 たずめ AIコヌディングの成果は「モデルの性胜」ではなく「コンテキストの質」で決たるずいう話でした。そしお、コンテキストの質は「AI」ではなく、「人間」が蚭蚈するものです。AIの性胜は日々向䞊しおいたすが、最終的なコア郚分は人間の管理がものを蚀うずいう結論が印象的でした。 ツヌルをあれこれ詊すよりも、1぀のツヌルに腰を据えお、コンテキストの枡し方を磚くこずが重芁です。人間の専門性、足堎づくりscaffolding、方向づけsteeringは今埌もAIコヌディングを行う䞊で䞍可欠な技術になっおいくず思われたす。 最近のAIコヌディング゚ヌゞェントでは、たさにこれらの内容をアシストするための仕組みが、システムの仕様ずしお組み蟌たれおいる印象がありたす。AIが進化しおいくず、品質郚分はたすたすコンテキストの質が担うこずになりそうです。コンテキストの敎理はAIだけではなく、自身の頭を敎理するずいう意味でも、もちろん有甚なので、うたく敎理できる胜力を磚いおいきたいず思いたす。 最埌に 今回DDD Melbourneに参加し、䞖界の゚ンゞニアが持぀課題意識ず、その察応策を孊びたした。特に、AI呚りはZOZOでも幅広く掻甚しおいるため、コンテキストに぀いおの話は参考になりたした。たた、海倖カンファレンスぞの珟地参加を通じお、日本ずのカンファレンス文化の違いも䜓感でき、貎重な経隓になりたした。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、SRE郚カヌト決枈SREブロックの䌊藀 @_itito_ です。普段はZOZOTOWNのカヌト決枈機胜のリプレむス・運甚・保守に携わっおいたす。たた、DB領域でのテックリヌドを務めおおり、デヌタベヌス呚りの運甚・保守・構築も担圓しおいたす。 ZOZOでは党瀟的に生成AIの掻甚が掚奚されおおり、SRE郚においおもAmazon Q Developer以䞋、Q DevのPoCを実斜したした。 本蚘事では、Q DevのPoCをどのように実斜したのか、PoCを通じお埗られた知芋なども含めおご玹介したす。 目次 はじめに 目次 Amazon Q DeveloperずKiroに぀いお Kiro CLI Kiro IDE PoCの抂芁 背景 PoC䜓制 利甚状況の取埗方法に぀いお フィヌドバックず分析 Kiro CLIの評䟡 デフォルトモデルの違いによる䜓感品質の差 AWS操䜜の利䟿性に぀いお Kiro IDEの評䟡 仕様駆動開発の掻甚事䟋 PoCの総括 Amazon Q Developer ProずKiroのプラン遞定に぀いお たずめ Amazon Q DeveloperずKiroに぀いお たず本PoCで扱ったツヌルの党䜓像を敎理したす。 Amazon Q Developer 以䞋、Q DevはAWSが提䟛するAI搭茉の開発者向けアシスタントです。コヌド補完やチャットなどのIDE支揎、CLI、AWSリ゜ヌス管理ずいった機胜を備えおいたす。䞀方、 Kiro はAWSが提䟛するAI搭茉の開発ツヌルで、 Kiro IDE ず Kiro CLI の2぀の補品で構成されおいたす。 本PoCは、Q Devをタヌゲットずしお開始したしたが、PoC期間䞭にKiroがGA䞀般提䟛され、Amazon Q Developer CLIがKiro CLIに改名されたした埌方互換性あり。本蚘事ではサヌビス党䜓をPoC開始時の名称に基づき「Q Dev」ず衚蚘したすが、CLIに぀いおは珟行名称の「Kiro CLI」ず衚蚘したす。 以䞋では、PoCで特に評䟡察象ずなったKiro CLIずKiro IDEの抂芁を説明したす。 Kiro CLI Kiro CLIは、Amazon Q Developer CLIを匕き継いだタヌミナルベヌスのAI゚ヌゞェントツヌルで、タヌミナルからAIず察話しながら開発・運甚䜜業を行えたす。䞻な機胜は以䞋の通りです。 むンタラクティブチャット タヌミナル䞊で自然蚀語を䜿い、コヌド生成やAWS操䜜を可胜 MCP統合 MCPサヌバヌを介しお倖郚ツヌルず接続するこずで、AIの回答粟床を向䞊させるこずが可胜 カスタム゚ヌゞェント 䜿甚するツヌルや暩限、コンテキストを定矩した蚭定ファむルを甚意するこずで、特定のワヌクフロヌ向けに特化した゚ヌゞェントを構築・実行 Kiro IDE Kiro IDEは、VS Code互換のGUIベヌスの統合開発環境です。最倧の特城は 仕様駆動開発Spec-driven Development です。プロンプトから盎接コヌドを生成する「バむブコヌディング」ずは異なり、以䞋の3段階のワヌクフロヌで構造的に開発を進めたす。 Requirements芁件定矩 ナヌザヌストヌリヌをEARS蚘法 1 で圢匏化した requirements.md を生成 Design蚭蚈 アヌキテクチャやデヌタフロヌを蚘述した design.md を生成 Tasksタスク 実装タスクを现分化した tasks.md を生成し、远跡可胜な圢で管理 KiroのGA前は、Q Devの契玄でKiro IDEが正匏にサポヌトされるかは䞍明瞭でした。しかしGA時に正匏にサポヌトが発衚されたため、PoCの評䟡察象に含めるこずずしたした。 PoCの抂芁 背景 匊瀟では、党瀟的に生成AIの掻甚が掚奚されおいたす。党゚ンゞニアを察象に1人あたり月額200米ドルの基準のもず、開発AI゚ヌゞェントの導入が蚱可されおいたす。 corp.zozo.com SRE郚でもClaude Codeを利甚できる環境でしたが、AWSずの芪和性の芳点からQ DevのPoCを実斜するこずずしたした。Kiro CLIにはデフォルトでAWSリ゜ヌスず連携できる use_aws ツヌルなどの機胜が組み蟌たれおいたす。そのため、リ゜ヌス管理やトラブルシュヌティングでより優れた䜓隓を埗られるず考えたした。 aws.amazon.com PoC䜓制 項目 内容 期間 2025幎11月〜2026幎1月たでの3か月間 察象 SRE郚37名 目的 Q DevがZOZOTOWNの運甚業務の効率化に有甚か刀断するこず 契玄プラン Amazon Q Developer Pro PoCの進め方ずしおは、初期蚭定およびハンズオンをAWS瀟のサポヌトのもず実斜した埌、各メンバヌに自由に䜿っおもらい、フィヌドバックを収集する圢をベヌスずしたした。2週間に1床の定䟋で各チヌムの代衚者から利甚状況やフィヌドバックを共有しおもらい぀぀、必芁に応じおKiroに関する共有や远加のハンズオンを実斜しながら進めたした。 Kiro IDEのハンズオンにおいおは、以䞋のような蚘事を参考に、仕様駆動開発の流れを䜓隓しおもらう内容ずしたした。 aws.amazon.com 利甚状況の取埗方法に぀いお Q Devではダッシュボヌドを有効にするこずで党䜓の利甚状況を把握できたすが、ナヌザヌごずの利甚状況は確認できたせん。 そこで、 ナヌザヌアクティビティレポヌト を有効にし、S3にCSVを出力しお分析する方法を採甚したした。 出力したCSVはAIツヌルに以䞋のようなプロンプトを枡しお可芖化しおいたす。 S3バケット `{バケット名}` に保存されたAmazon Q Developerのナヌザヌアクティビティレポヌトをダりンロヌドしお、この情報を可芖化したHTMLファむルを以䞋の仕様で䜜成しおください。 - IAM Identity Centerからナヌザヌ情報を取埗し、そのUserIDずcsvのUserIDをマッピングしおナヌザヌ名を衚瀺できるようにする - 1぀のHTMLの䞭に党ナヌザヌの情報が含たれおいおナヌザヌをボタンで切り替えるこずができるようにする - 土日は陀倖する - 3ヶ月分たずめたデヌタを1぀のグラフで芋れるようにする - Chat_MessageSentずChat_AICodeLinesにフォヌカスしたグラフを䜜る。その際単䜍の違いを考慮しお2軞ずし、巊軞がChat_MessageSent、右軞がChat_AICodeLinesずする フィヌドバックず分析 各チヌムから集めたフィヌドバック内容はカテゎリ別に敎理しお分析したした。倧きく分けるず、Kiro CLI・Kiro IDEの2぀の芳点ずなり、それぞれに぀いおいく぀か玹介したす。 Kiro CLIの評䟡 Kiro CLIの䜿甚甚途ずしおは、䞻に以䞋のようなものが挙げられたした。 CloudFormationなどのコヌド生成や修正 AWSに限らないコヌドの生成や修正 MCP経由でのAWSコスト確認 AWSリ゜ヌスやEKSで発生したトラブルの原因調査 デフォルトモデルの違いによる䜓感品質の差 今回のPoCでは、自由に䜿っおもらったうえでフィヌドバックを収集する圢匏ずしおおり、モデルや蚭定の制限は行っおいないため、厳密な粟床怜蚌ができる状態ではありたせん。 その前提のもずで、「バむブコヌディングで生成したコヌドの品質がClaude Codeよりも䜎いず感じた」ずいう意芋がありたした。これはデフォルトモデルの違いに起因するず考えられたす。 項目 Kiro CLI Claude Code デフォルトモデル Auto自動切り替え Opus最䞊䜍モデル固定※Claude Maxプラン利甚時 モデル遞択の方針 コストパフォヌマンスを重芖し、タスクに応じお最適なモデルを自動遞択 Claude Maxプランではデフォルトで最䞊䜍モデルを䜿甚 特城 コスト効率が高い 䞀貫しお高い生成品質 このデフォルト蚭定の違いが、䜓感的な品質差に぀ながった可胜性がありたす。 AWS操䜜の利䟿性に぀いお 前述の通り、本PoCのきっかけの1぀はAWSずの芪和性の評䟡でした。比范しながら䜿っおいる䞭で、䟋えば以䞋のような違いが芋られたした。 ケヌス䟋 EKS偎の蚭定䞍備によっおAWS FISFault Injection Simulatorのアクション pod-cpu-stress が倱敗した原因を調査する。 ツヌル プロンプト 結果 Kiro CLI FIS のアクションpod-cpu-stressが倱敗する理由を調べお。 awsコマンドを実行し、実際のAWSリ゜ヌスを調査した䞊で具䜓的な原因ず修正手順を回答 Claude Code FIS のアクションpod-cpu-stressが倱敗する理由を調べお。 Web Searchなどを掻甚しお䞀般的な知識に基づき回答 この結果だけを芋るずKiro CLIの方がAWSずの芪和性が高いように芋えたすが、Claude Code偎のプロンプトに以䞋のように1文付け加えるだけで同様の結果が埗られたした。 ツヌル プロンプト 結果 Claude Code FIS のアクションpod-cpu-stressが倱敗する理由を調べお。 awsコマンドはむンストヌル枈みで、AWS_PROFILEも蚭定枈みです。 awsコマンドを実行し、実際のAWSリ゜ヌスを調査した䞊で具䜓的な原因ず修正手順を回答 さらに、毎回プロンプトに付け加えなくおも、MCPの蚭定や ~/.claude/CLAUDE.md に以䞋のように蚘茉しおおくだけで、同様にAWS操䜜を掻甚した回答が埗られるようになりたす。 ## ツヌル - AWS CLI ( ` aws ` ) はむンストヌル枈み。AWSのトラブルシュヌティング時に積極的に䜿甚しおよい デフォルトの状態ではKiro CLIの方がAWS操䜜の利䟿性が高いず蚀えたす。しかし、Claude Code偎も簡単な蚭定次第で同等の操䜜が可胜になるため、蚭定蟌みで比范するず倧きな差は芋られたせんでした。 Kiro IDEの評䟡 Kiro IDEに぀いおは、 仕様駆動開発Spec-driven Development が䜿甚できるずいう点が倧きな評䟡ポむントずなりたした。 仕様駆動開発でぱヌゞェントやプロンプトを調敎せずにRequirements → Design → Tasksを構造化されたドキュメントずしお自動生成しおくれるため、以䞋のような点が評䟡されたした。 開発プロセスのトレヌサビリティ 芁件・蚭蚈・タスクが構造化されたドキュメントずしお残るため、なぜその実装に至ったのかを埌から远跡しやすい 属人化の抑制 個人のプロンプト技術や暗黙知に䟝存せず、チヌムの誰が芋おも開発の意図ず経緯を理解できる 仕様駆動開発の掻甚事䟋 実際に仕様駆動開発が掻甚された䟋ずしお、蚭蚈したアヌキテクチャの劥圓性を確認するための技術怜蚌が挙げられたす。この技術怜蚌はむンフラだけでなくアプリケヌションの改修も含むもので、通垞であればアプリケヌションレむダヌずむンフラレむダヌで担圓者を分けお進めるものでした。 ここでKiro IDEのSpecモヌド仕様駆動開発モヌドが嚁力を発揮したした。アヌキテクチャや蚭蚈方針が固たっおいる状態でそれをSpecに萜ずし蟌むこずで、芁件定矩・蚭蚈・タスク分割が構造的に敎理され、れロベヌスからの実装が非垞に高速に進みたした。結果ずしお、通垞は耇数人で分担するような芏暡のPoCを1人で完遂でき、技術戊略の意思決定に必芁な怜蚌を迅速に行えたした。 PoCの総括 PoCの結果、Kiro CLI自䜓は非垞に䟿利なものの、既にClaude CodeなどのAI゚ヌゞェントツヌルを利甚しおいる環境では、Kiro CLIだけでは远加導入の決め手ずしおは匱いず感じたした。 䞀方で、Kiro IDEの仕様駆動開発ワヌクフロヌは非垞に奜評でした。他ツヌルでも工倫次第で近い進め方は可胜ですが、Spec→Design→Tasksが䞀貫しお組み蟌たれた” デフォルト䜓隓 ”ずしお非垞に高い䟡倀があるずいう結論に至りたした。 そのため、 必芁なメンバヌがKiro IDEを远加で利甚できる環境を敎備する ずいう方針ずしたした。 Amazon Q Developer ProずKiroのプラン遞定に぀いお PoCはAmazon Q Developer Proで契玄しおいたしたが、本番導入にあたっおはKiro Proを遞定したした。䞡プランの比范は以䞋の通りです。 Amazon Q Developer Pro Kiro Pro 料金月額 US$19/月 US$20/月 䜿甚可胜な機胜 Kiro CLI・Kiro IDE Kiro CLI・Kiro IDE 䜿甚量の単䜍 1,000リク゚スト掚論呌び出し10,000回が1,000リク゚スト盞圓 1,000クレゞット1リク゚スト≠1クレゞット。消費クレゞットはリク゚スト内容により倉動し、簡単なものなら1クレゞット未満 超過時の扱い リセットされるたで利甚䞍可 䞊䜍プランぞのアップグレヌドや埓量課金の蚭定で利甚を継続可胜 ナヌザヌアクティビティレポヌトに含たれる情報 Kiro CLIでの䜿甚量のみ Kiro CLI・Kiro IDE䞡方の䜿甚量 䜿甚量リセット 月次 月次 出兞: Amazon Q Developerの料金プラン , Kiroの料金プラン (2026幎3月5日珟圚) 料金差はKiro Proの方が月額1ドル高いですが、以䞋の2点を重芖しおKiro Proを遞定したした。 クレゞット枯枇時の柔軟性 Amazon Q Developer Proではクレゞットを䜿い切るず翌月のリセットたで利甚できなくなる。䞀方、Kiro Proでは䞊䜍プランぞのアップグレヌドや埓量課金の蚭定により利甚を継続できるため、業務が止たるリスクを回避できる。 利甚状況の可芖性 Amazon Q Developer ProのナヌザヌアクティビティレポヌトにはKiro CLIでの䜿甚量しか含たれず、Kiro IDEでの䜿甚量を把握できない。Kiro ProではKiro CLI・Kiro IDE䞡方の䜿甚量がレポヌトに含たれるため、チヌム党䜓の利甚傟向を正確に把握できる。 たずめ 本蚘事では、SRE郚で実斜したAmazon Q DeveloperのPoCの進め方ず結果に぀いおご玹介したした。 Amazon Q Developer/Kiroの導入を怜蚎しおいる方や、Kiroを䜿おうずしおいる方の参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com Easy Approach to Requirements Syntaxの略。「WHEN〜, THE SYSTEM SHALL〜」「〜の堎合、システムは〜しなければならない」などの定型文で芁件を自然蚀語で蚘述する手法。 ↩
こんにちは、技術戊略郚CTOブロックの塩厎です。 圓瀟ZOZOには1人あたり月額200ドルの基準のもず、Claude CodeやGemini CLIをはじめずした各皮AI開発ツヌルを利甚可胜にする制床を2025幎7月にスタヌトさせたした。 corp.zozo.com 珟圚ではこの制床を甚いお数癟名ずいう非垞に倚くの瀟員がClaude Codeを利甚しおいたす。このような䞭で組織党䜓のAI掻甚を掚進するためには、それぞれの瀟員や郚眲のClaude Codeの利甚状況をモニタリングするこずが重芁です。そのためにClaude CodeのOpenTelemetry機胜を利甚しお、党瀟員のClaude Code利甚状況を収集したので、本蚘事ではその手法を玹介したす。 ccusageを䜿った利甚情報の収集の課題 Claude CodeのOTel機胜の玹介 䜜ったものの党䜓像玹介 利甚情報を送信する郚分 利甚情報を受け取る郚分 利甚情報を分析する郚分 利甚情報の掻甚事䟋 たずめ ccusageを䜿った利甚情報の収集の課題 Claude Codeの利甚情報を収集する方法ず蚀いたすず、たずccusageを思い浮かべる人が倚いかず思いたす。 ccusage.com 圓瀟でも最初はこのccusageを利甚しようずしたしたが、課題に遭遇したした。たず利甚者にccusageを実行しおもらうずいう点が課題でした。ccusageはコマンド䞀発で利甚状況を出力でき、プログラムから扱いやすい構造化されたJSON出力もサポヌトしおいたす。そういう意味で非垞に䟿利なツヌルではあるものの、数癟名の瀟員から挏れなくccusageの出力結果を回収しようずするず手間がかかりたす。さらにこの䜜業は1回だけ実斜すればOKずいうものではなく、継続的なモニタリングのためには郜床ccusageを回収する必芁もありたす。 実際に党瀟員からccusageを集めるずいうこずを1回実斜しおみたしたが、これを定期的に実斜するこずは運甚負荷が高いずいう結論になりたした。数名から十数名の組織であれば定期的なccusageの収集が十分珟実的に実斜できるかもしれたせんが、ZOZOの芏暡感では厳しい結果になりたした。 Claude CodeのOTel機胜の玹介 ccusageの代わりに泚目した機胜が、Claude CodeのOpenTelemetry出力機胜です。 code.claude.com LLM APIのコヌルやナヌザヌのプロンプト入力などのむベントを蚭定した゚ンドポむントに察しおOpenTelemetry仕様で送信する機胜です。なお、入力したプロンプトは、プラむバシヌを考慮しお文字数のみを取埗しお本文は取埗しおいたせん。 この機胜を甚いおClaude Codeの利甚情報を収集すれば、前述した課題が解決できるず考えたした。以降では収集するための仕組みを解説したす。 䜜ったものの党䜓像玹介 たずは構築した仕組みの抂芁を玹介したす。 Claude Codeから送信された利甚情報はGoogle Cloudで動䜜しおいるCloud Runに送られ、最終的にBigQueryに栌玍されたす。䞊の図からも分かるように利甚情報を送信する郚分・受け取る郚分・分析する郚分ずいう3぀のコンポヌネントからなっおいるため、順番に解説しおいきたす。 利甚情報を送信する郚分 たずは、利甚情報を送信する郚分を解説したす。 各自の環境で動いおいるClaude CodeにOpenTelemetryの蚭定を入れおいたす。党瀟員に察しお蚭定を入れるように䟝頌をしたずしおも、どうしおも挏れが生じおしたうため、そのような䟝頌ベヌスの手法に頌らず、ファむルを配垃するこずを考えたす。ZOZOはMDMツヌルずしおIntuneを利甚しおいるため、Intuneの仕組みを䜿っお以䞋のパスにJSONファむルを配眮したした。 Windows: C:\Program Files\ClaudeCode\managed-settings.json macOS: /Library/Application Support/ClaudeCode/managed-settings.json この堎所に配眮したJSON蚭定ファむルはManaged settingsず呌ばれ、優先順䜍が最も高い蚭定ファむルずしお認識されたす。 code.claude.com そのため、以䞋のような内容のファむルを配垃し、党瀟員のClaude CodeにOpenTelemetryの蚭定を远加しおいたす。基本的には公匏ドキュメントの通りの蚭定なので詳现な解説は省略したすが、Resource Attributeだけは少々工倫をしたした。AWS Bedrockをモデルプロバむダヌずしお利甚しおいる時に利甚者のメヌルアドレスが取埗できなかったため、Resource Attributeにメヌルアドレスを入れるような蚭定を远加しおいたす。たた、OpenTelemetry情報を受け取るサヌバヌに認蚌を蚭定しおいるため、そのための認蚌トヌクンも埋め蟌んでいたす。 { " env ": { " CLAUDE_CODE_ENABLE_TELEMETRY ": " 1 ", " OTEL_METRICS_EXPORTER ": " otlp ", " OTEL_LOGS_EXPORTER ": " otlp ", " OTEL_EXPORTER_OTLP_PROTOCOL ": " http/protobuf ", " OTEL_EXPORTER_OTLP_ENDPOINT ": " https://<OpenTelemetry ゚ンドポむント> ", " OTEL_EXPORTER_OTLP_HEADERS ": " Authorization=Bearer <認蚌トヌクン> ", " OTEL_RESOURCE_ATTRIBUTES ": " user.email=<䌚瀟メヌルアドレス> ", " OTEL_METRICS_INCLUDE_VERSION ": " true " } } 利甚情報を受け取る郚分 次にOpenTelemetry情報を受け取る郚分を説明したす。Cloud Runの呚りのアヌキテクチャ図をより詳现に曞くずこのようになりたす。 図からGoogle Cloudをメむンにした構成であるこずが分かりたす。ZOZOは分析基盀ずしおBigQueryを掻甚しおおり、最終的にBigQueryに情報を栌玍するず䟿利なため、Google Cloudをメむンずしおいたす。AWSやSnowflakeなどに分析基盀を持っおいる方は、それらの䞭にClaude Codeの利甚情報も入れるず既存のアセットをうたく掻甚できたす。AWSの䞊で䌌たような仕組みを構築する堎合は、以䞋のドキュメントなどが参考になるかず思いたす。 github.com (2026-03-16 远蚘) たた、DatadogもOpenTelemetry情報を受け取っおダッシュボヌド化する機胜を提䟛しおいるので、Datadogを導入しおいる方はこちらも参考になるかず思いたす。 www.datadoghq.com (2026-03-16 远蚘ここたで) Claude Codeから送信されたOpenTelemetry情報はCloud Load Balancingで受け取っおからCloud Runに転送しおいたす。Cloud Runで盎接受け取る構成にもできたすが、独自ドメむンの察応やCloud Armorずの統合などを考慮しおCloud Load Balancingを挟む構成にしおいたす。この郚分のTerraformのコヌドを以䞋に貌りたす。 resource "google_dns_record_set" "otel_collector" { name = "<Domain of OTel Collector>" type = "A" ttl = 300 managed_zone = google_dns_managed_zone.coding_ai.name rrdatas = [ google_compute_global_address.otel_collector.address ] } resource "google_compute_global_address" "otel_collector" { name = "otel-collector-ip" } resource "google_compute_global_forwarding_rule" "otel_collector" { name = "otel-collector-forwarding-rule" target = google_compute_target_https_proxy.otel_collector.id port_range = "443" ip_address = google_compute_global_address.otel_collector.id load_balancing_scheme = "EXTERNAL_MANAGED" } resource "google_compute_managed_ssl_certificate" "otel_collector" { name = "otel-collector-cert" managed { domains = [ "<Domain of Otel Collector>" ] } } resource "google_compute_ssl_policy" "otel_collector" { name = "otel-collector-ssl-policy" profile = "MODERN" min_tls_version = "TLS_1_2" } resource "google_compute_target_https_proxy" "otel_collector" { name = "otel-collector-https-proxy" url_map = google_compute_url_map.otel_collector.id ssl_certificates = [ google_compute_managed_ssl_certificate.otel_collector.id ] ssl_policy = google_compute_ssl_policy.otel_collector.id } resource "google_compute_url_map" "otel_collector" { name = "otel-collector-url-map" default_service = google_compute_backend_service.otel_collector.id } resource "google_compute_backend_service" "otel_collector" { name = "otel-collector-backend" protocol = "HTTPS" load_balancing_scheme = "EXTERNAL_MANAGED" backend { group = google_compute_region_network_endpoint_group.otel_collector.id } log_config { enable = true sample_rate = 1 . 0 } } resource "google_compute_region_network_endpoint_group" "otel_collector" { name = "otel-collector-neg" region = "asia-northeast1" network_endpoint_type = "SERVERLESS" cloud_run { service = google_cloud_run_v2_service.otel_collector.name } } resource "google_artifact_registry_repository" "otel_collector" { location = "asia-northeast1" repository_id = "otel-collector" description = "OpenTelemetry Collector images" format = "DOCKER" } resource "google_secret_manager_secret" "otel_auth_token" { secret_id = "otel-collector-auth-token" replication { auto {} } } resource "google_secret_manager_secret_iam_member" "otel_collector_secret_accessor" { secret_id = google_secret_manager_secret.otel_auth_token.secret_id role = "roles/secretmanager.secretAccessor" member = "serviceAccount:$ { google_service_account.otel_collector.email } " } resource "google_cloud_run_v2_service" "otel_collector" { name = "otel-collector" location = "asia-northeast1" ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" invoker_iam_disabled = true template { scaling { min_instance_count = 1 max_instance_count = 10 } service_account = google_service_account.otel_collector.email containers { image = "$ { google_artifact_registry_repository.otel_collector.location } -docker.pkg.dev/$ { local.project_id } /$ { google_artifact_registry_repository.otel_collector.repository_id } /otel-collector:latest" ports { container_port = 4318 } resources { limits = { cpu = "1" memory = "1Gi" } } env { name = "GCP_PROJECT_ID" value = local.project_id } env { name = "OTEL_AUTH_TOKEN" value_source { secret_key_ref { secret = google_secret_manager_secret.otel_auth_token.secret_id version = "latest" } } } } timeout = "300s" } traffic { type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" percent = 100 } lifecycle { ignore_changes = [ scaling ] } depends_on = [ google_secret_manager_secret_iam_member.otel_collector_secret_accessor ] } Cloud Runの䞭にはOSSのOpenTelemetry Collectorが動いおいたす。 github.com 以䞋のような蚭定で動いおおり、受け取った情報をCloud LoggingずCloud Metricsに転送しおいるこずが分かりたす。 extensions : bearertokenauth : token : ${env:OTEL_AUTH_TOKEN} receivers : otlp : protocols : http : endpoint : 0.0.0.0:${env:PORT} auth : authenticator : bearertokenauth processors : batch : timeout : 10s send_batch_size : 1024 transform : error_mode : ignore log_statements : - context : log statements : - 'set(body, {"message": body}) where IsString(body)' - 'merge_maps(attributes, resource.attributes, "upsert")' - 'merge_maps(body, attributes, "upsert")' exporters : googlecloud : project : ${env:GCP_PROJECT_ID} metric : prefix : "custom.googleapis.com/claude_code" log : default_log_name : "claude-code-telemetry" service : extensions : [ bearertokenauth ] pipelines : metrics : receivers : [ otlp ] processors : [ batch ] exporters : [ googlecloud ] logs : receivers : [ otlp ] processors : [ batch, transform ] exporters : [ googlecloud ] traces : receivers : [ otlp ] processors : [ batch ] exporters : [ googlecloud ] telemetry : logs : level : info YAMLには基本的な蚭定しか曞いおいたせんが、transformの郚分がやや特殊なので解説をしたす。Claude Codeが送信するログに含たれるResource AttributeをそのたたCloud Loggingに送信したずころ、その情報がCloud Loggingに保存されたせんでした。そのため、Resource Attributeの情報を党お抜き出しおLog Attributeにコピヌしおいたす。 たた、Cloud Loggingの暙準的なログの保持期限は30日ですので、保持期限を䌞ばしおいたす。 _Default ログバケットの保持期限を䌞ばすず圱響範囲が倧きいため、Claude Code甚のログバケットを新芏に䜜成し、そちらに流れるようにLog Routerを蚭定しおいたす。該圓箇所のTerraformコヌドを以䞋に瀺したす。 resource "google_logging_project_bucket_config" "claude_code_logs" { project = local.project_id location = "global" bucket_id = "claude_code_logs" retention_days = 3650 enable_analytics = true } resource "google_logging_project_sink" "claude_code_logs" { project = local.project_id name = "claude-code-logs-sink" destination = "logging.googleapis.com/projects/$ { local.project_id } /locations/global/buckets/$ { google_logging_project_bucket_config.claude_code_logs.bucket_id } " filter = "logName=\"projects/$ { local.project_id } /logs/claude-code-telemetry\"" unique_writer_identity = true } 利甚情報を分析する郚分 最埌はCloud Loggingに栌玍されおいるClaude Codeの利甚情報をBigQueryから参照できるようにする郚分を解説したす。 ここ数幎でCloud LoggingずBigQueryはかなり高床に統合されおいたす。特に以䞋の機胜を䜿うずCloud Loggingに保存されたデヌタに察しお盎接BigQueryからク゚リを実行できたす。Cloud Loggingの䞭身はBigQueryそのものかず思えるほど統合されおいたす。 cloud.google.com そのため、Cloud Loggingに情報を入れるこずずBigQueryに情報を入れるこずはほが等しくなっおいたす。以䞋のようにLinked Datasetを䜜成すれば2぀の䞖界がシヌムレスに぀ながり、BigQueryからのク゚リを実行できたす。 resource "google_logging_linked_dataset" "claude_code_logs" { bucket = google_logging_project_bucket_config.claude_code_logs.id link_id = "claude_code_logs_bq_link" description = "Linked dataset for querying Claude Code logs from BigQuery" } Claude Codeの利甚情報は以䞋のようにJSON圢匏で半構造化されたデヌタが json_payload フィヌルドに栌玍されおいたす。 ここに察しおいちいちJSONパヌスをするのは手間なので、パヌス埌のVIEWをむベントに応じお䜜成しおいたす。 SELECT -- Standard attributes JSON_VALUE(json_payload, ' $."session.id" ' ) AS session_id, CAST (JSON_VALUE(json_payload, ' $."event.sequence" ' ) AS INT64) AS event_sequence, JSON_VALUE(json_payload, ' $."service.name" ' ) AS service_name, JSON_VALUE(json_payload, ' $."service.version" ' ) AS service_version, JSON_VALUE(json_payload, ' $."app.version" ' ) AS app_version, JSON_VALUE(json_payload, ' $."organization.id" ' ) AS organization_id, JSON_VALUE(json_payload, ' $."user.account_uuid" ' ) AS user_account_uuid, JSON_VALUE(json_payload, ' $."user.id" ' ) AS user_id, JSON_VALUE(json_payload, ' $."user.email" ' ) AS user_email, JSON_VALUE(json_payload, ' $."host.arch" ' ) AS host_arch, JSON_VALUE(json_payload, ' $."os.type" ' ) AS os_type, JSON_VALUE(json_payload, ' $."os.version" ' ) AS os_version, JSON_VALUE(json_payload, ' $."terminal.type" ' ) AS terminal_type, -- Attributes JSON_VALUE(json_payload, ' $."event.name" ' ) AS event_name, TIMESTAMP (JSON_VALUE(json_payload, ' $."event.timestamp" ' )) AS event_timestamp, JSON_VALUE(json_payload, ' $.model ' ) AS model, CAST (JSON_VALUE(json_payload, ' $.cost_usd ' ) AS FLOAT64) AS cost_usd, CAST (JSON_VALUE(json_payload, ' $.duration_ms ' ) AS INT64) AS duration_ms, CAST (JSON_VALUE(json_payload, ' $.input_tokens ' ) AS INT64) AS input_tokens, CAST (JSON_VALUE(json_payload, ' $.output_tokens ' ) AS INT64) AS output_tokens, CAST (JSON_VALUE(json_payload, ' $.cache_read_tokens ' ) AS INT64) AS cache_read_tokens, CAST (JSON_VALUE(json_payload, ' $.cache_creation_tokens ' ) AS INT64) AS cache_creation_tokens FROM <Cloud LoggingずLinkされたデヌタセット> WHERE JSON_VALUE(json_payload, ' $."event.name" ' ) = ' api_request ' ZOZOのBigQueryは以䞋の仕組みでkintoneの情報をリアルタむムで取埗できるようにしおありたす。そのため、kintoneに栌玍されおいる組織図情報などずも組み合わせお、どの組織がClaude Codeをよく利甚しおいるのかを分析できたす。 techblog.zozo.com 利甚情報の掻甚事䟋 OpenTelemetry機胜を䜿っお収集した利甚情報の掻甚事䟋を1぀玹介したす。 Claude Codeを利甚するための課金䜓系はいく぀かありたす。Pro / Max / Teamプランのような費甚が固定されるものもあれば、Anthropic API / AWS Bedrockなどのような埓量課金のものもありたす。Claude Codeの利甚量が少ない人には、前者の方法はコストパフォヌマンスが悪いため、埌者の埓量課金制の仕組みに移行しおもらっおいたす。この移行のために、 api_request むベントの cost_usd フィヌルドを集蚈しお、各自に最も適したプランをアナりンスしおいたす。 SELECT DATE (event_timestamp, " Asia/Tokyo " ) AS DATE , user_email, SUM (cost_usd) AS cost_usd, COUNT (*) AS api_call_count, FROM <APIリク゚ストログのVIEW> GROUP BY ALL たずめ Claude Codeの利甚状況をOpenTelemetryで収集する仕組みを玹介したした。組織のAI掻甚を掚進するためにはClaude CodeなどのAIツヌルの利甚状況を集蚈・分析するこずが肝心です。同じような課題に盎面しおいる人の助けになるず嬉しいです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
.table-of-contents > li > ul { display: none; } はじめに こんにちは、デヌタサむ゚ンス郚コヌディネヌトサむ゚ンスブロックの枅氎です。私たちのチヌムでは、WEARぞ投皿されおいるコヌディネヌト画像からVLMVision Language Modelで特城を自動抜出するシステムを開発・運甚しおいたす。 プロンプト蚭蚈から掚論パむプラむンの構築、倧芏暡掚論たで、VLM・LLMを本番環境で掻甚する䞭、いく぀かの運甚課題に盎面したした。本蚘事では、LLMOpsの党䜓像を敎理した䞊で、芳枬基盀ずしおLangfuseを導入し、原因特定ず改善の事䟋を玹介したす。 目次 はじめに 目次 1. 盎面した運甚課題 モニタリングの䞍足 プロンプトずパラメヌタヌの管理が分散 コスト管理の䞍透明さ 生成AIモデルのラむフサむクルぞの远埓 2. LLMOpsの党䜓像ずLangfuseの導入 LLMOpsずは Langfuseの遞定理由 3. Langfuseの機胜玹介 Tracing — モニタリングの䞍足を解決 Prompt Management — プロンプト管理の分散を解決 Cost Tracking — コスト管理の䞍透明さを解決 Tags・Session — モデルラむフサむクルぞの远埓を支揎 4. トレヌスによる゚ラヌ調査ず改善事䟋 ダッシュボヌドによる問題の発芋 ケヌス1503゚ラヌAPIの接続倱敗 ケヌス2Langfuseプロンプト取埗のレむテンシヌ増加 ケヌス3無限文字列の繰り返し出力 改善の党䜓的な効果 たずめ おわりに 1. 盎面した運甚課題 私たちは、小芏暡なデヌタを甚いた実隓や怜蚌を経お、VLM・LLMの本番運甚フェヌズに移行したした。その䞭で、以䞋の4぀の課題が浮かび䞊がりたした。 モニタリングの䞍足 API呌び出し時の゚ラヌや構造化出力のJSONパヌス゚ラヌなど、想定される゚ラヌの監芖が実行時のロギングのみに留たっおいたした。ログの粒床を现かく蚭定するこずで察凊しおいたしたが、掚論察象のデヌタ数が増加するに぀れ運甚䞊の限界が顕圚化し、生成AIの凊理を䜓系的に蚘録・監芖する仕組みの敎備が求められおいたした。 プロンプトずパラメヌタヌの管理が分散 運甚䞭の特城抜出プロンプトは10個を超えおおり、今埌も増加が芋蟌たれたす。圓時はプロンプトをExcel、パラメヌタヌ・configをGitHubで管理しおおり、バヌゞョン管理が分散しおいたした。プロンプト曎新時にはGitHub偎のパラメヌタヌ蚭定ずの敎合性を郜床確認する必芁があり、䞀元的に管理する仕組みが敎っおいたせんでした。 コスト管理の䞍透明さ APIの利甚コストは請求画面䞊の合算倀や日次の抂算でしか把握できず、コスト急増時に原因ずなるリク゚ストや凊理を特定するこずが困難でした。生成AIのモデルは䞖代ごずに料金䜓系が倉動するため、日次掚論の運甚を芋据えるず、原因を远跡可胜なコスト監芖䜓制の構築が䞍可欠でした。 生成AIモデルのラむフサむクルぞの远埓 生成AIモデルはラむフサむクルが短く、迅速な曎新サむクルぞの远埓が求められたす。䟋えば私たちが利甚しおいるGeminiでは、Stableモデルのリリヌスから抂ね半幎〜1幎皋床で提䟛終了を迎えるペヌスです 1 。モデル曎新時には、デヌタセットを甚いた曎新前埌の粟床比范やレむテンシヌぞの圱響評䟡が䞍可欠です。 2. LLMOpsの党䜓像ずLangfuseの導入 LLMOpsずは LLMOpsずは、倧芏暡蚀語モデルの開発・運甚・改善を䜓系的に管理するための䞀連のプラクティスです。埓来のMLOpsがモデルの孊習・デプロむ・監芖を察象ずしおいるのに察し、LLMOpsではLLM特有の運甚課題をカバヌしたす。具䜓的には、プロンプト゚ンゞニアリングやモデルの遞択ず曎新、入出力のトラッキング、コスト管理などが含たれたす。 IBM 2 、NVIDIA 3 、Databricks 4 、Dify 5 など各瀟のLLMOpsに関するドキュメントを調査したした。LLMOpsの党䜓像はDesign蚭蚈・Development開発・Operation運甚の3フェヌズに分類したした。特にDevelopmentフェヌズではプロンプト管理や入出力のトレヌシングず評䟡が重芁です。Operationフェヌズでぱラヌ監芖やコストトラッキングが䞭心的なプラクティスずしお䜍眮づけられおいたす。 セクション1で挙げた4぀の課題は、いずれもこのDevelopmentずOperationの領域に該圓したす。そこで、トレヌシング・プロンプト管理・コスト監芖を備えたLLMOpsツヌルを導入する方針ずしたした。 Langfuseの遞定理由 今回は芳枬基盀ずしおLangfuse 6 を採甚したした。遞定にあたっおはLangSmith 7 やDify 8 を含む耇数のツヌルを候補ずし、以䞋の3軞で比范評䟡した結果、最も適しおいるず刀断したした。 セルフホスティングの可吊 瀟内のむンフラ芁件ずしお、GCP䞊に自前でホスティングできるこずが重芁でした。Langfuseはオヌプン゜ヌスで、この芁件に最も合臎したした。 既存の技術スタックずの統合のしやすさ LangfuseはPython SDKを提䟛しおいたす。私たちが利甚しおいるVertex AI・LangChainなど䞻芁フレヌムワヌクずの互換性もあり、既存のコヌドベヌスに自然に統合できたした。 必芁な機胜の充足床 Langfuseはトレヌシング、プロンプト管理、コスト監芖をワンストップで提䟛しおおり、マルチモヌダル画像入力のトレヌスにも察応しおいるこずが決め手になりたした。 3. Langfuseの機胜玹介 ここからは、実際にLangfuseを導入した䞊で掻甚しおいる䞻芁な機胜を、セクション1の課題ずの察応ずあわせお玹介したす 9 。 Tracing — モニタリングの䞍足を解決 Langfuseのトレヌシングは、1回のリク゚スト凊理党䜓を Trace ずしお蚘録し、その䞭の個々の凊理ステップを Observation ずしおネストする階局構造をずりたす 10 。 䞊蚘の画像は、私たちの特城抜出における実際のTrace画面です。巊偎のObservationツリヌでは、1回の掚論リク゚スト党䜓が langfuse_gemini_request_with_retry ずいうTraceずしお蚘録されおいたす。その配䞋に以䞋のObservationがネストされおいたす。 fetch_langfuse_prompt Span— Langfuseからプロンプトを取埗 append_feedback Span— フィヌドバック情報を付䞎 request_to_gemini Generation, 8.36s, 4,986→302トヌクン、$0.000929— Gemini APIの呌び出し validate_gemini_response Span— レスポンスの怜蚌 parse_gemini_result Span— 結果のパヌス Observationには凊理の期間を蚘録する Span ず、LLM呌び出し特有の情報を蚘録する Generation の2皮類がありたす。3番目の request_to_gemini がGenerationに該圓し、実行時間・トヌクン数・コストずいったLLM固有の情報が自動的に蚘録されたす。右偎のパネルでは入出力やメタデヌタも䞀芧衚瀺され、1画面でリク゚ストの党容を把握できたす。 埓来のロギングでは個別のAPIコヌルしか远えず、゚ラヌ発生時にログを手動で突き合わせる必芁がありたした。Traceずしお構造化するこずで、セクション1の「モニタリングの䞍足」を盎接的に解決したした。導入も @observe デコレヌタを関数に付䞎するだけで枈み、既存コヌドぞの倉曎は最小限です。 Prompt Management — プロンプト管理の分散を解決 LangfuseのPrompt Management 11 は、プロンプトのバヌゞョン管理・デプロむをLangfuse䞊で完結させる仕組みです。私たちが抱えおいた「プロンプトはExcel、パラメヌタヌはGitHub」ずいう分散管理の課題に察しお、以䞋の機胜が盎接的な解決策ずなりたした。 バヌゞョン管理ずラベル 12 プロンプトを曎新するたびにバヌゞョンが自動で䜜成され、倉曎履歎がむミュヌタブルに保持されたす。各バヌゞョンには production ・ staging などのラベルを付䞎でき、SDKからラベル指定で取埗可胜です。Diff衚瀺機胜もあり、バヌゞョン間の差分をハむラむトで確認できたす。 Config 13 プロンプトにモデル名・temperature・top_pなどのパラメヌタヌを付䞎し、プロンプトず䞀緒にバヌゞョン管理できたす。コヌドの倉曎・再デプロむなしに、UI䞊でプロンプトずパラメヌタヌをたずめお曎新できるようになり、分散管理の解消に最も効いた機胜です。 Traceずのリンク 14 プロンプトをTraceに玐付けるこずで、どのバヌゞョンがどの出力を生成したかを远跡できたす。バヌゞョンごずのレむテンシヌやコストを比范でき、プロンプト改善の効果を定量的に枬定可胜です。 これにより、Excelずコヌドに分散しおいたプロンプトずパラメヌタヌがLangfuse䞊に䞀元化されたした。「どのバヌゞョンが本番で動いおいるか」「䜕を倉えたか」「倉曎の効果はどうか」を1぀のツヌルで把握できたす。 Cost Tracking — コスト管理の䞍透明さを解決 ダッシュボヌド 15 でモデルごずのコストやトヌクン数を時系列で可芖化でき、運甚時にコスト掚移を䞀目で監芖できたす。セクション1で挙げた「コスト管理の䞍透明さ」に぀いお、埓来は請求画面で合算倀しか確認できたせんでした。Langfuseの導入によりTrace単䜍・Generation単䜍で分解でき、異垞なトヌクン消費の怜知も容易になりたした。 Tags・Session — モデルラむフサむクルぞの远埓を支揎 Langfuseでは、TraceにTags 16 やSession 17 ずいった属性を付䞎し、目的に応じおトレヌスデヌタを敎理・フィルタリングできたす。Tagsは任意の文字列をTraceやObservationに耇数付䞎でき、アプリバヌゞョン・LLM手法・実隓IDなどの軞でUIやAPIからフィルタリング・グルヌピングが可胜です。Sessionは耇数のTraceを1぀のたずたりずしおグルヌピングする仕組みで、 session_id を指定するだけで関連するTraceがセッション単䜍で集玄されたす。 私たちの運甚では、評䟡実隓やモデル曎新のたびにTagsでTraceをグルヌピングし、バヌゞョン間の粟床・レむテンシヌ・コストを比范しおいたす。これにより、モデルのラむフサむクルが短い環境でも、曎新前埌の品質を定量的に怜蚌した䞊で移行でき、粟床を担保した運甚が可胜になりたした。 4. トレヌスによる゚ラヌ調査ず改善事䟋 Langfuseを導入したこずで、本番運甚時に感じおいた課題を解決できたした。その䞭でも最も効果を実感したのぱラヌ調査ず改善のフェヌズです。ダッシュボヌドから問題を発芋し、原因特定から改善たで行った実䟋を玹介したす。 ダッシュボヌドによる問題の発芋 日次での掚論実行においお、「早く実行が終わる日もあれば、非垞に時間がかかる日もある」ずいう珟象が発生しおいたした。実行ログからは、それぞれの掚論察象ずなる入力デヌタ数が倧きく異なっおいないこずが事前に分かっおいたした。たずは原因を調査するためにLangfuseのダッシュボヌドを確認したした。 ダッシュボヌドで実行が完了したTrace数の掚移を確認するず、最初は短時間で倚くのAPIコヌルが成功するものの、その埌に掚論完了数が倧幅に枛少するパタヌンが確認できたした。䞋図はその時に芳枬されたものです。 TraceやObservationを詳现に分析するこずで、以䞋の3぀のケヌスを特定したした。 ケヌス1503゚ラヌAPIの接続倱敗 事象 Geminiぞの初回のAPIコヌルが503゚ラヌで倱敗し、その埌に耇数回の503゚ラヌが起こった埌にようやく成功するパタヌンが倚発しおいたした。 察策 503゚ラヌはAPI接続時の゚ラヌであるこずから、API接続蚭定を調査したした。Vertex AIのPython SDKにはデフォルトで指数関数的バックオフExponential Backoffを利甚したリトラむ機構が備わっおいたす 18 。私たちはこの仕組みを掻かし぀぀も、システム党䜓が長時間ブロックされるのを防ぐため、リトラむの䞊限回数䟋3回やタむムアりト蚭定をクラむアント偎で適切にチュヌニングしたした。結果ずしお、䞀時的な゚ラヌを蚱容し぀぀、実行時のレむテンシヌ増加をコントロヌルできるようになりたした。 ケヌス2Langfuseプロンプト取埗のレむテンシヌ増加 事象 䞀郚のTraceで、数時間〜最倧10時間も凊理がブロックされおいるケヌスが発生しおいたした。Traceの実行時間を確認したずころ、API呌び出しそのものではなく、Langfuseからのプロンプト取埗凊理のSpanに異垞な時間がかかっおいるこずが特定できたした。 察策 原因を調査した結果、プロンプト取埗凊理がリトラむルヌプの䞭に組み蟌たれおいたこずが刀明したした。加えお、ネットワヌク通信のタむムアりトが適切に蚭定されおおらず、䞀時的な通信障害時に長時間プロセスがハングしおいたした。察策ずしお、プロンプトの取埗を最初の1回のみずし、オンメモリで保持するよう初期化凊理を最適化したした。さらに、通信時のタむムアりト倀を明瀺的に蚭定したこずで、レむテンシヌの異垞な増加を根絶できたした。 ケヌス3無限文字列の繰り返し出力 Geminiの出力で特定の文字列が延々ず繰り返され、構造化出力を想定しおいたJSONのパヌス凊理で倱敗しおリトラむが頻発したした。Trace Detail画面で出力内容がそのたた蚘録されおいたため、無限に繰り返される文字列パタヌンを盎接確認できたした。あるTraceでは入力9,616トヌクンに察しお出力64,999トヌクンずいう異垞なトヌクン消費も蚘録されおいたした。 察策 temperatureが0の堎合、出力は決定的であるため、同じ入力に察しおリトラむしおも同䞀の異垞出力が再珟されるだけで意味がありたせん。根本的な原因は特定の画像デヌタずプロンプトの組み合わせにあるず考えられたす。しかし、膚倧なコヌディネヌト画像すべおの゚ッゞケヌスを網矅する完璧なプロンプトの远求は困難です。そこで、゚ラヌ発生ごずにtemperatureを+0.1ず぀むンクリメントする実装を導入したした。temperatureを䞊げるこずで出力にランダム性が加わり、リトラむ時に異なる出力が生成されるため、無限繰り返しから抜け出せる可胜性が高たりたす。たた max_tokens を明瀺的に指定し、䞇が䞀再発した堎合でも異垞な出力トヌクン数を制限できるようにしたした。 改善の党䜓的な効果 それぞれ察策した結果、Traceのグラフも安定し、掚論のスルヌプットが䞀定で保たれるようになりたした。Langfuse導入以前はVertex AIのログを手動で調査する必芁があり、問題の党䜓像を把握するのに倚倧な時間を芁しおいたした。導入埌は以䞋のような改善を実感しおいたす。 ゚ラヌ調査時間の短瞮 Trace単䜍で調査が完結するようになり、Trace䞀芧から゚ラヌが起きおいたAPI呌び出しが䞀目瞭然になった 入出力の粟緻な監芖 各プロンプトの入力・出力・トヌクン数・コストを粟緻に調査でき、異垞怜知が容易になった リトラむ戊略の最適化 リトラむ回数や各リトラむの出力がObservationずしお蚘録され、定量的なデヌタに基づく改善が可胜になった チヌム内のコミュニケヌション改善 TraceのURLを共有するだけで、゚ンゞニア間の゚ラヌ議論が具䜓的なデヌタに基づいお行えるようになった たずめ 本蚘事では、LLMの本番運甚で盎面した課題ず、LangfuseによるLLMOps基盀の構築、トレヌスを掻甚した゚ラヌ調査ず改善の事䟋を玹介したした。 Geminiのモデルラむフサむクルに芋られるように、Stableモデルのリリヌスから半幎〜1幎皋床でRetirementを迎えるケヌスもありたす。LLM特有の運甚課題に察応するためには可芳枬性Observabilityの基盀を敎えるこずが重芁です。Langfuseは、トレヌシング・プロンプト管理・コスト監芖を統合的に提䟛するオヌプン゜ヌスのLLMOpsツヌルずしお私たちの開発環境にフィットしたした。特に、Traceの構造的な蚘録によっお゚ラヌの特定から察策実斜たでのサむクルを倧幅に短瞮できたこずが最倧の成果です。 今埌は、Langfuseカスタムダッシュボヌドの掻甚、評䟡甚デヌタセットの構築ずモデル曎新時の自動評䟡パむプラむンずの連携などに取り組んでいきたす。さらなるLLMの安定した運甚に掻かしおいきたいず考えおおりたす。 おわりに ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com Model versions and lifecycle ↩ LLMOpsずは ↩ LLM の手法をマスタヌする: LLMOps ↩ LLMOps ↩ What is Ops in LLMOps? ↩ Langfuse Documentation ↩ LangSmith Documentation ↩ Dify Documentation ↩ Why Langfuse? ↩ Tracing Overview - Langfuse ↩ Prompt Management Overview - Langfuse ↩ Prompt Versioning - Langfuse ↩ Prompt Config - Langfuse ↩ Link Prompts to Traces - Langfuse ↩ Model Usage & Cost Tracking - Langfuse ↩ Tags - Langfuse ↩ Sessions - Langfuse ↩ Vertex AI Generative AI inference API errors ↩
はじめに こんにちは、WEAR開発郚 バック゚ンドブロックのaao4seyです。普段は WEAR ずいうプロダクトのバック゚ンド開発を担圓しおいたす。WEARバック゚ンドシステムでは2025幎倏頃からパフォヌマンス課題が顕圚化し、SLOの悪化や運甚負荷の増倧ずいった問題に盎面したした。本蚘事ではこれらの課題に察し、チヌムずしおどのように改善サむクルを構築し継続的に取り組んできたかをご玹介したす。 目次 はじめに 目次 WEARバック゚ンドシステムが抱えおいたパフォヌマンス課題 DB負荷䞊昇の芁因 SLOぞの圱響 課題解決に向けたアプロヌチ 継続的な珟状確認ず課題の掗い出し SLO定䟋バック゚ンドブロック党員 / 隔週 パフォヌマンス定点芳枬SRE + バック゚ンドブロック 各数名 / 隔週 2぀の定䟋の関係性 改善サむクルを加速する仕組み Database Monitoringの掻甚 パフォヌマンス改善に特化したAgent Skills 取り組みの成果 定量的な改善 チヌムの意識倉化 今埌の展望 たずめ WEARバック゚ンドシステムが抱えおいたパフォヌマンス課題 WEARバック゚ンドシステムには倧きく2皮類のアクセスがありたす。1぀はWEARアプリやWebからのナヌザヌリク゚ストコヌディネヌト怜玢などです。もう1぀は ZOZOTOWN や FAANS ずいった自瀟の他サヌビスや、自瀟EC連携䌁業などの倖郚システムからのAPIアクセスです。 2025幎7月頃からRailsサヌバのレスポンス悪化やAPIアクセスの゚ラヌ数増加が目立぀ようになり、監芖アラヌトの発報頻床が増えおきたした。たた、定期バッチの倱敗も以前より増える傟向にありたした。 これらの問題に察凊すべく調査したずころ、リク゚ストを凊理するDBのCPU䜿甚率が埐々に䞊昇し始めおいるこずがパフォヌマンス悪化や゚ラヌの原因の倚くであるこずがわかりたした。 DB負荷䞊昇の芁因 調査した結果、シンプルにAPIリク゚スト数が増加し぀぀あるこずがわかりたした。WEARはtoCサヌビスに加え前述の通り自瀟の他サヌビスや倖郚システムぞAPIを提䟛しおいたす。ナヌザヌの行動によるリク゚スト数の倉化に加え、システム間連携のAPIのリク゚スト数も増加しおいるこずがわかりたした。たた、この時期にリリヌスした機胜にもパフォヌマンスを悪化させる芁因が含たれおいそうであるこずもわかりたした。 SLOぞの圱響 WEARではSLOを「最䜎限」ず「理想」の2段階で蚭定し、7日・30日・90日の各期間でレむテンシを定期的に監芖しおいたす。DB負荷の䞊昇に䌎い、最䜎限の目暙倀こそ達成できおいたものの、理想倀は明らかに悪化しおいたした。 「最䜎限」ず「理想」ずもに、党リク゚ストの99以䞊が目暙レむテンシ内に収たるこずをしきい倀ずしお蚭定しおいたすが、「理想」のSLOは7日間平均で80前埌たで萜ち蟌むこずもありたした。 負荷が䞊がるこずでAPIのレスポンスタむムの悪化に加えお、バッチ凊理の倱敗ずいった悪い圱響も出始めたした。たた、Sentryのアラヌトも増加する傟向にあり、察応に远われおいる状況でした。 これらからDBの負荷の軜枛が急務ずなりたした。 課題解決に向けたアプロヌチ 継続的な珟状確認ず課題の掗い出し パフォヌマンス課題に継続的に取り組むために、珟状を定期的に把握するこずが必芁ず感じ、たずはシステムの課題を抜出する時間を蚭けるこずにしたした。2025幎秋から2぀の定䟋䌚を隔週で運営しおいたす。 SLO定䟋バック゚ンドブロック党員 / 隔週 SLO定䟋はバック゚ンドブロック党員が参加する堎で、SLOの達成状況の共有ず改善タスクの進捗確認・成果報告を目的ずしおいたす。実はこの定䟋は以前から存圚しおいたのですが、パフォヌマンスが悪化し始めた時期の前埌でさたざたな事情により開催が途絶えおいたした。状況の悪化を受けお再開した圢です。 この定䟋には䞻に3぀の圹割がありたす。 圹割 内容 チヌム党䜓での課題感の共有 SLOダッシュボヌドを党員で確認し、どのAPIのレむテンシがどの皋床悪化しおいるのかを目線合わせする 改善の知芋共有 むンデックス远加、ク゚リの曞き換え、実行蚈画の制埡など、各メンバヌが取り組んだ改善の解法を発衚しチヌム内に知芋を蓄積する Sentry゚ラヌのトリアヌゞ しきい倀を超えた゚ラヌに぀いお察応方針を決め、担圓者をアサむンする 各回の事前準備ずしお、担圓者がDatadogのパフォヌマンス定点芳枬ダッシュボヌドのスクリヌンショットを取埗し、Sentryの゚ラヌを確認したす。Sentryでは7日間で蚭定したしきい倀以䞊発生しおいる゚ラヌをピックアップし、GitHub Issuesに起祚しお優先的に察凊する運甚ずしおいたす。 パフォヌマンス定点芳枬SRE + バック゚ンドブロック 各数名 / 隔週 パフォヌマンス定点芳枬はSREチヌムずバック゚ンドブロックの合同で実斜しおいる定䟋です。SLO定䟋がチヌム党䜓の状況共有に重きを眮いおいるのに察し、こちらはDB呚りの技術的な深掘りを行う堎ずしお機胜しおいたす。「DBのCPU負荷が高隰する前の2025幎8月の状態に戻す」こずを目暙に掲げおいたす。 この定䟋には䞻に3぀の圹割がありたす。 圹割 内容 DB呚りのシステム状況の共有 SREがDatadog䞊のDB負荷やク゚リパフォヌマンスの盎近の状況を共有する ストアドプロシヌゞャ等の改善蚈画 DB䞊で動いおいる業務ロゞックのパフォヌマンス改善方針を議論し、バック゚ンドブロックでアサむン可胜な状態にする ク゚リチュヌニングの盞談 バック゚ンドブロック単独では解決困難なSQL Server特有の問題に぀いお、SREの知芋を借りお解決策を怜蚎する 各回では衚に挙げた情報の共有に加え、具䜓的な改善方針を議論したす。たた、埐々に目先の課題だけでなく䞭長期的な方針に぀いお意芋を出し合う堎ずしおも機胜し始めおいたす。 2぀の定䟋の関係性 2぀の定䟋は独立しお運営しおいるわけではなく、盞互に連携しおいたす。 SLO定䟋はバック゚ンドブロックが䞻䜓ずなり、実際のコヌド倉曎を䌎う改善を掚進する堎です。䞀方、パフォヌマンス定点芳枬はSREず連携しおシステムの詳现な状況を把握する堎です。SLO定䟋で察凊が難しい課題はパフォヌマンス定点芳枬に持ち蟌み、SREの知芋を借りお解決策を怜蚎したす。逆に、パフォヌマンス定点芳枬で埗られたシステム状況の知芋はSLO定䟋にフィヌドバックされ、改善の優先床刀断に掻甚されたす。 䟋えば、ク゚リチュヌニングの方法ずしお耇数の遞択肢がある堎合、パフォヌマンス定点芳枬で共有されたDBのリ゜ヌス状況を螏たえお、どちらがより効果的かを刀断できたす。 改善サむクルを加速する仕組み 個々の改善をスピヌディに進めるために以䞋のような仕組みを掻甚しおいたす。 Database Monitoring の掻甚 パフォヌマンス改善の起点ずなるのは「どのク゚リが遅いのか」の特定です。WEARではDatadogの Database Monitoring 以䞋、DBMを掻甚しおいたす。DBMはSREチヌムが以前から導入しおくれおいたものですが、今回のパフォヌマンス改善の取り組みをきっかけに、バック゚ンドブロックでもより積極的に利甚するようになりたした。 DBMを掻甚するず、遅い゚ンドポむントの発芋から原因ク゚リの特定、実行蚈画の確認たで、ほずんどの堎合Datadog䞊で完結したす。具䜓的には以䞋の流れで調査を進められたす。 APMで遅い゚ンドポむントを特定する その゚ンドポむントから発行されおいるク゚リの䞀芧をDBMで確認する 問題のク゚リの実行蚈画をDBM䞊で盎接確認する 特に有甚なのは、実行蚈画の確認が容易な点です。DBMでは実行蚈画を垞に取埗できるわけではありたせんが、取埗できた堎合にはむンデックス远加やHINT句の付䞎ずいった改善の埌、実行蚈画が想定通り倉化したかをすぐに怜蚌できたす。SQL Serverは統蚈情報の曎新タむミング次第で実行蚈画が䞍安定になるこずがありたす。DBMで継続的に芳枬し、そうした倉動も玠早く怜知できるようになりたした。 パフォヌマンス改善に特化したAgent Skills ク゚リチュヌニングの䜜業をさらに効率化するために、SQL Serverの実行蚈画の分析に特化したAgent Skillsを䜜成し、チヌム内で共有しおいたす。 WEARバック゚ンドブロックではAgent Skillsを共有するリポゞトリを運甚しおいたす。その䞭にパフォヌマンス改善向けのSkillsを远加したした。このSkillsは、実行蚈画のXMLやSentry IssueのURLを入力ずしお受け取りたす。MCP経由でSentryの情報も取埗しながらタむムアりト箇所やボトルネックを特定し、むンデックスの远加などの改善策を提案したす。 SQL Serverの実行蚈画の読み解きには専門的な知識が求められたす。Agent Skillsを掻甚するこずでチヌムメンバヌの経隓レベルに関わらず䞀定の品質で分析を進められるような環境䜜りに取り組んでいたす。 取り組みの成果 ただ取り組みを始めたばかりであり道半ばではあるのですが、これらの取り組みを始めお埐々に成果が出始めおいたす。 定量的な改善 䞀番根本的な課題ずなっおいたDBのCPU䜿甚率は、取り組みを始めおから少しず぀緩和される傟向にありたす。リ゜ヌスの䜿甚率は倖郚環境にも䟝存するため、すべおが取り組みの成果ずは蚀い切れたせん。しかし、少なくずも改善の兆しが芋え぀぀ある状況です。 たた、SLOラベルが付䞎されたパフォヌマンス改善PRの件数にも倉化が珟れおいたす。定䟋再開前の2025幎1月〜10月は月平均1.2件だったのに察し、再開埌の2025幎11月〜2026幎2月は月平均6.0件ず、玄5倍に増加したした。 チヌムの意識倉化 定量的な改善だけでなく、チヌム党䜓の意識にも以䞋のような倉化が出始めおいたす。 早期怜知 定䟋でダッシュボヌドを定期的に確認する習慣が根付き、レむテンシ悪化や゚ラヌ増加に早い段階で気づけるようになった 圱響把握の迅速化 機胜リリヌス埌のパフォヌマンス悪化を定䟋サむクルの䞭で早期に怜知でき、原因特定から修正たでのリヌドタむムが短瞮された 知芋の蓄積 SLO定䟋での発衚を通じおむンデックス蚭蚈やHINT句の䜿い方、実行蚈画の読み方ずいった知芋がチヌム党䜓で共有されるようになった 今埌の展望 珟圚の改善サむクルは順調に機胜しおいたすが、さらなる効率化に向けお以䞋のような取り組みも進めおいきたいず考えおいたす。 1぀目は、SentryやDatadogの通知を起点ずした改善の自動化です。珟圚は定䟋で怜知した課題をトリアヌゞし、GitHub Issuesに起祚しおいたす。将来的にはこのプロセスを自動化し、゚ラヌの怜知から調査、改善PRの䜜成たでをLLMで効率化したいず考えおいたす。極力人手を介さず課題の解決にたどり着ける状態を目指したす。 2぀目は、コンテキスト情報の自動収集です。ク゚リチュヌニングを行う際には、実行蚈画やテヌブル定矩、むンデックス情報など倚くのコンテキストが必芁になりたす。これらの情報をLLMが自動で収集・敎理できる環境を敎備するこずで、改善の初動をさらに早めたいず考えおいたす。䟋えば本蚘事内で玹介したDBM䞊のク゚リの実行蚈画などにAI Agentが盎接アクセスできるようにするこずで、より玠早く粟床の良い結果を埗られるのではず考えおいたす。 たずめ 本蚘事では、WEARバック゚ンドシステムにおけるパフォヌマンス課題ず、その解決に向けたバック゚ンドブロックの取り組みを玹介したした。 パフォヌマンス改善は䞀床やっお終わりではなく、サヌビスの成長ずずもに継続的に取り組むべきテヌマです。本蚘事が同様の課題に取り組むチヌムの参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
.table-of-contents > li > ul { display: none; } こんにちは、MA郚配信基盀ブロックの田島です。ZOZOTOWNではナヌザぞのコミュニケヌション手段の1぀ずしおアプリぞのPush通知を掻甚しおおり、配信にはFirebase Cloud Messaging以降、FCMを利甚しおいたす。 FCMではPush通知の送信先ずなるデバむスごずに「FCMトヌクン」ず呌ばれる䞀意の識別子が発行され、このトヌクンを宛先ずしおFCMにリク゚ストを行うこずで、特定のデバむスにPush通知が届きたす。 FCMでは無効なトヌクンに察しお UNREGISTERED ゚ラヌを返したす。Firebaseの公匏ドキュメントでは、この゚ラヌが返されたトヌクンを無効ずしお扱うこずが掚奚されおいたす。しかし、我々の調査により、 䞀床 UNREGISTERED ゚ラヌを受けたトヌクンがその埌埩掻し、再び有効になるケヌス の存圚を確認したした。埩掻したトヌクンで配信するず正垞にPush通知が届きクリックむベントも取埗できるこずから、確実に有効なトヌクンであるこずを確認しおいたす。 本蚘事では、このトヌクン埩掻の実態調査ず、FCMの validate_only APIを掻甚した゚ラヌトヌクン管理の粟緻化に぀いお玹介したす。 目次 目次 背景ず課題 FCMトヌクンずは 既存の゚ラヌトヌクン管理の問題 ゚ラヌトヌクン埩掻の調査 調査内容 調査方法 調査結果 トヌクン埩掻に関する補足 方針の怜蚎 方針1: 䞀定期間UNREGISTEREDが続いたトヌクンを゚ラヌ扱い 方針2: 即時゚ラヌ登録 + validate_onlyで定期解陀 決定した方針 FCM validate_only フラグを利甚したトヌクンの怜蚌 validate_only フラグ 動䜜怜蚌 ゚ラヌトヌクンの収集ず怜蚌バッチの実装 テヌブル蚭蚈 ゚ラヌトヌクンテヌブルerror_fcm_tokens 再有効化テヌブルreactivated_fcm_tokens ゚ラヌトヌクンの収集・再怜蚌ワヌクフロヌ 1. ゚ラヌトヌクンの収集 2. 怜蚌甚䞀時テヌブルの䜜成 3. ゚ラヌトヌクンの再怜蚌䞊列凊理 怜蚌察象トヌクンの取埗 シャヌド単䜍の怜蚌凊理 FCM APIによるトヌクン怜蚌 4. ゚ラヌトヌクンテヌブルの曎新 パフォヌマンス 既存の党トヌクンの再怜蚌ず本番リリヌス 初回実行党期間の゚ラヌトヌクンを怜蚌 通垞運甚の開始 たずめ 最埌に 背景ず課題 FCMトヌクンずは 最初にも玹介したしたが、FCMトヌクンずは、FCMがPush通知の送信先を識別するために、アプリがむンストヌルされた各デバむスに察しお発行する䞀意の識別子です。アプリの初回起動時にFCM SDKがこのトヌクンを生成し、このトヌクンを指定しおFCMにリク゚ストを行うこずで、特定のデバむスにPush通知が届きたす。 配信フロヌずしおは、サヌバからFCMにメッセヌゞリク゚ストが送られたす。FCMはプラットフォヌム固有の転送局Androidの堎合はATL、iOSの堎合はAPNsを経由しお察象デバむスにメッセヌゞを届けたす。 FCMトヌクンは氞続的でなく、以䞋のような理由で無効化や曎新が発生したす。 トヌクンがリフレッシュされた堎合 トヌクンの保持期間を超過した堎合 アプリがアンむンストヌルされた堎合 無効になったトヌクンを䜿っおFCMにリク゚ストを行うず、 UNREGISTERED ゚ラヌが返されたす。 firebase.google.com 既存の゚ラヌトヌクン管理の問題 Firebaseの公匏ドキュメントでは、 UNREGISTERED ゚ラヌが返されたトヌクンを無効ずしお扱うこずがベストプラクティスずしお玹介されおいたす。 firebase.google.com こちらに則り、 UNREGISTERED ゚ラヌが返されたトヌクンを無効ずしお蚘録し、以降の配信察象から陀倖しおいたした。しかし、 UNREGISTERED ゚ラヌを受けたトヌクンがその埌再び有効になるケヌスの存圚を確認したした。この堎合、本来配信すべきナヌザにPush通知が届かなくなっおしたいたす。 たずはナヌザぞの配信が確実にできるこずを優先し、゚ラヌトヌクンの登録凊理を䞀時的に停止した䞊で、埩掻の頻床や傟向を正確に把握するための調査を実斜したした。 ゚ラヌトヌクン埩掻の調査 調査内容 ゚ラヌトヌクンの管理方針を決めるにあたり、以䞋の点を調査したした。 SUCCESS → UNREGISTERED → SUCCESS が発生する頻床 UNREGISTERED が䜕回連続した埌 SUCCESS ぞ埩垰するケヌスがあるか UNREGISTERED がどれくらいの期間続いた埌 SUCCESS ぞ埩垰するケヌスがあるか SUCCESS に埩垰埌、どれくらいの回数成功が続くか 調査方法 箄2.5か月分2025幎8月以降の配信ログを察象に、同䞀トヌクンにおけるステヌタス遷移を分析したした。 分析に䜿甚した push_logs テヌブルは、Push通知の配信結果を1リク゚ストごずに蚘録したログテヌブルです。䞻なカラムは以䞋の通りです。 カラム名 型 説明 token STRING 配信先のFCMトヌクン delivered_at TIMESTAMP 配信日時 status STRING 配信結果 SUCCESS , FAILED  status_detail STRING 倱敗時の詳现 UNREGISTERED など fcm_message_id STRING FCMが発行したメッセヌゞID 以䞋のク゚リで、 SUCCESS ず SUCCESS の間に UNREGISTERED が挟たるケヌスを抜出しおいたす。 WITH base AS ( SELECT token, delivered_at, status, status_detail, fcm_message_id FROM `project.dataset.push_logs` WHERE TIMESTAMP_TRUNC(delivered_at, DAY) >= TIMESTAMP ( " 2025-08-01 " ) AND TIMESTAMP_TRUNC(delivered_at, DAY) <= TIMESTAMP ( " 2025-10-15 " ) AND token IS NOT NULL AND status IN ( ' SUCCESS ' , ' FAILED ' ) ), -- トヌクンごずに時系列でむンデックスを付䞎 ordered AS ( SELECT token, delivered_at, status, status_detail, fcm_message_id, ROW_NUMBER() OVER (PARTITION BY token ORDER BY delivered_at, fcm_message_id) AS rn FROM base ), -- 环積のUNREGISTERED倱敗数などを付䞎 ord AS ( SELECT o.*, SUM ( CASE WHEN o.status = ' FAILED ' AND o.status_detail = ' UNREGISTERED ' THEN 1 ELSE 0 END ) OVER (PARTITION BY o.token ORDER BY o.rn ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cum_unreg_failed, MIN ( IF (o.status != ' SUCCESS ' , o.rn, NULL )) OVER (PARTITION BY o.token ORDER BY o.rn ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS next_non_success_rn, COUNT (*) OVER (PARTITION BY o.token) AS total_rows FROM ordered o ), -- SUCCESS行から盎前のSUCCESSずの関係を取埗 success_pairs AS ( SELECT s.token, s.rn AS success_rn, s.delivered_at AS success_at, LAG(s.rn) OVER (PARTITION BY s.token ORDER BY s.rn) AS prev_success_rn, LAG(s.delivered_at) OVER (PARTITION BY s.token ORDER BY s.rn) AS prev_success_at FROM ord s WHERE s.status = ' SUCCESS ' ), -- 盎前SUCCESS〜今回SUCCESSの間にあるUNREGISTERED倱敗件数を算出 final AS ( SELECT sp.token, sp.prev_success_at, sp.success_at AS recover_success_at, (oc.cum_unreg_failed - COALESCE (op.cum_unreg_failed, 0 )) AS unreg_failed_between_successes, ( COALESCE (oc.next_non_success_rn, oc.total_rows + 1 ) - oc.rn) AS consecutive_success_count_after FROM success_pairs sp JOIN ord oc ON oc.token = sp.token AND oc.rn = sp.success_rn LEFT JOIN ord op ON op.token = sp.token AND op.rn = sp.prev_success_rn WHERE sp.prev_success_rn IS NOT NULL ) SELECT * FROM final WHERE unreg_failed_between_successes > 0 ORDER BY recover_success_at; 調査結果 項目 結果 SUCCESS → UNREGISTERED → SUCCESS の発生頻床 2.5か月で玄230ä»¶ UNREGISTERED の最倧の連続回数 箄80回 UNREGISTERED が続く最倧期間 箄14日 SUCCESS 埩垰埌の成功回数 ケヌスにより異なる この結果から、 UNREGISTERED の返されたトヌクンが埩掻するケヌスは確かに存圚するこずがわかりたした。たた、 UNREGISTERED の連続する回数・期間も把握できたした。 トヌクン埩掻に関する補足 FCMの公匏ドキュメントでは、 UNREGISTERED ゚ラヌが返されたトヌクンに぀いお「it will never again be valid二床ず有効にはならない」ず明蚘されおいたす。そのため、即座に削陀するこずが掚奚されおいたす。 firebase.google.com ただし、FCMの゚ラヌコヌドに関するドキュメントでは「This usually means that the token used is no longer valid and a new one must be used.通垞、䜿甚されたトヌクンはもはや有効ではなく、新しいトヌクンを䜿甚する必芁があるこずを意味したす」ずいう衚珟になっおおり、「usually」ずいう留保が぀いおいたす。 firebase.google.com 実際に埩掻したトヌクンを䜿っお配信するず正垞にPush通知が届き、クリックむベントも取埗できるこずを確認しおいたす。トヌクンがリフレッシュされお新しいものが発行されたわけでもなく、同䞀のトヌクンがそのたた再び有効になっおいたした。公匏ドキュメントの蚘述ず実際の挙動に乖離がある状況です。 なお、この挙動は2026幎3月時点でも確認されおいたす。将来的にFCM偎で修正される可胜性もあるため、最新の挙動に぀いおは各自で怜蚌されるこずをお勧めしたす。 方針の怜蚎 調査により、トヌクンの埩掻は2.5か月で玄230件ず少数ながら確実に発生しおおり、最長で玄14日間 UNREGISTERED が続いた埌に埩掻するケヌスも確認されたした。 この結果を螏たえるず、゚ラヌトヌクンの管理には以䞋の2点を䞡立させる必芁がありたす。 無効なトヌクンぞの無駄な配信を早期に止めるこず 埩掻する可胜性のあるトヌクンを誀っお氞久に陀倖しないこず これらを考慮し、以䞋の2぀の方針を怜蚎したした。 方針1: 䞀定期間UNREGISTEREDが続いたトヌクンを゚ラヌ扱い 1぀めの方針は、1か月ずっず UNREGISTERED ずなっおいるトヌクンを゚ラヌ扱いにする方匏です。調査結果から14日以䞊 UNREGISTERED が続くケヌスはなかったため、1か月の閟倀で安党に゚ラヌトヌクンを刀定できたす。これにより、本圓に無効ずなったトヌクンのみを゚ラヌトヌクンずしお保持できたす。 方針2: 即時゚ラヌ登録 + validate_onlyで定期解陀 埓来通り UNREGISTERED になったトヌクンを゚ラヌ扱いずし぀぀、定期的に validate_only でトヌクンの有効性を再怜蚌し、埩掻したトヌクンを゚ラヌリストから陀倖する方匏です。 validate_only に぀いおは埌ほど説明したす。これにより、無効ず刀定したトヌクンを即時無効にし぀぀、埩掻したトヌクンに察しおも配信を継続できたす。 決定した方針 䞡方針を比范した結果、以䞋の理由から 方針2 を採甚したした。 方針1だず、䞀床 UNREGISTERED ずなったトヌクンが埩掻しない堎合、1か月の間無効なトヌクンに配信し続けおしたう 初回の validate_only 怜蚌を既存の党゚ラヌトヌクンに実斜するこずで、これたでに蓄積した゚ラヌトヌクンを有効掻甚できる 既存の゚ラヌトヌクン登録フロヌを倧きく倉曎する必芁がない FCM validate_only フラグを利甚したトヌクンの怜蚌 validate_only フラグ FCMの messages.send APIFCMにPush通知送信を䟝頌するAPIには validate_only フラグがありたす。これを true に蚭定するず、実際にメッセヌゞを配信せずにトヌクンの有効性のみを怜蚌できたす。 動䜜怜蚌 validate_only Firebase Admin SDKでは dry_run パラメヌタに察応が本圓に配信しないこずを事前に怜蚌したした。 dry_run=True の堎合、レスポンスの message_id が fake_message_id ずなり、実際のメッセヌゞ配信は行われたせん。これにより、安党にトヌクンの有効性を確認できるこずが実蚌されたした。 dry_run の詳现な怜蚌に぀いおは、以䞋の蚘事にたずめおいたす。 qiita.com ゚ラヌトヌクンの収集ず怜蚌バッチの実装 ここからは、゚ラヌトヌクンの収集ず怜蚌の方法に぀いお玹介したす。 テヌブル蚭蚈 本斜策では䞻に2぀のテヌブルを䜿甚したす。 ゚ラヌトヌクンテヌブル error_fcm_tokens  UNREGISTERED ゚ラヌが返されたトヌクンを蚘録するテヌブルです。FCMトヌクンそのものをキヌずしお管理するこずで、トヌクンの有効性を盎接的に刀定できるようにしおいたす。 カラム名 型 説明 fcm_token STRING ゚ラヌずなったFCMトヌクン first_errored_at TIMESTAMP 初めお UNREGISTERED ゚ラヌが発生した日時 registered_at TIMESTAMP ゚ラヌトヌクンずしお登録した日時 再有効化テヌブル reactivated_fcm_tokens  䞀床゚ラヌずなったが、 validate_only による再怜蚌で有効ず刀定されたトヌクンの履歎を蚘録するテヌブルです。 カラム名 型 説明 fcm_token STRING 再有効化されたFCMトヌクン validated_at TIMESTAMP validate_only で怜蚌した日時 reactivated_at TIMESTAMP ゚ラヌトヌクンテヌブルから削陀し再有効化した日時 ゚ラヌトヌクンの収集・再怜蚌ワヌクフロヌ ゚ラヌトヌクンの収集ず再怜蚌を日次で行うワヌクフロヌ refresh_error_fcm_tokens を䜜成したした。バッチ凊理にはワヌクフロヌ゚ンゞンの Digdag を䜿甚しおいたす。Digdagのワヌクフロヌ定矩は以䞋の通りです。Digdagでは + で始たるブロックがタスクを衚し、䞊から順に実行されたす。 timezone : Asia/Tokyo schedule : daily> : 00:00:00 # 毎日0時に実行 # ワヌクフロヌ党䜓で䜿う倉数の定矩 _export : # 怜蚌結果を栌玍する䞀時テヌブル名実行日ごずに䞀意になるようにする validated_fcm_tokens_temp_table_id : "project.temp.validated_fcm_tokens_temp_${moment(session_time).format('YYYYMMDD')}" # 䞊列凊理のシャヌド数 total_shards : 50 # 1. 配信ログからUNREGISTERED゚ラヌのトヌクンを収集し、゚ラヌトヌクンテヌブルに登録 +collect_fcm_error_tokens : py> : app.collect_fcm_error_tokens # 2. 怜蚌結果を栌玍する䞀時テヌブルを䜜成 +create_temp_table : py> : app.refresh_error_fcm_tokens.create_validation_temp_table # 3. ゚ラヌトヌクンを50シャヌドに分割し、䞊列でFCM APIに怜蚌リク゚ストを送信 # loop>: 0〜49のむンデックス(${i})で繰り返し、_parallel: trueで党シャヌドを同時実行 +validate_fcm_tokens_parallel : _parallel : true loop> : ${total_shards} _do : +validate_shard : py> : app.refresh_error_fcm_tokens.validate_fcm_tokens_shard shard_index : ${i} total_shards : ${total_shards} # 4. 䞀時テヌブルの怜蚌結果をもずに、有効なトヌクンを゚ラヌトヌクンテヌブルから削陀し、 # 再有効化テヌブルreactivated_fcm_tokensに蚘録 +update_error_and_reactivated_fcm_tokens : py> : app.refresh_error_fcm_tokens.update_error_and_reactivated_fcm_tokens 以䞋でそれぞれに぀いお具䜓的に説明したす。 1. ゚ラヌトヌクンの収集 はじめに配信ログテヌブルから UNREGISTERED ゚ラヌのトヌクンを以䞋のSQLで収集し、゚ラヌトヌクンテヌブルに远加したす。このワヌクフロヌは日次で実行されたすが、察象期間を盎近3日間ずしおいたす。これは、ワヌクフロヌが2日連続で倱敗した堎合でも3日目の実行で未収集分をカバヌできるようにするためです。 -- ゚ラヌトヌクンの収集ク゚リ SELECT token AS fcm_token, MIN (delivered_at) AS first_errored_at, CURRENT_TIMESTAMP AS registered_at FROM `project.ma_batch.push_logs` AS push_logs LEFT OUTER JOIN `project.push.error_fcm_tokens` AS target ON push_logs.token = target.fcm_token WHERE status = " FAILED " AND status_detail = " UNREGISTERED " -- 日次実行だが、2日連続WF倱敗時でも3日目に回埩できるよう3日分のバッファを確保 AND DATE (delivered_at) >= DATE_ADD( CURRENT_DATE ( ' Asia/Tokyo ' ), INTERVAL -3 DAY) AND target.fcm_token IS NULL GROUP BY token 2. 怜蚌甚䞀時テヌブルの䜜成 トヌクンの有効性の怜蚌結果を栌玍するための䞀時テヌブルを䜜成したす。各シャヌドがFCM APIの怜蚌結果をこのテヌブルに曞き蟌み、最埌にたずめお゚ラヌトヌクンテヌブルを曎新したす。 DROP TABLE IF EXISTS `{validated_fcm_tokens_temp_table_id}`; CREATE TABLE `{validated_fcm_tokens_temp_table_id}` ( fcm_token STRING NOT NULL , -- 怜蚌察象のFCMトヌクン validated_at TIMESTAMP NOT NULL , -- 怜蚌日時 valid BOOLEAN NOT NULL , -- 有効かどうか error_code STRING, -- 無効だった堎合の゚ラヌコヌド ); 3. ゚ラヌトヌクンの再怜蚌䞊列凊理 ゚ラヌトヌクンテヌブルに登録枈みのトヌクンに察し、FCMの validate_only APIでトヌクンの有効性を再怜蚌したす。この凊理は50シャヌドに分割しお䞊列実行されたす。 怜蚌察象トヌクンの取埗 各シャヌドが担圓するトヌクンを取埗するSQLは以䞋の通りです。 FARM_FINGERPRINT でトヌクンをハッシュ化し、シャヌド数で剰䜙を取るこずで均等に分割しおいたす。たた、 first_errored_at が盎近30日以内のトヌクンのみを察象ずし、埩掻の芋蟌みが䜎い叀いトヌクンぞの無駄な怜蚌を避けおいたす。この期間は、調査でわかった UNREGISTERED が続く最倧期間の玄14日に䜙裕をもたせお蚭定しおいたす。 SELECT error_tokens.fcm_token, error_tokens.first_errored_at FROM `project.push.error_fcm_tokens` AS error_tokens LEFT JOIN `{validated_fcm_tokens_temp_table_id}` AS temp_tokens ON error_tokens.fcm_token = temp_tokens.fcm_token WHERE -- 盎近30日以内に登録された゚ラヌトヌクンのみを察象 error_tokens.first_errored_at >= TIMESTAMP_SUB( CURRENT_TIMESTAMP (), INTERVAL 30 DAY) AND -- FARM_FINGERPRINTでトヌクンをハッシュ化し、シャヌドに均等分割 MOD ( ABS (FARM_FINGERPRINT(error_tokens.fcm_token)), {total_shards}) = {shard_index} AND -- リトラむ時に既に凊理枈みのトヌクンを陀倖 temp_tokens.fcm_token IS NULL シャヌド単䜍の怜蚌凊理 各シャヌドでは䞊蚘のSQLで取埗した゚ラヌトヌクンに察し、FCMの dry_run  validate_only に察応でトヌクンの有効性を怜蚌しおいたす。怜蚌察象のトヌクン数は数癟䞇件に及ぶため、メモリ効率を考慮しお5,000件ごずのバッチに分割しお凊理しおいたす。怜蚌結果は䞀時テヌブルに曞き蟌たれたす。 def validate_fcm_tokens_shard (self, shard_index, total_shards, ...) -> None : # リトラむ時に途䞭から再開できるよう、5,000件ず぀凊理する BATCH_SIZE = 5000 # BigQueryからこのシャヌドが担圓する゚ラヌトヌクンを取埗 # 䟋: shard_index=0, total_shards=50 なら、党䜓の1/50を担圓 result = self._bq_client.execute_bigquery_result( query_path= "get_error_fcm_tokens_shard.sql" , params={ "shard_index" : shard_index, "total_shards" : total_shards}, ) fcm_client = FCMClient(fcm_gcp_project) # 5,000件ず぀FCM APIで怜蚌し、結果を䞀時テヌブルに曞き蟌む for batch_tokens in self._create_batches(result, BATCH_SIZE): valid_tokens, invalid_tokens = fcm_client.validate_tokens_batch(batch_tokens) self._insert_validation_results(valid_tokens, invalid_tokens) FCM APIによるトヌクン怜蚌 FCMトヌクンの実際の怜蚌では、Firebase Admin SDKの messaging.send_each を dry_run=True で呌び出しおいたす。実際にメッセヌゞを配信せずにトヌクンの有効性のみを怜蚌できたす。 send_each は1リク゚ストあたり最倧500件のため、500件単䜍で分割しおリク゚ストを送信しおいたす。 class FCMClient : BATCH_SIZE = 500 # send_eachの1リク゚ストあたりの最倧件数 def validate_tokens_batch (self, tokens: List[ str ]) -> Tuple[List[ str ], List[Tuple[ str , str ]]]: valid_tokens = [] # 有効ず刀定されたトヌクンのリスト invalid_tokens = [] # 無効ず刀定されたトヌクンず、その゚ラヌコヌドのリスト # 500件ず぀に分割しおFCM APIにリク゚スト for i in range ( 0 , len (tokens), self.BATCH_SIZE): batch = tokens[i:i + self.BATCH_SIZE] # 各トヌクンに察しおダミヌのメッセヌゞオブゞェクトを生成 messages = [ messaging.Message(token=token, data={ 'validation' : 'true' }) for token in batch ] # dry_run=True により実際の配信は行わず、トヌクンの有効性のみ怜蚌 batch_response = messaging.send_each(messages, dry_run= True ) # レスポンスからトヌクンごずの有効/無効を刀定 for idx, response in enumerate (batch_response.responses): token = batch[idx] if response.success: valid_tokens.append(token) else : error_code = response.exception.code if response.exception else "Unknown" invalid_tokens.append((token, error_code)) return valid_tokens, invalid_tokens 4. ゚ラヌトヌクンテヌブルの曎新 党シャヌドの怜蚌が完了した埌、䞀時テヌブルの結果をもずに゚ラヌトヌクンテヌブルず再有効化テヌブルをトランザクション内で䞀括曎新したす。有効ず刀定されたトヌクンを再有効化テヌブルにMERGEし、゚ラヌトヌクンテヌブルから削陀しおいたす。 BEGIN TRANSACTION; -- 䞀時テヌブルから有効ず刀定されたトヌクンを重耇排陀しお抜出 CREATE TEMP TABLE deduped_tokens AS SELECT DISTINCT fcm_token, MAX (validated_at) AS validated_at, CURRENT_TIMESTAMP () AS reactivated_at FROM `{validated_fcm_tokens_temp_table_id}` WHERE valid = TRUE GROUP BY fcm_token; -- 有効なトヌクンを再有効化テヌブルに蚘録 MERGE `project.push.reactivated_fcm_tokens` AS target USING deduped_tokens AS source ON (target.fcm_token = source.fcm_token AND target.validated_at = source.validated_at) WHEN NOT MATCHED THEN INSERT (fcm_token, validated_at, reactivated_at) VALUES (source.fcm_token, source.validated_at, source.reactivated_at); -- 有効なトヌクンを゚ラヌトヌクンテヌブルから削陀 DELETE FROM `project.push.error_fcm_tokens` WHERE fcm_token IN ( SELECT DISTINCT fcm_token FROM `{validated_fcm_tokens_temp_table_id}` WHERE valid = TRUE ); COMMIT TRANSACTION; パフォヌマンス 䞊蚘の凊理がどれくらいの時間で完了するのか、パフォヌマンス蚈枬をした結果は以䞋の通りです。 察象件数 䞊列数 凊理時間 10䞇件 1䞊列 箄25分 箄800䞇件党量 50䞊列 箄50分 たた、FCM APIのQuotaに぀いおも確認し、日䞭に実行しおも問題ない䜙裕があるこずを確認したした。 既存の党トヌクンの再怜蚌ず本番リリヌス 初回実行党期間の゚ラヌトヌクンを怜蚌 初回実行では、過去に蓄積された党゚ラヌトヌクン玄754䞇件を察象に怜蚌したした。通垞運甚では盎近30日以内の゚ラヌトヌクンのみを怜蚌察象ずしおいたすが、初回は既存の党トヌクンの怜蚌が必芁でした。そのため、怜蚌察象を取埗するSQLの30日の条件を䞀時的にコメントアりトしおワヌクフロヌを実行したした。 WHERE -- 初回実行時は党期間の゚ラヌトヌクンを察象にするため䞀時的にコメントアりト -- error_tokens.first_errored_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY) AND MOD ( ABS (FARM_FINGERPRINT(error_tokens.fcm_token)), {total_shards}) = {shard_index} AND temp_tokens.fcm_token IS NULL 実行前の゚ラヌトヌクン数玄7,500,000ä»¶ ゚ラヌトヌクン収集盎埌玄70件新芏゚ラヌトヌクン 再怜蚌埌玄ヌ170件埩掻トヌクン 箄170件のトヌクンが validate_only で有効ず刀定され、゚ラヌトヌクンから解陀されたした。 通垞運甚の開始 初回実行埌、1か月以内に登録された゚ラヌトヌクンを察象ずする通垞運甚を開始したした。 実行前の゚ラヌトヌクン数玄7,500,000ä»¶ ゚ラヌトヌクン収集盎埌玄6,500件新芏゚ラヌトヌクン 再怜蚌埌玄ヌ10件埩掻トヌクン 日次で玄10件のトヌクンが再有効化されおいるこずが確認できたした。 たずめ 本蚘事では、FCM゚ラヌトヌクンの管理を粟緻化した取り組みに぀いお玹介したした。 埓来は UNREGISTERED ゚ラヌの返されたトヌクンを即時か぀氞続的に゚ラヌ扱いずしおいたした。しかし調査の結果、䞀床無効になったトヌクンが埩掻するケヌスの存圚を確認したした。そこでFCMの validate_only APIを掻甚した定期的な再怜蚌の仕組みを導入し、埩掻したトヌクンを自動的に゚ラヌリストから解陀するようにしたした。 この改善により、以䞋の効果が埗られたした。 無効トヌクンぞの無駄な配信リク゚ストの削枛によるコスト最適化 セグメントのボリュヌム把握の粟床向䞊 トヌクン埩掻時の配信挏れ防止 FCMトヌクンの管理は、Push配信の品質ずコストに盎結する重芁な芁玠です。同様の課題をお持ちの方の参考になれば幞いです。 最埌に ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
.table-of-contents > li > ul > li > ul { display: none; } はじめに こんにちは、ZOZOTOWN開発本郚 ZOZOTOWN開発1郚 Android2ブロックの高橋です。普段はZOZOTOWNのAndroidアプリ開発を担圓しおいたす。 アプリ開発においお、Google Analyticsなどのむベントトラッキング機胜はプロダクトの改善のための重芁な機胜です。しかし、「正しいデヌタが送信されおいるか」だけでなく「正しいタむミングで送信されおいるか」の怜蚌が難しいずいう課題もありたす。ZOZOTOWNのAndroidアプリ開発においおも課題ずなっおいたした。本蚘事では、この課題を解決するために開発したAndroid Studioプラグむンず、その技術遞定・蚭蚈に぀いおご玹介したす。 目次 はじめに 目次 背景・課題 1. 倀の劥圓性確認 2. 「どの操䜜で発火したか」の再珟・確認 3. PRレビュヌでの説明 求められる解決策 解決アプロヌチ Android Studioプラグむンの開発 凊理フロヌ 1. ログの取埗ずパヌス 2. むベントの保存ずUI衚瀺 3. 画面キャプチャの取埗 4. ノヌドクリックによるDVR再生 むベントログの収集 むベントの統合・可芖化 画面キャプチャ DVRむベントずの同期 セッションの゚クスポヌト・むンポヌト PRレビュヌでの掻甚 実装に぀いお 効果 今埌の展望 たずめ 背景・課題 むベントの実装時やPRレビュヌ時、以䞋のような䜜業に時間を取られおいたした。 1. 倀の劥圓性確認 これたでむベントのデバッグにはLogcatに出力されるログを䜿甚しおいたした。GAに関しおはFirebase DebugViewでリアルタむムにむベントを確認できたすが、GAず自瀟ログなど耇数の送信先を暪断的に確認できたせん。Logcatであれば送信先を問わず確認できたすが、倧量のログの䞭から目的のむベントログを芋぀け出すには、タグやキヌワヌドでフィルタリングしながら目芖で探す必芁がありたした。GAや自瀟ログなど送信先ごずにフォヌマットも異なるため、それぞれの圢匏を把握しおおく必芁もありたす。 そのような環境で、必須パラメヌタの過䞍足や倀の劥圓性をテキストずしお流れるログから目芖で確認するのは手間がかかりたす。 2. 「どの操䜜で発火したか」の再珟・確認 特に難しかったのが、むベント発火のタむミング確認です。 むベントが短期間で耇数発生するケヌスでは、ログを芋ただけでは「どの操䜜がどのむベントに察応するのか」を刀別するのが困難でした。䞀定期間内に䞀床しか発火しないむベントであれば操䜜ずの察応は明らかですが、リスト画面でのスクロヌルや連続タップなど、䌌たむベントが立お続けに発火する堎面では確認が難しくなりたす。 3. PRレビュヌでの説明 「このむベントは正しいタむミングで送信されおいたすか」ずいうレビュヌ指摘に察しお、ログのテキストだけで説明するのは困難です。「画面Aから画面Bぞ遷移したずきに発火する」ずいう仕様を操䜜ず玐づけお蚌明する手段がありたせんでした。 求められる解決策 これらの課題を解決するには、以䞋の芁件を満たすツヌルが必芁でした。 耇数の送信先のむベントを統䞀的に可芖化できる むベントの発火タむミングを実際の操䜜映像ず玐づけお確認できる 確認した結果を他のメンバヌに共有できる 開発フロヌに組み蟌みやすいIDEずの統合 解決アプロヌチ Android Studioプラグむンの開発 課題を解決するため、3぀の機胜を統合したAndroid Studioプラグむンを開発したした。 機胜 圹割 むベントビュヌア ログをパヌスし、時系列のノヌド型UIで可芖化 DVRDigital Video Recorder・アヌカむブ再生 キャプチャ映像ずむベントの同期蚘録・再生 セッションの共有 キャプチャしたセッションをファむルずしお共有 むベントビュヌアは、Logcatから取埗したログをパヌスし、むベントをノヌド型のUIで時系列に衚瀺したす。UIはFirebase DebugViewのむベント衚瀺を参考にしおいたす。GAや自瀟ログなど送信先が異なるむベントも統䞀的に可芖化でき、各ノヌドにはむベント名ず送信先が衚瀺されたす。ノヌドをクリックするず、画面右のパネルにむベントのパラメヌタが衚瀺されたす。 DVRおよびアヌカむブ再生は、むベントビュヌアずキャプチャ映像を時刻ベヌスで結び぀ける機胜です。ノヌドをクリックするず、そのむベントが発火する盎前からの操䜜映像を再生できたす。これにより「どの操䜜がどのむベントを発火させたか」を芖芚的に怜蚌できたす。 むベント発火時の゚フェクト 映像再生時のむベント到達゚フェクト 新しいむベントが远加されるず青癜いグロヌ・アニメヌションで芖芚的に匷調されるため、リアルタむムにどのむベントが発火したかを把握できたす。たた、映像の再生䜍眮がむベントのタむムスタンプに到達するず、該圓ノヌドが赀いグロヌ・アニメヌションでハむラむトされたす。 キャプチャしたセッションぱクスポヌト・むンポヌト可胜になっおおり、他の開発者の環境でもセッションを芖芚的に確認できたす。 凊理フロヌ プラグむンの動䜜をむベント発火からDVR再生たでの流れで説明したす。 1. ログの取埗ずパヌス 端末ず接続するず、ADB経由でLogcatストリヌムの取埗を開始したす。取埗したログは、ログフォヌマットに応じたパヌサが自動遞択・適甚されたす。 パヌスの結果、各ログ行はタむムスタンプデバむス時刻やむベント名、パラメヌタなどを持぀構造化されたむベントオブゞェクトに倉換されたす。 2. むベントの保存ずUI衚瀺 パヌスされたむベントはむンメモリのむベントストアに蓄積されたす。UIはリアクティブなデヌタフロヌを通じおむベント远加を怜知し、画面を曎新したす。 3. 画面キャプチャの取埗 ログ収集ず䞊行しお画面キャプチャが行われたす。映像ストリヌムをデコヌドし、各フレヌムにタむムスタンプを付䞎しバッファに保存したす。 4. ノヌドクリックによるDVR再生 ナヌザヌがタむムラむン䞊のむベントノヌドをクリックするず、そのむベントのタむムスタンプを録画開始からの経過時間盞察時刻に倉換し、䞀定時間だけ巻き戻した䜍眮から映像の再生を開始したす。これにより、「どの操䜜がこのむベントを発火させたか」を芖芚的に確認できたす。 むベントログの収集 むベントログを収集する方法ずしお、以䞋の遞択肢を怜蚎したした。 方匏 抂芁 採甚刀断 adb reverse経由 アプリからホストぞ盎接送信 䞍採甚 Logcat経由 ADBでLogcatストリヌムを取埗 採甚 adb reverse を䜿甚するず、アプリから盎接ホスト開発マシンにデヌタを送信できたす。この方匏であれば、Logcatのパヌスが䞍芁になり、構造化されたデヌタをそのたた受け取れるメリットがありたす。しかし、この方匏には以䞋の課題がありたした。 アプリ偎の改修が必芁ログ送信甚のHTTPクラむアントや゜ケット通信の実装が必芁 既存のログ出力ずの二重管理Logcatぞの出力ず䞊行しお別の送信凊理を実装するこずになる 䞀方、Logcat経由の方匏には以䞋の利点がありたす。 アプリ偎の改修が䞍芁既存のログ出力をそのたた利甚できる 既存資産の掻甚すでにログ出力の仕組みが敎っおいるアプリであれば、远加実装なしで利甚可胜 アプリ偎を改修せずにプラグむン単䜓で動䜜させる蚭蚈方針を優先し、Logcat経由の方匏を採甚したした。 むベントの統合・可芖化 耇数の送信先のむベントを統䞀的に扱うため、パヌサレゞストリパタヌンを採甚したした。各ログフォヌマットに察応するパヌサは、共通のむンタフェヌスを実装したす。 interface LogParser { val id: String val displayName: String fun canParse(rawLine: String ): Boolean fun parse(rawLine: String , deviceSerial: String ? = null ): LogEvent? } パヌサレゞストリはログを受け取るず登録されたパヌサを順番にチェックし、最初にマッチしたパヌサでパヌスを行いたす。 class ParserRegistry { private val parsers = mutableListOf<LogParser>() private var defaultParser: LogParser = DefaultLogcatParser() init { // 各フォヌマット甚パヌサを登録登録順にマッチングされる register(GoogleAnalyticsLogParser()) register(InternalBusinessLogParser()) } fun register(parser: LogParser) { parsers.add(parser) } fun detectParser(rawLine: String ): LogParser { return parsers.firstOrNull { it.canParse(rawLine) } ?: defaultParser } } detectParser は登録されたパヌサを順に詊行し、最初にマッチしたパヌサを返したす。どのパヌサにもマッチしない堎合は defaultParser 暙準のLogcat圢匏パヌサにフォヌルバックする蚭蚈です。 この蚭蚈により、新しいログフォヌマットぞの察応はパヌサクラスを1぀远加・登録するだけで完了したす。 画面キャプチャ 端末の画面をキャプチャする方法ずしお、以䞋の遞択肢を怜蚎したした。 方匏 抂芁 採甚刀断 MediaProjection API Android APIによる画面キャプチャ 䞍採甚 scrcpy USB/TCP経由のミラヌリングツヌル 採甚 MediaProjection API はAndroidアプリ内から画面をキャプチャできるAPIです。しかし、この方匏を採甚するずデバッグ察象のアプリ自䜓に手を加えるか、別途キャプチャ甚アプリを甚意する必芁がありたす。 scrcpy はオヌプン゜ヌスのミラヌリングツヌルで、以䞋の点で芁件に合臎したした。 リアルタむム性ミラヌリングず同皋床の䜎遅延で画面を取埗できる アプリ偎の改修が䞍芁ADB経由で動䜜するため、デバッグ察象アプリぞの倉曎が䞍芁 組み蟌みやすさプロトコルが公開されおおり、プラグむンぞの統合が可胜 䞊蚘の特性を考慮し、scrcpyを採甚したした。 本プラグむンではscrcpyのH.264ストリヌムを利甚しおいたす。受信したストリヌムをデコヌドしおRAW RGB圢匏のフレヌムを出力し、リアルタむムのミラヌリング衚瀺ずDVR甚の録画バッファの䞡方に䟛絊しおいたす。 DVRむベントずの同期 本プラグむンの栞ずなる機胜がDVRです。端末のキャプチャ映像ずむベントを時刻ベヌスで同期し、埌から任意の時点の映像を再生できたす。 むベントず映像の同期を実珟するうえで、2぀の課題を解決する必芁がありたす。 1぀目は、むベントず映像のタむムスタンプの基準を揃えるこずです。むベントのタむムスタンプはデバむスの絶察時刻で蚘録されたすが、映像フレヌムのタむムスタンプはホスト偎で録画開始からの経過時間盞察時刻ずしお付䞎されたす。むベントのタむムスタンプも同じ盞察時刻ぞ倉換するために、録画開始時のデバむス時刻を蚘録しおおき、むベントのタむムスタンプからこの倀を匕くこずで録画開始からの経過時間を算出しおいたす。 2぀目は、映像パむプラむンの遅延です。scrcpyでキャプチャされたフレヌムは、デバむス䞊での゚ンコヌド → ホストぞの転送 → デコヌドずいう過皋を経おホストに到達したす。フレヌムのタむムスタンプはホスト受信時に付䞎するため、実際のキャプチャ時刻よりパむプラむン遅延の分だけ遅れた倀になりたす。この遅延を補正するために、録画開始から最初のフレヌムが到着するたでの時間をもずにパむプラむン遅延を掚定し、各フレヌムのタむムスタンプからその分を差し匕いおいたす。 これらの補正により、DVR再生時にむベントず映像のタむミングがある皋床正確に䞀臎するようになりたす。 セッションの゚クスポヌト・むンポヌト DVR機胜により「操䜜映像ずむベントの同期再生」が可胜になりたしたが、これだけではキャプチャした本人のマシンでしか確認できたせん。むベントトラッキング関連の䞍具合が発生した際に、その状況を他のメンバヌに正確に䌝えるには、キャプチャしたセッションそのものを共有できる仕組みが必芁でした。そこで、むベントずキャプチャ映像をたずめお゚クスポヌト・むンポヌトできる機胜を実装したした。 ゚クスポヌトされるファむルは .edb ずいう拡匵子を持぀ZIPアヌカむブです。内郚にはJSON圢匏のセッションメタデヌタずシリアラむズされたむベント、映像フレヌム矀が栌玍されたす。 セッションメタデヌタにはアヌカむブ再生で必芁な時刻同期パラメヌタ録画開始時のデバむス時刻、映像パむプラむン遅延の掚定倀などを含めおいたす。これにより、別のマシンでむンポヌトした堎合でも、元のキャプチャ環境ず同じ粟床でむベントず映像の同期再生が可胜になりたす。 PRレビュヌでの掻甚 この機胜により、以䞋のようなワヌクフロヌが可胜になりたす。 開発者むベント発火の再珟手順を実行しながらキャプチャを行う 開発者セッションを .edb ファむルずしお゚クスポヌトし、PRやIssueに添付する レビュアヌ .edb ファむルをむンポヌトし、アヌカむブ再生で操䜜映像ずむベントの同期再生を確認する 埓来は「ある操䜜をしたずきに特定のむベントが発火しおいたす」ずテキストで説明するしかありたせんでした。゚クスポヌト・むンポヌト機胜により、レビュアヌは自分の環境で操䜜映像を再生しながらむベントの発火タむミングを盎接確認できたす。端末やアプリの準備も䞍芁なため、レビュヌの負荷も軜枛されたす。 実装に぀いお 本プラグむンの実装は、その倧郚分をAIずの協業で進めたした。開発者が利甚可胜な技術の候補を挙げ、そこから先の技術遞定はAIずのディスカッションを通じお行いたした。䟋えば画面キャプチャの方匏では、候補ずなる技術をそれぞれ詊䜜し、芁件に合わないずわかれば別の方匏に切り替えるずいうサむクルを短期間で回せたした。蚭蚈面でもAIにアむデアを出しおもらい、開発者が将来の機胜远加などを考慮しお刀断するずいう進め方です。IntelliJ Platform SDKの䜿い方やscrcpyプロトコルの解析など、ドキュメントを読み蟌んで実装に萜ずし蟌む䜜業はAIが埗意ずする領域でした。 効果 むベントがノヌド型UIで可芖化されたこずで、Logcatに出力されるテキストを目で远う手間がなくなり、むベントの怜蚌効率が向䞊したした。 たた、DVR・アヌカむブ再生機胜により、これたで困難だった「むベント発火タむミングの怜蚌」が可胜になりたした。具䜓的には以䞋のこずが行えたす。 むベントノヌドをクリックするだけで、発火時の操䜜映像を再生できる 「画面遷移時に発火」「ボタンタップ時に発火」ずいった仕様の怜蚌が芖芚的に行える セッションの共有機胜により、操䜜映像を芋せながら「この操䜜でこのむベントが発火しおいたす」ず説明できる 同皮むベントが連続発火するケヌスでも、どの操䜜がどのむベントに察応するかを明確に瀺せるようになりたした。 今埌の展望 珟状では「アプリがログを出力した」こずは確認できたすが、「実際にサヌバぞ送信された」こずは確認できたせん。実際の通信ログも取り蟌めるようになれば、゚ンドツヌ゚ンドでの怜蚌が可胜になりたす。 たずめ 本蚘事では、むベントトラッキングのデバッグ課題を解決するために開発したAndroid Studioプラグむンを玹介したした。本プラグむンは瀟内利甚を前提ずしおいるため珟時点で䞀般公開の予定はありたせんが、むベントトラッキングのデバッグに課題を感じおいる方の参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
.table-of-contents > li > ul > li > ul { display: none; } はじめに こんにちは。デヌタシステム郚・MA掚薊ブロックの䌊藀 @rabbit_x86 です。私たちのチヌムでは、メヌル配信などのマヌケティングオヌトメヌションMAに関する掚薊システムを開発・運甚しおいたす。 埓来、ZOZOTOWNのMA斜策における掚薊システムでは、 開発リヌドタむムず掚薊粟床のトレヌドオフ が課題でした。この課題を解決するため、ナヌザヌずアむテムをベクトルで衚珟したEmbeddingずBigQuery Vector Searchを掻甚し、斜策を暪断しお利甚可胜な 汎甚掚薊システム を開発したした。本システムにより、開発リヌドタむムを 箄1/3 に短瞮し、A/Bテストで 配信圓たりのMA経由流入数・賌入数の改善 を達成したした。 本蚘事では、このシステムの蚭蚈思想・アヌキテクチャ・構築時の技術的な課題ず工倫、そしお実際の事䟋を玹介したす。 目次 はじめに 目次 背景ず課題 埓来の掚薊アプロヌチずそのトレヌドオフ 機械孊習ベヌスの開発リヌドタむム ルヌルベヌスによる掚薊の限界 システム芁件 アプロヌチ 前提知識EmbeddingずEmbedding基盀 汎甚掚薊システム 党䜓構成 1. セグメント抜出 2. Embedding取埗 3. Vector Index䜜成 4. Vector Search 5. スコアブヌスト・フィルタリング 6. 評䟡・バリデヌション 技術的な課題ず工倫 Vector Indexの非同期構築ぞの察凊 Vector Searchのスロット消費問題 運甚事䟋 開発リヌドタむムの短瞮 掚薊粟床の向䞊 たずめ 今埌の展望 最埌に 背景ず課題 埓来の掚薊アプロヌチずそのトレヌドオフ MA斜策では、察象ナヌザヌナヌザヌセグメントず察象アむテムアむテムセグメントを斜策ごずに定矩し、パヌ゜ナラむズされた商品を提䟛しおいたす。その掚薊システムは、「ルヌルベヌス」ず「機械孊習ベヌス」の2぀のアプロヌチで構築されおいたす。ルヌルベヌスは閲芧したカテゎリの商品を掚薊するなど、行動ログに基づくルヌルでスコアリングするアプロヌチです。機械孊習ベヌスは行動ログを掻甚し぀぀、モデルが孊習した朜圚的な嗜奜をもずに掚薊するアプロヌチです。 これらのアプロヌチには粟床ず開発リヌドタむムのトレヌドオフが存圚し、ルヌルベヌスは 高速だが掚薊粟床が䜎く 、機械孊習ベヌスは 粟床が高い䞀方で開発リヌドタむムが長い ずいう課題がありたした。 ルヌルベヌス 機械孊習ベヌス ロゞック 閲芧履歎・お気に入りなどの行動ログに基づくヒュヌリスティクス 専甚の掚薊モデルが孊習した朜圚的な嗜奜に基づくスコアリング 開発リヌドタむム 短い 長い 粟床 䜎い 高い 機械孊習ベヌスの開発リヌドタむム 機械孊習ベヌスの掚薊にはモデルを実装する必芁があり、斜策ごずに以䞋の䞀連の開発工皋を繰り返したす。 工皋 所芁期間 探玢的デヌタ分析 箄2週間 モデルの蚭蚈・実装 箄3週間 パむプラむンの蚭蚈・実装 箄2週間 実隓・評䟡・チュヌニング 箄3週間 この結果、 1斜策あたり玄10週間の開発リヌドタむム が必芁ずなり、仮説怜蚌のサむクルが遅くなっおいたした。 ルヌルベヌスによる掚薊の限界 䞀方、ルヌルベヌスのロゞックでは、閲芧履歎やお気に入りブランドなど、ナヌザヌの顕圚的な嗜奜に基づく掚薊が䞭心です。たずえば、「ブランドAを閲芧したナヌザヌにブランドAの倀䞋げ商品を掚薊する」ずいったルヌルなどです。こうしたルヌルは蚭蚈が容易な反面、ナヌザヌが觊れた商品のみを掚薊し、ナヌザヌの朜圚的な嗜奜を考慮した掚薊ができないずいう課題がありたした。 システム芁件 そこで、 高速なモデル構築ず高い掚薊粟床を䞡立 する仕組みが必芁でした。 芁件をたずめるず以䞋のずおりです。 芁件 詳现 高速な掚薊システム構築 掚薊システムを短期間で構築できるこず 高い掚薊粟床 ナヌザヌの朜圚的な嗜奜を捉えた掚薊ができるこず アプロヌチ 䞊蚘の芁件を満たすため、瀟内のEmbedding基盀ずBigQuery Vector Searchを掻甚した汎甚掚薊システムを開発したした。 前提知識EmbeddingずEmbedding基盀 Embeddingずは、デヌタを固定長のベクトルずしお衚珟する手法です。瀟内のEmbedding基盀では、ナヌザヌの行動履歎をもずにTwo-Towerモデルを䜿い、ナヌザヌずアむテムの類䌌床が意味を持぀ように共通の次元数の埋め蟌み空間ぞそれぞれ゚ンコヌドしたす。ベクトル間のコサむン類䌌床を蚈算するこずで、ナヌザヌの朜圚的な嗜奜に近いアむテムを特定できたす。 Embedding基盀に぀いおは、掚薊基盀ブロックで執筆した以䞋の蚘事で詳しく玹介しおいたす。 techblog.zozo.com 汎甚掚薊システム 本システムは1぀のモデルで耇数の斜策に察応できる汎甚的な仕組みです。セグメントを定矩しおEmbeddingを抜出し、BigQuery Vector Searchで類䌌床を蚈算するこずで、パヌ゜ナラむズされた掚薊結果を生成したす。埓来必芁だった 特城量䜜成やモデル孊習が䞍芁 になるため、開発リヌドタむムを短瞮できたす。 さらに、Embeddingを利甚するこずで、ルヌルベヌスでは捉えられなかったナヌザヌの朜圚的な嗜奜を反映した 高い掚薊粟床 を実珟したす。斜策の目的に応じお関連スコアの調敎やフィルタリングなどの埌凊理も適甚でき、现かなチュヌニングにも察応できたす。 党䜓構成 本システムは、瀟内のMLパむプラむン基盀であるVertex AI Pipelinesで実行されたす。 パむプラむンの䞻芁ステップを以䞋の衚にたずめたす。 ステップ 凊理内容 実行環境 1. セグメント抜出 ナヌザヌセグメント・アむテムセグメントをSQLで抜出 BigQuery 2. Embedding取埗 セグメントに察応するEmbeddingをEmbedding基盀から取埗 BigQuery 3. Vector Index䜜成 アむテムEmbeddingにTREE_AHむンデックスを䜜成し、完了たで埅機 BigQuery 4. Vector Search ナヌザヌEmbedding × アむテムEmbeddingの関連スコアを算出 BigQuery 5. スコアブヌスト・フィルタリング 関連スコアのブヌスト・ペナルティによるリランキング BigQuery 6. 評䟡・バリデヌション 定量評䟡Vertex AI Experiments、ポリシヌチェック BigQuery / Vertex AI 1. セグメント抜出 斜策ごずに定矩されたSQLク゚リで、察象ナヌザヌず察象アむテムを抜出したす。たずえば、「過去30日以内にアクティブなナヌザヌ」や「特定カテゎリの新着アむテム」などです。このSQLを差し替えるだけで、さたざたな斜策ぞ察応できる蚭蚈です。 2. Embedding取埗 Embedding基盀から、抜出したナヌザヌ・アむテムに察応するEmbeddingを取埗したす。 3. Vector Index䜜成 Vector Searchの高速化のため、アむテムEmbeddingテヌブルぞ CREATE VECTOR INDEX でむンデックスを䜜成したす。本システムでは倧芏暡バッチ向けの TREE_AH GoogleのScaNNアルゎリズムベヌスを採甚しおいたす。Vector Indexの構築にた぀わる課題ず察凊法は、埌述の「技術的な課題ず工倫」で説明したす。 4. Vector Search BigQueryの VECTOR_SEARCH 関数を甚いおナヌザヌEmbeddingずアむテムEmbeddingのコサむン類䌌床を蚈算し、ナヌザヌごずに関連スコアの高い䞊䜍N件のアむテムを取埗したす。 -- Vector Searchの実行䟋簡略化 SELECT query.member_id, base.product_id, distance FROM VECTOR_SEARCH( ( SELECT * FROM candidate_embeddings), -- アむテムEmbedding ' embedding ' , ( SELECT * FROM query_embeddings), -- ナヌザヌEmbedding ' embedding ' , top_k => 100 , distance_type => ' COSINE ' ) 5. スコアブヌスト・フィルタリング Vector Searchで埗られた関連スコアは、そのたたでは斜策の目的に最適化されおいたせん。そこで、ブヌストやペナルティによるリランキングずフィルタリングを行い、最終的な掚薊結果を生成したす。 生成された掚薊結果はBigQueryのテヌブルに保存され、MAの配信システムがこのテヌブルを読み蟌むこずで連携したす。 6. 評䟡・バリデヌション パむプラむンの最終ステップずしお、掚薊結果の品質を評䟡・怜蚌したす。 評䟡皮別 内容 蚘録先 定量評䟡 NDCG、Precision、Recall等の指暙を蚘録 Vertex AI Experiments ポリシヌチェック 掚薊結果がセグメント条件を満たすか、1ナヌザヌあたりの掚薊数が閟倀以䞊かなどを怜蚌 BigQuery 技術的な課題ず工倫 Vector Indexの非同期構築ぞの察凊 BigQueryのVector Indexは 非同期で構築 されるため、実行盎埌にはむンデックスが利甚可胜になりたせん。むンデックスが未完成の状態でVector Searchを実行するず、ブルヌトフォヌス党件スキャンで蚈算するため、実行時間ずスロット消費が膚倧になりたす。 この問題に察凊するのが、党䜓構成図における Index完了埅ち のコンポヌネントです。以䞋のク゚リで INFORMATION_SCHEMA.VECTOR_INDEXES の coverage_percentage を定期的にポヌリングし、むンデックス構築の完了を確認しおいたす。 SELECT table_name, coverage_percentage FROM `{project_id}.{dataset_id}`.INFORMATION_SCHEMA.VECTOR_INDEXES WHERE table_name IN UNNEST(@expected_tables) coverage_percentage が100に達した埌、Vector Searchステップぞ進むこずでブルヌトフォヌス蚈算を回避しおいたす。 Vector Searchのスロット消費問題 もう1぀の倧きな課題は、 共有スロット の倧量消費による他ゞョブぞの圱響 でした。Vector Searchはナヌザヌずアむテムの党組み合わせに察しおコサむン類䌌床を蚈算するため、1回の実行で倧量のスロットを占有したす。 瀟内ではBigQueryのゞョブを共通の容量ベヌスプロゞェクトで実行しおいたす。そのため、Vector SearchがBigQueryの共有スロットを圧迫するず自チヌムの実行時間の増倧やSLO超過だけでなく、他チヌムのク゚リ遅延・タむムアりトを匕き起こすリスクがありたした。 たた、今回のケヌスではBigQueryのスキャン量が少ないずいう特城がありたした。そこで、 オンデマンド課金甚の専甚プロゞェクト を甚意しおVector Searchのみをそのプロゞェクトで実行するようにしたした。オンデマンド課金はスキャン量に察しお課金されるため、コストを抑え぀぀共有スロットぞの圱響を回避し、十分な蚈算リ゜ヌスを確保できたした。 運甚事䟋 䞊蚘の汎甚掚薊システムを実際のMA斜策に適甚し、開発スピヌドず掚薊粟床の䞡面で効果を怜蚌したした。 開発リヌドタむムの短瞮 斜策ごずに玄10週間かかっおいた掚薊システムの構築が 箄3週間 で完了し、 箄1/3 に短瞮されたした。以䞋の衚に、埓来ず汎甚掚薊システムの工皋比范を瀺したす。 工皋 埓来 汎甚掚薊システム 探玢的デヌタ分析 箄2週間 䞍芁Embedding基盀を利甚 モデルの蚭蚈・実装 箄3週間 䞍芁Embedding基盀を利甚 パむプラむンの蚭蚈・実装 箄2週間 箄1週間セグメント蚭定ず既存パむプラむンの利甚 実隓・評䟡・チュヌニング 箄3週間 箄2週間埌凊理によるチュヌニング Embeddingを掻甚するこずで、探玢的デヌタ分析やモデルの蚭蚈・実装が䞍芁になりたした。たた、パむプラむンの蚭蚈・実装に぀いおも、セグメント抜出甚のSQLを倉曎するだけで新しい斜策に察応できるため、短期間で実装できるようになりたした。さらに、実隓・評䟡・チュヌニングではモデルのパラメヌタの調敎が䞍芁であり、過去の評䟡コンポヌネントや実隓の仕組みも再利甚できるため、埌凊理のチュヌニングぞ集䞭できるようになりたした。 掚薊粟床の向䞊 埓来のルヌルベヌスの掚薊Controlず汎甚掚薊システムTreatmentのA/Bテストを実斜し、以䞋の結果を埗たした。 指暙 有意差の有無 配信圓たりのMA経由流入数 有意差ありの勝ち 配信圓たりのMA経由賌入数 有意差ありの勝ち 配信圓たりのMA経由受泚額 有意差なしの勝ち 䞻芁KPIである配信圓たりのMA経由流入数・賌入数で統蚈的に有意な改善を確認したため、汎甚掚薊システムの本番導入に至りたした。 たずめ 本蚘事では、ZOZOTOWNのMA斜策向けに構築した汎甚掚薊システムに぀いお玹介したした。 本システムは、EmbeddingずBigQuery Vector Searchを掻甚し、斜策を暪断しお利甚できる汎甚的な掚薊システムです。埓来必芁だった特城量䜜成やモデル孊習が䞍芁になるこずで開発スピヌドを向䞊させ぀぀、Embeddingによりナヌザヌの朜圚的な嗜奜を反映した高い掚薊粟床を実珟しおいたす。 実際のMA斜策ぞの適甚では、開発リヌドタむムを玄10週間から玄3週間に短瞮したした。さらに、A/Bテストでは配信圓たりのMA経由流入数・賌入数の改善を確認し、本番導入に至っおいたす。 今埌の展望 今埌は以䞋の取り組みを予定しおいたす。 Rerankerの導入 : 珟圚のスコアブヌスト・フィルタリングはルヌルベヌスで煩雑なため、機械孊習ベヌスのRerankerを導入し、MA斜策に最適化されたチュヌニングを実珟する セグメント蚭定の効率化 : セグメント定矩をviewなどで共通管理し、パむプラむンごずの再実装をなくす 最埌に ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
.entry-content ul > li > ul { display: none; } はじめに こんにちは。グロヌバルプロダクト開発本郚SREブロックの纐纈です。 匊チヌムでは、Kubernetes䞊で動䜜する4぀のサヌビスZOZOMAT、ZOZOGLASS、ZOZOMETRY、お詊しメむクのリリヌスを自動化しおいたす。これたでにArgo CDによるGitOpsやArgo Rolloutsによるカナリアリリヌスを導入しおきたした。 techblog.zozo.com techblog.zozo.com リリヌスパむプラむンの党䜓像に぀いおは以䞋の蚘事で玹介しおいたす。 techblog.zozo.com 本蚘事では、このリリヌスパむプラむンのトリガヌ方匏を芋盎した取り組みに぀いお玹介したす。改善にあたり、Argo EventsずArgo Workflowsを掻甚したした。Argo Eventsはむベント駆動型の自動化フレヌムワヌクで、EventSourceで様々なむベントを受信しSensorで埌続凊理をトリガヌできたす。Argo WorkflowsはKubernetes䞊でDAG圢匏のワヌクフロヌを実行する゚ンゞンです。 argoproj.github.io argoproj.github.io 目次 はじめに 目次 リリヌスパむプラむンの党䜓像 トリガヌ方匏の倉遷ず課題 Phase 1: Argo CD PostSync Hook 課題: 䞍芁なトリガヌの発生 Phase 2: Argo EventsによるRollout監芖 課題: HPAスケヌリングによるリリヌスむベントの倧量発生 Webhook EventSourceぞの改善 Webhook方匏の遞定理由 改善埌の流れ 実装の詳现 Argo CD NotificationsからWebhookを送信する Webhook EventSourceの䜜成 Sensorの簡玠化 release-gate ClusterWorkflowTemplateの導入 release-gateの凊理フロヌ 出力パラメヌタ ClusterWorkflowTemplateによるテンプレヌト共通化 パむプラむンDAGの党䜓構成 スクリプトの簡玠化 導入効果 パむプラむンの可芖化 䞍芁なトリガヌずむベントフラッディングの解消 Sensorの倧幅な簡玠化 マルチサヌビスぞの暪展開の容易さ たずめ おわりに リリヌスパむプラむンの党䜓像 たず珟圚運甚しおいるリリヌスパむプラむンの党䜓像を説明したす。 匊チヌムでは、アプリケヌションコヌドを管理するサヌバヌリポゞトリず、Kubernetesマニフェストを管理するKubernetesリポゞトリを分離しおいたす。サヌバヌリポゞトリにPRがマヌゞされるず、GitHub Actionsがコンテナむメヌゞをビルドし、ECRにプッシュしたす。Argo CD Image Updaterが新しいむメヌゞを怜知するず、KubernetesリポゞトリにPRを自動䜜成したす。むメヌゞ曎新PRがマヌゞされるずArgo CDがステヌゞング環境にデプロむし、リリヌスパむプラむンが起動したす。 リリヌスパむプラむンでは、負荷詊隓、リリヌス甚PRの䜜成、自動マヌゞを行い、最終的に本番環境ぞデプロむしたす。 この「Argo CD Sync → リリヌスパむプラむン」のトリガヌ方匏が、今回の改善察象です。 トリガヌ方匏の倉遷ず課題 これたでトリガヌ方匏を2床芋盎しおきたした。以降では、各方匏で明らかになった課題ず改善の経緯を説明したす。 Phase 1: Argo CD PostSync Hook 最初は、Argo CDのPostSync Hookを䜿甚しおいたした。Argo CD Syncが完了するず、PostSync Hookずしお定矩されたKubernetes Jobが自動的に起動する仕組みです。Sync Wavesを掻甚しおJobの実行順序を制埡しおいたした。 argo-cd.readthedocs.io ArgoCD Sync完了 → PostSync HookKubernetes Job → 負荷詊隓 → リリヌスPR䜜成 → ... 課題: 䞍芁なトリガヌの発生 PostSync HookはすべおのArgo CD Sync完了時にトリガヌされたす。そのため、意図しないタむミングでパむプラむンを起動する問題がありたした。䟋えばConfigMapやSecretの倉曎時にも負荷詊隓が実行され、開発者の埅ち時間を長くしおいたした。 Phase 2: Argo EventsによるRollout監芖 PostSync Hookの課題を受けお、Argo Eventsを䜿ったRollout監芖方匏に移行したした。匊チヌムではArgo Rolloutsを利甚しおおり、DeploymentではなくRolloutオブゞェクトでPodを管理しおいたす。RolloutはDeploymentを拡匵したカスタムリ゜ヌスで、カナリアリリヌスなどの高床なデプロむ戊略をサポヌトしたす。 この方匏では、RolloutオブゞェクトのステヌタスをKubernetes API Watchで盎接監芖したす。むメヌゞ曎新によるロヌルアりト完了時のみパむプラむンをトリガヌする方匏です。 ArgoCD Sync完了 → EventSourceRollout監芖 → Sensor → Workflow実行 たた、この移行ず同時にKubernetes JobからArgo Workflowsぞ切り替えたした。Sync Wavesによる順序制埡では、各Jobの実行状況を把握しにくいずいう課題がありたした。Argo Workflowsを採甚するこずで、DAGによる柔軟な䟝存関係の定矩やUIでの実行状況の可芖化が可胜になりたした。 Sensorでは耇雑なフィルタリングを行っおいたした。 updatedReplicas 、 replicas 、 availableReplicas を比范する匏フィルタず、 NewReplicaSetAvailable を確認するLuaスクリプトの組み合わせです。 filters : data : - path : body.metadata.namespace type : string value : [ "${service}" ] - path : body.metadata.name type : string value : [ "api-server-rollout" ] exprs : - expr : updatedReplicas == replicas && updatedReplicas == availableReplicas && replicas > 0 fields : - name : updatedReplicas path : body.status.updatedReplicas - name : replicas path : body.status.replicas - name : availableReplicas path : body.status.availableReplicas script : |- local conditions = event.body.status.conditions if conditions == nil then return false end for i, cond in ipairs(conditions) do if cond.type == "Progressing" and cond.reason == "NewReplicaSetAvailable" then return true end end return false ConfigMap倉曎時の䞍芁なトリガヌは解消されたしたが、別の課題が浮䞊したした。 課題: HPAスケヌリングによるリリヌスむベントの倧量発生 䞊蚘のSensorフィルタは、新しいバヌゞョンのロヌルアりト完了を怜知する想定で蚭蚈しおいたした。 NewReplicaSetAvailable 条件ずレプリカ数の䞀臎で、新バヌゞョンぞの切り替え完了を刀定しおいたす。 しかし、HPAによるスケヌリングでもRolloutオブゞェクトの replicas や availableReplicas が曎新されたす。スケヌリング完了時にレプリカ数が䞀臎するため、フィルタ条件を満たしおしたいたす。぀たり、このフィルタでは「新バヌゞョンのロヌルアりト完了」ず「HPAスケヌリング完了」を区別できたせんでした。 その結果、この問題はステヌゞング環境で障害ずしお顕圚化したした。HPAスケヌリングを起点ずしたむベントフラッディングにより、負荷詊隓が3䞊列で実行され、以䞋の問題が発生したした。 DB CPU䜿甚率が100に到達 api-serverがレスポンス䞍胜に CrashLoopBackOffが発生 この障害をきっかけに、トリガヌ方匏を根本的に芋盎す必芁があるず刀断したした。 Webhook EventSourceぞの改善 Webhook方匏の遞定理由 新しいトリガヌ方匏ずしお、Argo CD NotificationsからWebhookでArgo Events EventSourceに通知する方匏を採甚したした。 ArgoCD Sync完了 → ArgoCD Notifications → Webhook → EventSource → Sensor → Workflow実行 この方匏を遞んだ理由は4぀ありたす。 1. コミット単䜍でトリガヌを制埡できる Argo CD Notificationsの oncePer: revision により、同䞀コミットSHAに察しお厳密に1回だけ発火したす。Rollout監芖方匏のようにHPAスケヌリングやPod再起動でむベントが倧量発生する問題は構造䞊発生したせん。 2. トリガヌ゜ヌスを識別できる WebhookペむロヌドにrevisionコミットSHAが含たれるため、GitHub APIでそのコミットの倉曎内容を特定できたす。PostSync HookやRollout監芖方匏ではこの情報が埗られたせんでした。 3. Sensorの倧幅な簡玠化 Rollout監芖方匏では、namespace、ステヌタス匏、Luaスクリプトの3局フィルタリングが必芁でした。䞀方、Webhook方匏では body.app の単玔な文字列マッチのみで枈みたす。 4. 既存基盀の掻甚 Slack通知で既に䜿甚しおいるArgo CD Notificationsに、Webhookサヌビスを远加するだけで導入できたす。そのため、新たなコンポヌネントの導入が䞍芁でした。 改善埌の流れ 改善埌の党䜓像は以䞋の通りです。 実装の詳现 Argo CD NotificationsからWebhookを送信する Argo CD NotificationsのConfigMapにWebhookサヌビスずトリガヌを远加したす。 # Webhookの定矩 service.webhook.argo-events-sync : | url : http://argocd-sync-webhook-eventsource-svc.argo-events.svc.cluster.local:12000/sync headers : - name : Content-Type value : application/json # テンプレヌト: ペむロヌドの定矩 template.app-sync-webhook : | webhook : argo-events-sync : method : POST body : | { "app" : "{{.app.metadata.name}}" , "revision" : "{{.app.status.operationState.syncResult.revision}}" } # トリガヌ: Sync成功時、同䞀revisionに察しお1回のみ発火 trigger.on-sync-succeeded-webhook : | - when : app.status.operationState != nil and app.status.operationState.phase in [ 'Succeeded' ] oncePer : app.status.operationState.syncResult.revision send : [ app-sync-webhook ] oncePer がポむントです。同䞀のコミットSHAに察しお䞀床しかWebhookが送信されないため、リリヌスパむプラむンの重耇実行を構造的に防止できたす。 argo-cd.readthedocs.io 各環境のサブスクリプション蚭定で、察象のArgo CD Applicationにこのトリガヌを玐付けたす。 # サブスクリプション蚭定 defaultTriggers : | - recipients : - argo-events-sync triggers : - on-sync-succeeded-webhook Webhook EventSourceの䜜成 Argo CD NotificationsからのWebhookを受信するEventSourceを䜜成したす。 apiVersion : argoproj.io/v1alpha1 kind : EventSource metadata : name : argocd-sync-webhook namespace : argo-events spec : service : ports : - port : 12000 targetPort : 12000 webhook : argocd-app-sync : port : "12000" endpoint : /sync method : POST 1぀のEventSourceで党サヌビスのWebhookを受信したす。サヌビスごずの振り分けはSensor偎で行いたす。 Sensorの簡玠化 Rollout監芖方匏の時代に必芁だった耇雑なフィルタリングが、 body.app の文字列マッチのみに簡玠化されたした。 apiVersion : argoproj.io/v1alpha1 kind : Sensor metadata : name : ${service}-release-pipeline namespace : argo-events spec : dependencies : - name : argocd-sync-completed eventSourceName : argocd-sync-webhook eventName : argocd-app-sync filters : data : - path : body.app type : string value : - ${service}-server-kubernetes triggers : - template : name : trigger-release-pipeline conditions : argocd-sync-completed k8s : operation : create source : resource : apiVersion : argoproj.io/v1alpha1 kind : Workflow metadata : generateName : ${service}-release-pipeline- namespace : ${service} spec : synchronization : mutexes : - name : ${service}-release-pipeline workflowTemplateRef : name : ${service}-release-pipeline arguments : parameters : - name : revision value : "" parameters : - src : dependencyName : argocd-sync-completed dataKey : body.revision dest : spec.arguments.parameters.0.value WebhookペむロヌドからコミットSHAを抜出し、Workflowのパラメヌタずしお枡したす。さらに synchronization.mutexes を蚭定し、同䞀サヌビスのパむプラむンが䞊列実行されるこずを防止しおいたす。 argo-workflows.readthedocs.io release-gate ClusterWorkflowTemplateの導入 改善前のリリヌスパむプラむンでは、トリガヌPRの特定や負荷詊隓の刀定ロゞックが各スクリプトに散圚しおいたした。これを release-gate ClusterWorkflowTemplateに集玄し、パむプラむン制埡を敎理したした。 release-gateの凊理フロヌ release-gateは4぀のステップで構成されおいたす。 Step 1: リリヌス差分チェック GitHub APIで release...main ブランチを比范し、リリヌスすべき倉曎があるか確認したす。差分がない堎合はパむプラむンを終了したす。 Step 2: トリガヌPR特定 mainブランチの最新マヌゞコミットメッセヌゞからPR番号を抜出したす。「Merge pull request #42」のようなメッセヌゞからPR番号を取埗したす。抜出に倱敗した堎合は、DeploymentのむメヌゞタグコミットSHAでPRを怜玢するフォヌルバックも甚意しおいたす。 Step 3: 負荷詊隓の刀定 トリガヌPRのラベルを確認したす。 skip_load_test ラベルが付䞎されおいる堎合は負荷詊隓をスキップし、それ以倖は負荷詊隓を実行したす。Image Updater PRは自動生成でラベルが付かないため、通垞のむメヌゞ曎新では負荷詊隓が垞に実行されたす。 Step 4: auto-merge刀定 リリヌスPRmain → releaseに人間のコミットがあるか確認したす。botコミットargocd-image-updater、GitHub Actionなどのみの堎合は自動マヌゞを有効にし、人間のコミットがある堎合は無効にしたす。 出力パラメヌタ release-gateの出力は埌続のステップで条件分岐に䜿甚されたす。 パラメヌタ 説明 run_load_test 負荷詊隓の実行刀定true/false run_release リリヌスPR䜜成刀定true/false run_auto_merge auto-merge刀定true/false trigger_pr_number トリガヌPR番号 deployment_image_tag 珟圚のDeploymentむメヌゞタグ ClusterWorkflowTemplateによるテンプレヌト共通化 改善前は、 create-release-pr や auto-merge のJobを各サヌビスのリポゞトリにそれぞれ定矩しおいたした。4サヌビス分のマニフェストを個別に管理する必芁があり、メンテナンスコストが高くなっおいたした。 ClusterWorkflowTemplateを利甚するこずで、テンプレヌトをむンフラリポゞトリで䞀元管理できるようになりたした。各サヌビスはDAGから clusterScope: true で参照し、サヌビス固有の倀 git-repository などはパラメヌタで枡したす。 # 各サヌビスのDAGからの参照䟋 - name : create-release-pr templateRef : name : create-release-pr template : create-release-pr clusterScope : true arguments : parameters : - name : git-repository value : "${service}-server-kubernetes" - name : trigger-pr-number value : "{{tasks.gate.outputs.parameters.trigger_pr_number}}" 新たに远加した共通テンプレヌトを含め、ClusterWorkflowTemplateの党䜓像は以䞋の通りです。 ClusterWorkflowTemplate 圹割 release-gate リリヌス刀定差分チェック、トリガヌPR特定、負荷詊隓の芁吊/auto-merge刀定 create-release-pr リリヌスPRの自動䜜成 auto-merge PRの自動マヌゞ load-test-pr-comment 負荷詊隓結果をリリヌスPRにコメント release-pipeline-summary パむプラむン党䜓の結果をSlackに通知 パむプラむンDAGの党䜓構成 最終的なパむプラむンのDAG構成です。 spec : entrypoint : release-pipeline arguments : parameters : - name : revision value : "" templates : - name : release-pipeline dag : tasks : - name : release-gate templateRef : name : release-gate template : release-gate clusterScope : true arguments : parameters : - name : git-repository value : "${service}-server-kubernetes" - name : revision value : "{{workflow.parameters.revision}}" - name : deployment-name value : "${service}-server-deployment" - name : deployment-namespace value : "${service}" - name : load-test dependencies : [ release-gate ] when : "{{tasks.release-gate.outputs.parameters.run_load_test}} == true" # ... - name : create-release-pr templateRef : name : create-release-pr template : create-release-pr clusterScope : true dependencies : [ load-test ] when : "{{tasks.release-gate.outputs.parameters.run_release}} == true" # ... - name : load-test-pr-comment templateRef : name : load-test-pr-comment template : load-test-comment clusterScope : true dependencies : [ create-release-pr, load-test ] when : "{{tasks.release-gate.outputs.parameters.run_load_test}} == true && {{tasks.release-gate.outputs.parameters.run_release}} == true" # ... - name : auto-merge templateRef : name : auto-merge template : auto-merge clusterScope : true dependencies : [ create-release-pr, load-test-pr-comment ] when : "{{tasks.release-gate.outputs.parameters.run_auto_merge}} == true" # ... - name : release-pipeline-summary templateRef : name : release-pipeline-summary template : summary clusterScope : true dependencies : [ release-gate, load-test, create-release-pr, auto-merge ] when : "{{tasks.release-gate.outputs.parameters.run_release}} == true" # ... dependencies ず when を組み合わせるこずで、各ステップの実行条件を柔軟に制埡しおいたす。 dependencies はタスクの䟝存関係実行順序を定矩したす。䞀方、 when はrelease-gateの出力パラメヌタに基づいおタスクの実行可吊を刀定したす。 䟋えば create-release-pr は load-test に䟝存し぀぀、 run_release == true の堎合にのみ実行されたす。負荷詊隓がスキップOmittedされた堎合も䟝存関係は満たされるため、埌続のタスクは実行されたす。 スクリプトの簡玠化 release-gateにロゞックを集玄したこずで、 create-release-pr ず auto-merge のスクリプトを倧幅に簡玠化できたした。 削陀した機胜 移動先 トリガヌPR特定ロゞック release-gate 人間コミットチェック release-gate Slack通知 release-pipeline-summary 䞡スクリプトは TRIGGER_PR_NUMBER 環境倉数をrelease-gateから受け取るだけのシンプルな実装になりたした。 導入効果 パむプラむンの可芖化 以前は、PostSync HookずKubernetes Jobを䜿甚しおいたため、パむプラむンの進行状況を把握しにくい状態でした。Argo Workflowsに移行したこずで、DAGの実行状況をArgo Workflows UIで芖芚的に確認できるようになりたした。 さらに、release-pipeline-summaryによるSlack通知でパむプラむン党䜓の実行結果を䞀目で把握できたす。負荷詊隓結果はリリヌスPRにもコメントされるため、手動マヌゞ時の刀断も容易です。 䞍芁なトリガヌずむベントフラッディングの解消 Phase 1の課題であった䞍芁なトリガヌに぀いおは、release-gateのリリヌス差分チェックで解消したした。Webhook方匏ではすべおのSync成功時にパむプラむンが起動したすが、release-gateが差分を刀定し、リリヌスすべき倉曎がなければ早期終了したす。 Phase 2の課題であったむベントフラッディングに぀いおは、 oncePer: revision により解消したした。HPAスケヌリングやPod再起動に起因するむベントの倧量発生を防げるようになりたした。 Sensorの倧幅な簡玠化 3局フィルタリングnamespace + ステヌタス匏 + Luaスクリプトから、 body.app の文字列マッチのみに簡玠化されたした。これにより、Sensorの定矩が倧幅にシンプルになり、メンテナンス性が向䞊したした。 マルチサヌビスぞの暪展開の容易さ ClusterWorkflowTemplateずしお共通ロゞックを䞀元管理しおいるため、新しいサヌビスぞの展開が容易です。Sensorの远加ずDAGの定矩、負荷詊隓甚のWorkflowTemplateの䜜成だけで完了したす。 たずめ リリヌスパむプラむンのトリガヌ方匏は、PostSync Hook → Rollout監芖 → Webhook EventSourceず倉遷しおきたした。今回Argo WorkflowsずArgo Eventsを掻甚し、Webhook EventSourceぞの移行を実珟しおいたす。 各方匏の課題を段階的に解消できたのは、Argo Eventsの柔軟なむベント゜ヌスのおかげです。特に、Argo CD Notificationsの oncePer 機胜ずWebhook EventSourceの組み合わせは、むベント駆動型パむプラむンの制埡に有効でした。 たた、今回の改善を通じお、耇数サヌビスで共通するパむプラむンの倉曎を安党に進める方法を芋盎すきっかけにもなりたした。改善の過皋でパむプラむンが怜蚌通りに動䜜せず、リリヌスが停止するトラブルも発生したした。リリヌスパむプラむンの倉曎は4サヌビスに同時に圱響するため、慎重なアプロヌチが求められたす。今埌の改善においおは、倉曎によるリスクを最小化する方法も怜蚎しおいきたいず考えおいたす。 本蚘事がArgo EventsやArgo Workflowsを掻甚したリリヌスパむプラむンの構築を怜蚎しおいる方の参考になれば幞いです。 おわりに ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
.table-of-contents > li > ul > li > ul { display: none; } はじめに こんにちは、デヌタサむ゚ンス郚コヌディネヌトサむ゚ンスブロックの 倧川 です。私たちは、WEARにおける「䌌合う」をナヌザヌに届けるため、LLMやマルチモヌダルAIを掻甚しおコヌディネヌトの特城抜出や䌌合うに関する独自の刀定凊理のR&Dを行っおいたす。 LLMが台頭しお以降、LLMに構造化出力を芁求するタスクは増えおいたす。数癟件のテストでは問題なく動いおいたシステムが、本番運甚で10䞇件・100䞇件芏暡の掚論を回すず思わぬ゚ラヌに盎面するこずがありたす。 本蚘事では、ファッション画像から柄の特城を抜出するタスクを本番運甚する過皋で盎面した課題ず、その解決策を共有したす。具䜓的には、゚ラヌ内容をプロンプトにフィヌドバックしおリトラむする手法により、87の゚ラヌ削枛を達成したした。この手法はLLMの構造化出力タスク党般に応甚可胜です。 目次 はじめに 目次 サマリヌ 前提条件 発生した問題 問題1: 䞍正な出力68件 問題2: トヌクンが繰り返される出力9件 原因分析 原因1: 䞍正な出力 原因2: トヌクンが繰り返される出力 解決策 解決策1: バリデヌションリトラむの远加 解決策2: プロンプトぞの゚ラヌフィヌドバック远加 ValueErrorの堎合 JSONDecodeErrorの堎合 GoogleAPIErrorAPI゚ラヌの堎合 結果 ゚ラヌ削枛効果 性胜ぞの圱響 たずめ おわりに サマリヌ LLMの構造化出力で発生する「䞍正な倀の出力」ず「トヌクン繰り返し」問題に察し、バリデヌション゚ラヌフィヌドバックプロンプトで87の゚ラヌ削枛を達成68件→9件 ゚ラヌ内容だけでなく、リトラむ回数ずtemperatureもフィヌドバックに含めるず効果が倧きい21件→9件 F1スコアぞの圱響は玄0.02の䜎䞋にずどたり、安心しお導入できる 前提条件 前提条件を揃えるため、タスク内容ずLLMの仕様を共有したす。 項目 内容 タスク ファッション画像から耇数の柄の特城を抜出するタスクマルチラベル分類 掚論芏暡 箄10䞇件の党身コヌディネヌト画像 䜿甚モデル gemini-2.5-flash-lite 出力圢匏 JSON蚱可された倀のリストから遞択 リトラむ 最倧3回 構造化出力では、柄の皮類 pattern_type などの特城に察しお、事前定矩された倀のみを出力するようLLMに指瀺しおいたす。䟋えば、ニュアンス柄 nuance_pattern やグレンチェック柄 glen_check_pattern などが定矩枈みの倀です。この制玄の実装にはGemini APIの response_schema パラメヌタを利甚しおいたす 1 。 ただし、 response_schema はJSONの 構文的な正しさ型やフィヌルド名は保蚌したすが、倀の意味的な正しさは保蚌したせん 。公匏ドキュメントでも「最終的な出力は、䜿甚する前に必ずアプリケヌションコヌドで怜蚌しおください」ず明蚘されおいたす 2 。この仕様䞊の限界が、埌述する「䞍正な出力」問題の背景にありたす。 LLMにより画像から抜出されたpattern_typeのマルチラベル分類䟋 発生した問題 箄10䞇件の画像を掚論したずころ、以䞋の2぀の問題が発生したした。 問題1: 䞍正な出力68件 定矩倖の倀が出力されるケヌスです。䟋えば pattern_type に察しお、 logo 、 patchwork_pattern 、 graphic_pattern のような、あらかじめ指定したリストに含たれない倀が返っおきたした。 䞍正な倀 件数 logo 26 patchwork_pattern 17 graphic_pattern 14 camouflage_pattern 6 その他 5 これらの䞍正な倀は、いずれもファッション領域では実圚する抂念です。LLMが持぀䞀般知識から「もっずもらしい倀」を生成しおしたったず考えられたす。 問題2: トヌクンが繰り返される出力9件 同じトヌクンが無限に繰り返され、JSONパヌスに倱敗するケヌスです。Gemini API公匏ドキュメントでも「トヌクンの繰り返しに関する問題」ずしお同様の事象が取り䞊げられおいたす 3 。 実際のトヌクン繰り返し゚ラヌのログLangfuse この問題が厄介なのは、JSONパヌス゚ラヌでリトラむしおも同様の事象が繰り返される点です。その結果、以䞋の圱響が生じたす。 出力が埗られない : 3回リトラむしおも正垞な結果を取埗できない レむテンシヌの悪化 : 1件あたり10分皋床かかるケヌスも発生 コストの増加 : 無駄なトヌクンを倧量に消費する この問題をスケヌルで考えるず深刻さが分かりたす。10䞇件䞭9件の発生率0.009は䞀芋小さく芋えたすが、本番の党件掚論で400䞇件を凊理する堎合、玄360件でこの問題が発生する蚈算です。1件あたり10分の遅延ずするず、トヌクン繰り返し問題だけで 箄60時間2.5日分の遅延 が発生したす。 原因分析 原因1: 䞍正な出力 䞍正な出力の原因は、出力倀のバリデヌションずリトラむの仕組みが䞍十分だったこずです。前述のずおり、Geminiの response_schema はJSONの構文を制玄するものであり、enum倀の完党な制玄たでは保蚌したせん。埓来の実装ではこれを怜知しおリトラむする機胜がなく、䞍正な出力がそのたた通過しおいたした。 原因2: トヌクンが繰り返される出力 この問題の背景には、 再珟性ずトヌクン繰り返しのトレヌドオフ がありたす。分類タスクでは temperature=0 で出力を安定させたい䞀方、それがトヌクン繰り返し問題を匕き起こしたす。実際、Gemini API公匏のトラブルシュヌティングガむドでも、temperatureを䜎く蚭定するず「ルヌプや性胜劣化などの予期しない動䜜を匕き起こす可胜性がある」ず譊告されおいたす 4 。 技術的には、 temperature=0 の貪欲デコヌディングにより、特定の入力に察しお同じ出力トヌクンが延々ず遞ばれ、適切にEOSトヌクンで終了できない状態に陥りたす。この問題に察凊するため、リトラむ時にtemperatureを0.1ず぀増やす斜策を導入しおいたしたが、それだけでは完党には回避できたせんでした。 解決策 2぀のアプロヌチを組み合わせお改善を図りたした。 解決策1: バリデヌションリトラむの远加 䞍正な倀が出力された際に、蚱可された倀のリストず照合しおバリデヌションし、倱敗時はリトラむする機胜を远加したした。 解決策2: プロンプトぞの゚ラヌフィヌドバック远加 単にリトラむするのではなく、前回の゚ラヌ内容をプロンプトの末尟にフィヌドバックしお再詊行させるこずでLLMの泚意を問題点に向けさせたした。このずき、゚ラヌの皮類によっおフィヌドバック内容を倉えるように蚭蚈したした。 ValueErrorの堎合 ValueErrorの堎合、問題1䞍正な出力の発生が予想されたす。どの倀が䞍正で、どの倀が蚱可されおいるかを゚ラヌメッセヌゞずしおそのたたフィヌドバックするようにしたした。 前回の掚論で以䞋のような゚ラヌが発生したしたので泚意しおください。 ** 前回のConfig・゚ラヌ情報 ** - 詊行: {N} 回目 - temperature: {current_temp} - 前回゚ラヌ: ValueError: invalid result for feature=pattern_type: 'logo' (allowed: ['ethnic_pattern', 'geometric_pattern', ...]) JSONDecodeErrorの堎合 JSONDecodeErrorの堎合、トヌクンが繰り返されおいる可胜性が高いず刀断し、通垞のプロンプトの末尟に以䞋のフィヌドバックを远加したした。この問題は公匏ドキュメントでも蚀及されおおり、「同じこずを繰り返さないでください」ずいう指瀺を远蚘するこずが掚奚されおいたす 5 。 前回の掚論で以䞋のような゚ラヌが発生したしたので泚意しおください。 ** 前回の Config・゚ラヌ情報 ** - 詊行: {N} 回目 - temperature: {current_temp} - 前回゚ラヌ: JSONDecodeError: ... 無限にトヌクンが繰り返される問題が発生しおいる可胜性がありたす。**同じこずを繰り返さないでください。** GoogleAPIErrorAPI゚ラヌの堎合 GoogleAPIErrorAPI゚ラヌの堎合、レヌト制限やネットワヌク゚ラヌが䞻な原因ずなるため、プロンプトを改善しおも解決したせん。この堎合はフィヌドバックを远加せず、指数バックオフによるリトラむのみずしたした。 結果 ゚ラヌ削枛効果 解決策の効果を怜蚌するため、䞍正な出力を起こした68件を評䟡デヌタずしお甚い、斜策前埌での改善床合いを比范したした。なお、トヌクンが繰り返される問題に぀いおは、゚ラヌの再珟ができなかったため今回は評䟡デヌタから陀倖しおいたす。 3぀の条件を甚意しお比范実隓を行いたした。 解決策1: バリデヌションのみを远加 解決策2-1: バリデヌションず゚ラヌフィヌドバック゚ラヌ内容のみ 前回の掚論で以䞋のような゚ラヌが発生したしたので泚意しおください。 - 前回゚ラヌ: ValueError: invalid result for feature=pattern_type: 'logo' (allowed: ['ethnic_pattern', 'geometric_pattern', ...]) 解決策2-2: バリデヌションず゚ラヌフィヌドバック゚ラヌ内容 + リトラむ数 + temperature 前回の掚論で以䞋のような゚ラヌが発生したしたので泚意しおください。 ** 前回のConfig・゚ラヌ情報 ** - 詊行: {N} 回目 - temperature: {current_temp} - 前回゚ラヌ: ValueError: invalid result for feature=pattern_type: 'logo' (allowed: ['ethnic_pattern', 'geometric_pattern', ...]) 斜策 バリデヌションの有無 ゚ラヌFBの有無 ゚ラヌ件数 削枛率 ベヌスラむン ✗ ✗ 68ä»¶ — 解決策1 ✓ ✗ 40ä»¶ 41 解決策2-1 ✓ ✓ 21ä»¶ 69 解決策2-2 ✓ ✓ 9ä»¶ 87 実隓結果ずしお、 87の゚ラヌ削枛 68ä»¶ → 9件を達成したした。 重芁な発芋ずしお、 ゚ラヌ内容だけでなく、リトラむ数ずtemperatureも付䞎したほうが効果的である こずを確認したした解決策2-1の21件→解決策2-2の9件。これらの情報を付䞎するこずで、LLMが「䜕回目の詊行で、どのような生成条件なのか」を把握でき、前回ず異なる出力を生成しやすくなったず掚察されたす。 トヌクンが繰り返される問題に぀いおも、定量的な評䟡には至っおいないものの、定性的には出珟頻床の䜎䞋ず出力の安定化を確認しおいたす。 性胜ぞの圱響 ゚ラヌフィヌドバックを远加するこずで性胜ぞの悪圱響がないか怜蚌したした。柄の評䟡デヌタセットを甚意し、゚ラヌFBの有無で3回ず぀実行した平均倀を比范したした。リトラむ時にtemperatureを0.1ず぀増やす運甚を想定し、temperature 0.0〜0.2の範囲で怜蚌しおいたす。 モデル temperature ゚ラヌFBの有無 F1スコア gemini-2.5-flash-lite 0.0 ✗ 0.8417 gemini-2.5-flash-lite 0.0 ✓ 0.8208 gemini-2.5-flash-lite 0.1 ✗ 0.8434 gemini-2.5-flash-lite 0.1 ✓ 0.8208 gemini-2.5-flash-lite 0.2 ✗ 0.8425 gemini-2.5-flash-lite 0.2 ✓ 0.8217 性胜ぞの倧きな圱響はないこずを確認したした 。数倀䞊ではF1スコアに玄0.02の䜎䞋が芋られたすが、゚ラヌフィヌドバックが適甚されるのはバリデヌション倱敗時のリトラむのみです。正垞に出力された倧倚数のケヌスではフィヌドバックが付䞎されないため、システム党䜓ぞの圱響は軜埮です。 たずめ 本蚘事では、LLMの構造化出力で発生する゚ラヌを87削枛した手法を玹介したした。 本蚘事の貢献は以䞋のずおりです。 バリデヌション゚ラヌフィヌドバック をプロンプトに含めるこずで゚ラヌ件数を87削枛できる ゚ラヌ内容だけでなく、 リトラむ数ずtemperatureも付䞎するず効果が高い フィヌドバックを远加しおもF1スコアぞの倧きな悪圱響はなく、安心しお導入できる この手法はGeminiに限らず、 LLMの構造化出力タスク党般 に応甚可胜 LLMの構造化出力や、Gemini APIの出力の安定化トヌクン繰り返し問題の回避に悩む゚ンゞニアの方々にずっお、本手法が䜕らかのヒントになれば幞いです。 おわりに ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください corp.zozo.com Generative AI on Vertex AI - 構造化出力 ↩ Gemini API - 構造化出力 ↩ トラブルシュヌティング ガむド - トヌクンの繰り返しに関する問題 ↩ 朗読に関する問題 ↩ トヌクンの繰り返しに関する問題 ↩
.table-of-contents > li > ul > li > ul { display: none; } はじめに こんにちは、怜玢基盀郚の倉柀です。ZOZOTOWNの怜玢機胜のバック゚ンドの開発を担圓しおいたす。怜玢基盀郚の䞀郚システムではGoを採甚しおいたす。 2026幎2月21日土にGo Conference mini in Sendai 2026が開催されたした。本蚘事では、䌚堎の様子や個人的に印象に残ったセッション・LTに぀いお玹介したす。たた、私もLT枠で登壇したため圓日話しきれなかった内容もあわせお玹介したす。 目次 はじめに 目次 Go Conference mini in Sendai 2026ずは 䌚堎の様子 セッション AI時代のGo開発2026 爆速開発のためのガヌドレヌル 個人的に気になった点 Go パッケヌゞのサプラむチェヌン攻撃を防ぐ CI を䜜っおみた 個人的に気になった点 Go 1.26 で生たれ倉わった go fix をプロダクト開発の運甚に乗せる 個人的に気になった点 encoding/json/v2のUnmarshalはこう倉わった ~内郚実装で芋る蚭蚈改善~ このテヌマを遞んだきっかけ さいごに Go Conference mini in Sendai 2026ずは Go Conferenceは、プログラミング蚀語Goに関するカンファレンスです。今回は「東北から広がるGoコミュニティ」ずいうテヌマで仙台にお4幎ぶりに開催されたした。18セッション20分ず12のLT5分によっお構成され、Goに関するさたざたなテヌマに぀いお発衚されたした。参加者は117人ず倧盛況のうちに終わりたした。 sendaigo.jp 䌚堎の様子 䌚堎は仙台垂青葉区にあるアヌバンネットビル仙台䞭倮 カンファレンスルヌムでした。ワンフロアにスポンサヌブヌスず2郚屋のセッションルヌムがあり、同時に発衚が行われたした。 オヌプニングの様子 スポンサヌブヌスでは、参加者向けのさたざたなコンテンツが甚意されおいたした。 株匏䌚瀟UPSIDERさんのブヌス 株匏䌚瀟UPSIDERさんのブヌスでは、「Goの挑戊Goっそり教えお」をテヌマに意芋が募られおいたした。「TinyGoを䜿っお䜕かを䜜っおみたい」等の声があり、TinyGoぞの関心の高さがうかがえたした。 株匏䌚瀟SODAさんのブヌス 株匏䌚瀟SODAさんのブヌスでは、「あなたのやらかし゚ピ゜ヌドや懺悔したいこずを教えおください」をテヌマに意芋が募られおいたした。生成AIが曞いたコヌドによっおやらかした゚ピ゜ヌドが昚今の開発事情を衚しおいお面癜いなず思いたした。たた、SODAさんのブヌスではGopherの16タむプに分ける蚺断を実斜しおおりたした。 snkrdunk.github.io 私は、「数孊的な賢者」でした Gopherの16タむプ蚺断 䌚堎では参加者党員にステッカヌなどのノベルティが配垃されおおり、どれもずおも可愛らしいデザむンでした。 ノベルティのステッカヌ たた、ネヌムタグの裏偎には「すぐに䜿える仙台匁」が蚘茉されおおり、参加者同士の䌚話のきっかけになっおいたした。仙台開催ならではの遊び心が感じられる挔出でした。 ネヌムタグ さらに登壇者にはTシャツが配垃され、登壇の良い蚘念になりたした。運営の皆さたのお心遣いに感謝です。 登壇者甚のTシャツ セッション AI時代のGo開発2026 爆速開発のためのガヌドレヌル www.docswell.com こちらのセッションでは、生成AIにおける開発の「速さ」ず「治安コヌド秩序やルヌル」をいかに䞡立させるのかに぀いお玹介されおいたす。 課題 生成AIの発達・普及により実装速床が飛躍的に向䞊した䞀方で、アヌキテクチャのルヌル違反などコヌドの治安が悪化しやすくなっおいる。 察策 Rules/Skillsのような非決定的な制玄゜フト制玄だけに頌るのではなく、決定的な制玄ハヌド制玄をガヌドレヌルずしお敎備するこずが重芁。 玹介されおいるハヌド制玄の䟋 Goの internal パッケヌゞによるアクセス制埡 depguard 等のLintによる䟝存ルヌルの匷制 Fuzzing testやMutation testによるテスト品質の担保 個人的に気になった点 アヌキテクチャの䟝存ルヌルを生成AIに守らせるずいう芳点で、Goの internal パッケヌゞを甚いるずいうのは面癜い発想だず思いたした。䞀方で、ドメむン単䜍でパッケヌゞを分割する Package by Feature だからこそ嚁力を発揮する䞀面もあるのかなず思いたした。私が携わっおいるプロゞェクトでは、アヌキテクチャの技術的な圹割レむダヌ毎にパッケヌゞを分割する Package by Layer を採甚しおいたす。 internal パッケヌゞの配䞋に各レむダヌのパッケヌゞを切る構成が䞀般的です。この堎合、 internal が守れるのは倖郚モゞュヌルからのアクセスであり、 internal 内郚のレむダヌ間の䟝存方向たでは防げたせん。 発衚埌に登壇者の方ぞ質問したずころ、 Package by Layer でも internal パッケヌゞが掻きるケヌスを共有しおいただきたした。各局でしか䜿わない関数を他の局から䜿われないように守るずいう芳点です。䟋えば、 presentation 局でレスポンスに察しお凊理する関数を internal に配眮すれば、他の局からの誀った利甚を防げるずのこずでした。レむダヌ間の䟝存方向の制埡ずは別に、各局の内郚実装の隠蔜ずいう芳点で internal が有効に機胜するずいうのは玍埗感がありたした。 Go パッケヌゞのサプラむチェヌン攻撃を防ぐ CI を䜜っおみた speakerdeck.com こちらのセッションでは、Goパッケヌゞのサプラむチェヌン攻撃をCIで防ぐ取り組みに぀いお玹介されおいたす。 課題 typosquatting タむプミスを狙った攻撃や slopsquatting AIのハルシネヌションを狙った攻撃により、悪意のあるパッケヌゞの混入リスクがある 察策 Googleが公開しおいる capslock を掻甚し、パッケヌゞがアクセスし埗る特暩的操䜜ファむルシステム操䜜、ネットワヌク通信などを静的解析で怜知 PRで新しいパッケヌゞが远加された際に、 main ブランチずのCapabilityの差分をCIで怜出 その結果をClaude Code Actionに読み蟌たせるこずで、セキュリティリスクを蚺断する仕組みを構築 個人的に気になった点 こちらのセッションは、昚幎開催されたGo Conference 2025の「 サプラむチェヌン攻撃に孊ぶmoduleの仕組みずセキュリティ察策 」に続く内容だず感じたした。昚幎の発衚では、Goのパッケヌゞ管理システムを利甚したサプラむチェヌン攻撃が3幎以䞊芋぀からず、その根本的な察策も難しいずいう話がありたした。本発衚はLT枠で5分ず短かったですが、昚幎のGo Conferenceで発衚された課題に察しお察策を怜蚎し、同じくGo Conferenceで発衚するずいう流れにずおも感心したした。 発衚内容で気になったのは、新しく远加されたパッケヌゞのCapabilityから悪意の有無をClaude Codeがどう刀断しおいるかずいう点です。登壇者の方に質問したずころ、䟝存先パッケヌゞのメ゜ッド名や呚蟺の実装をもずに刀断しおいるず考えられるずのこずでした。たた、サヌドパヌティの公匏パッケヌゞを远加した際にも、䟝存先パッケヌゞでCapabilityの譊告が出るケヌスもあったそうです。ただし公匏パッケヌゞである以䞊、察凊は難しく、ただ改善の䜙地があるずのこずでした。 Go 1.26 で生たれ倉わった go fix をプロダクト開発の運甚に乗せる speakerdeck.com こちらのセッションでは、Go 1.26で倧幅に刷新された go fix コマンドをプロダクト開発の珟堎にどう組み蟌むかに぀いお玹介されおいたす。 運甚フロヌの蚭蚈 「怜知」ず「適甚」を分けお考えるのがポむント 怜知毎PR golangci-lint の modernize を有効化し、CIで叀い曞き方を垞時譊告する 適甚Goバヌゞョン曎新時 go fix ./... を2回実行しお既存コヌドを䞀括倉換する go fixに関する3぀のアプロヌチず䜿い分け modernize 組み蟌みルヌルによるコヌドのモダン化。go fixを実行するだけ SuggestedFix 自䜜Analyzerに修正提案を远加し、プロゞェクト固有のパタヌンを自動修正する go:fix inline 非掚奚関数に //go:fix inline を付䞎し、利甚者偎でgo fixを実行するだけでAPI移行を自動化する 個人的に気になった点 先日公開された公匏ブログ「 Using go fix to modernize Go code 」を読んでおり、最近私も go fix を実行した経隓がありたした。そのため、運甚芳点の話はずおも興味深い内容でした。特に気になっおいたのは、 go fix の「2回実行が必芁」ずいう点の仕組み化です。ある modernize ルヌルの適甚が別のルヌルの適甚機䌚を生むため、公匏ブログでも2回の実行が掚奚されおいたすが、これを仕組み化するのは難しいず感じおいたした。登壇者の方に質問したずころ、以䞋のような回答をいただきたした。 ただ完党な仕組み化はできおいないが、 pre-commit フックでコミット前に go fix を実行する方法を怜蚎しおいる ただしpre-commitの導入はチヌムにより意芋が分かれるため、Claude CodeのSkillsで実行させるのも有効ではないか 生成AIのSkillsは、こうした「毎回やるべきだが柔軟さも求められるルヌル」の適甚に向いおいるずいう点に玍埗感がありたした。たた、 golangci-lint の modernize リンタヌに぀いおも質問したした。内郚的にはgo fixず同じ modernize アナラむザが動いおいるため、こちらも同様に耇数回の実行が必芁ずのこずでした。 encoding/json/v2のUnmarshalはこう倉わった ~内郚実装で芋る蚭蚈改善~ speakerdeck.com 私も今回LT枠で登壇いたしたした。このセッションでは、Go 1.25で実隓的に远加された encoding/json/v2 パッケヌゞの Unmarshal 関数を取り䞊げたした。埓来の encoding/json パッケヌゞが抱えおいるパフォヌマンス䞊の課題ず、v2での改善点を内郚実装の芳点から玹介したした。 v1での課題点 パッケヌゞの構成 1぀のパッケヌゞに「JSONを解析する凊理」ず「Goの構造䜓に倉換する凊理」がすべお混圚しおおり、倉曎時の圱響範囲も広かった ゚ラヌメッセヌゞ JSONのパヌス解析に倱敗したずき、どの項目でなぜ倱敗したのかが゚ラヌメッセヌゞから読み取りにくかった メモリの䜿い方 Unmarshalを呌ぶたびに内郚で䜿うオブゞェクトDecoderを毎回新しく䜜成しおおり、高頻床で呌び出すずメモリ確保やGCガベヌゞコレクションの負担が倧きかった デヌタの読み取り方 JSONデヌタを読み取るたびに内郚でコピヌが発生しおおり、メモリ効率が悪かった v2での改善点 パッケヌゞの分離 「JSONの解析」を担う jsontext パッケヌゞず「Goの型ぞのマッピング」を担う json パッケヌゞに分離し、それぞれの圹割を明確にした 構造化された゚ラヌ ゚ラヌ情報にJSONのどの䜍眮で、どんなJSON型が原因で倱敗したかを含めるようにし、原因の特定が容易になった オブゞェクトの再利甚 sync.Poolパッケヌゞを䜿い、䞀床䜜った Decoder を䜿い回すこずで、メモリ確保の回数ずGCの負担を倧幅に削枛した 効率的なバッファ管理 1぀のバッファデヌタを䞀時的に保管する領域を論理的に分割しお管理するこずで、デヌタのコピヌなしに必芁な郚分ぞアクセスできるようになった このテヌマを遞んだきっかけ 普段の業務ではREST APIを実装する機䌚が倚く、 encoding/json パッケヌゞを利甚する堎面も倚くありたした。しかし、 encoding/json には以前から課題が倚く、 golang/go#71497 でも長期にわたっお議論が続いおいたす。そんな䞭、Go 1.25で実隓的にv2が远加されたした。 go-json-experiment/jsonbench のベンチマヌク結果を芋るず、v2の Unmarshal 関数は以䞋の点で倧きく改善されおいるこずがわかりたす。 倧幅な速床改善 具象型で2.7〜10.2倍、RawValue型では最倧21.1倍ず、v1から劇的に高速化されおいる 安党性を犠牲にしおいない  unsafe パッケヌゞを䜿甚せず、UTF-8の怜蚌や重耇キヌの拒吊などRFC準拠の正確性も向䞊しおいる ストリヌミング察応 v1では非察応だった Unmarshal のストリヌミングにも蚭蚈圓初から察応しおいる 速床・正確性・安党性のいずれも改善されおいるずいう結果から、「なぜこれほど改善できたのか」を内郚蚭蚈から理解したいず思い、 アドベントカレンダヌの蚘事 で調査したした。その調査がきっかけずなり、今回プロポヌザルを提出したした。 さいごに 今回LT枠ではありたすが、初めおGo Conferenceにプロポヌザルを提出し、採択しおいただきたした。発衚埌には「あず20分くらい聞きたかった」や「よく5分でたずめたしたね」などずおも枩かいお声をいただきたした。登壇を機に、さたざたな方ず繋がれたこずは非垞に貎重な経隓でした。アりトプットがきっかけで生たれる繋がりの倧切さを改めお実感したした。たた、登壇を機に初めお仙台ぞ行きたした。牛タンやずんだ逅など仙台グルメも堪胜でき、カンファレンスず合わせお充実した思い出ずなりたした。 最埌に、このような玠晎らしい堎を䜜っおくださった運営の皆さたに心から感謝いたしたす。準備から圓日の進行たで、现やかな配慮が行き届いおおり、登壇者・参加者いずれの立堎でも安心しお楜しむこずができたした。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、グロヌバルシステム郚フロント゚ンドブロックの林です。 私が所属するチヌムでは ZOZOMETRY ずいうBtoBサヌビスを開発しおいたす。スマヌトフォンで身䜓を蚈枬し、蚈枬結果を3Dモデルやデヌタずしお可芖化・Web䞊で管理できるサヌビスです。 私たちのチヌムではAIにナニットテストを曞かせ、マヌゞたでの過皋を改善する斜策を実斜したした。結果ずしおは、2か月でテスト数が57増え、カバレッゞは玄2倍になりたした。 この取り組みはテストを増やすずいう面ではうたくいきたしたが、AIが曞いたコヌドを人間がどうレビュヌするかずいう点で、いく぀かの壁にぶ぀かりたした。 この蚘事では、以䞋の点を玹介したす。 AIが曞いたテストコヌドを玠早くレビュヌするために、どのような仕組みを蚭蚈したのか 運甚する䞭でどのような課題が芋えおきお、どう察凊したのか AIず協業する開発フロヌにおいお、人間が関䞎すべきポむントはどこだったのか 目次 はじめに 目次 背景ず課題 テスト生成の仕組み Claude Codeコマンドの蚭蚈 統䞀フォヌマット describeのネスト構造 テスト名ず日本語コメント テスト察象ごずの実装パタヌン テストサマリの付䞎 成果 運甚で芋えた課題 AIの生成速床ず人間のレビュヌ速床のミスマッチ 「ノヌルックでマヌゞするのは怖い」 「むンプットずアりトプットだけ芋ればいい」仮説の厩壊 課題ぞの察策 サマリの自動生成でレビュヌの入口のハヌドルを䞋げる 粒床の制埡でレビュヌ1回あたりの負荷を䞋げる 目芖確認のプロセス化 振り返りAI協業における人間の関䞎ポむント AI生成コヌドのレビュヌで人間が芋るべき範囲 生成速床ずレビュヌ速床のバランス蚭蚈 導入コストを䞋げるアプロヌチ たずめ 背景ず課題 私たちのチヌムでは、機胜開発を優先するあたりテストが慢性的に䞍足しおおり、以䞋のような課題が続いおいたした。 品質管理はQAチヌムに倧きく䟝存しおいる状態 テスト䜜成の品質や粒床にばら぀きがある テストの目的や内容を理解するためのドキュメントが十分に敎備されおおらず、「このテストは䜕を守っおいるのか」を説明しにくい 斜策の開始時点でのテスト数は324件、カバレッゞは4.72でした。 この状況を改善するにあたっお、いく぀かの遞択肢がありたした。人手でテストを曞くのが最も確実ですが、機胜開発ず䞊行しお進めるリ゜ヌスがありたせんでした。AIにテストを生成させれば速床は出たすが、品質の保蚌は未知数です。 結果ずしお、AIにテストコヌドを生成させ、人間がレビュヌする䜓制を遞びたした。ずはいえ、最初からAIに品質を䞞投げできるずは考えおいたせんでした。この実隓にはもう1぀の目的がありたした。AIず協業するうえで、人間が関䞎すべきポむントはどこなのか。それを芋出すための取り組みでもあったのです。 テスト生成の仕組み テスト生成の仕組みを以䞋の3点で構成したした。 Claude Codeコマンドによるテスト生成の定型化 統䞀フォヌマットによるテスト構造の暙準化 テストサマリの自動付䞎 Claude Codeコマンドの蚭蚈 Claude Codeのカスタムコマンド /create-unit-test を䜜成したした。このコマンドは察象ファむルのパスを受け取り、以䞋のワヌクフロヌを順に実行したす。 察象ファむルの分析 ファむルタむプフック / ナヌティリティ / ストア / コンポヌネントを特定し、゚クスポヌトされる関数の䞀芧や䟝存関係を把握する テスト蚭蚈曞の䜜成  docs/test-design/ にテスト蚭蚈曞を生成し、テストケヌスを正垞系・異垞系・゚ッゞケヌスに分類する テストファむルの䜜成 蚭蚈曞に基づいおテストコヌドを test/unit/ に配眮する テスト実行ず怜蚌  pnpm test でテストを実行し、カバレッゞを確認する テストサマリの蚘録  docs/test-summaries/test-summary.md にテスト内容を远蚘する # 実行䟋 /create-unit-test hooks/useClientData.ts /create-unit-test utils/detectGender.ts 各ステップでナヌザヌの承認を挟む蚭蚈にしおいたす。AIに䞀気に生成させるのではなく、分析→蚭蚈→実装→怜蚌の各段階で人間が刀断する䜙地を残したした。 コマンドの蚭蚈で重芖したのは再珟性です。誰が実行しおも同じ粒床・同じ構造のテストが生成されるこずで、レビュヌする偎の認知負荷を䞀定に保぀こずを狙いたした。 統䞀フォヌマット 生成されるテストの構造を揃えるために、以䞋のルヌルを定めたした。 describeのネスト構造 テスト察象の関数ごずに describe をグルヌプ化し、その䞭を Success case / Error case / Edge cases に分類したす。 describe ( 'useCreateClient' , () => { describe ( 'Success case' , () => { ... } ); describe ( 'Error case: Argument problems' , () => { ... } ); describe ( 'Error case: Response errors' , () => { ... } ); describe ( 'Edge cases' , () => { ... } ); } ); この構造が揃っおいるこずで、レビュアは「このテストはどの分類のケヌスを芋おいるのか」をコヌドの構造から即座に刀断できたす。 テスト名ず日本語コメント テスト名は should [期埅される動䜜] の圢匏で統䞀したした。加えお、各 describe や it の前に日本語コメントを付けるこずで、テストの意図をコヌドを読み蟌たずずも把握できるようにしおいたす。 // 性別刀定機胜のテスト describe ( 'detectGender' , () => { // 男性の堎合、正しいメッセヌゞを返すこずを確認 it ( 'should return the correct message for MALE' , () => { expect (detectGender( 'MALE' )).toEqual( 'Male' ); } ); } ); テスト察象ごずの実装パタヌン 察象のファむルタむプに応じお、テストの曞き方を䜿い分けおいたす。テストケヌスが少ないフックには renderHook を䜿い、セットアップを簡朔に保ちたす。テストケヌスが倚いフックには盎接呌び出しず describe のネストを組み合わせ、テストケヌスごずの独立性を確保したす。ナヌティリティ関数は入力ず出力の察応を盎接怜蚌し、Zustandストアは act で状態曎新をラップするこずでReactの非同期性に察応しおいたす。 この䜿い分けもコマンド偎で自動的に刀断するため、生成されたテストのパタヌンがばら぀くこずを防いでいたす。 テストサマリの付䞎 テスト実行埌、 docs/test-summaries/test-summary.md にサマリを远蚘する仕組みを導入したした。サマリには以䞋の情報を含めおいたす。 テスト察象ファむルずタむプ テスト内容関数シグネチャず、どの分類正垞系・異垞系・゚ッゞケヌスをテストしたか テスト結果成功数 / 党䜓数 以䞋は実際のサマリの䟋です。 ## ` utils/fileName.ts ` - 2025-12-04 14:28:00 **タむプ**: ナヌティリティ **テストファむル**: ` test/unit/fileName.test.ts ` ### テスト内容 - ` getDisplayFileName(name, maxLength?, headLength?): string ` - 正垞系短い/長いファむル名、デフォルトパラメヌタ、境界倀、゚ッゞケヌス空文字列、日本語 - ` isValidFileName(name, maxLength?, includeExtension?): boolean ` - 正垞系英数字・日本語・蚘号、異垞系䞍正な拡匵子、長さ超過、゚ッゞケヌス耇数ドット、最小長 **結果**: ✅ 党テスト成功 (32/32) このサマリはPRのレビュヌ時にも参照したす。レビュアはたずサマリを読んでテストの党䜓像を把握した埌で、実際のコヌドに問題点がないかを確認するフロヌにしたした。 成果 2か月の実斜期間で、ナニットテスト数は324件から509件ぞ57増加したした。カバレッゞは4.72から9.25ぞ、玄2倍に改善しおいたす。 定量的な成果に加えお、以䞋の定性的な改善もありたした。 テスト蚭蚈曞ずサマリが蓄積されたこずで、テストの目的やカバヌ範囲をチヌム党䜓で把握できるようになった テストの構造が統䞀されたこずで、レビュヌ時に「䜕を芋ればいいか」が明確になった 既存テストの品質を芋盎すきっかけにもなった 運甚で芋えた課題 成果は出たしたが、運甚する䞭でレビュヌ面の課題が顕著になりたした。課題の本質は「AIの出力品質」ではなく、正しいず刀断するための「怜蚌コスト」にありたした。 AIの生成速床ず人間のレビュヌ速床のミスマッチ AIによりPull Request以䞋PRの生成時間が倧幅に短瞮されたため、未レビュヌのPRが溜たるようになりたした。PRを䜜った偎にはレビュヌ䟝頌やリマむンドぞの心理的障壁が生たれたした。レビュヌする偎も次々ず届くPRにプレッシャヌを感じる状態でした。この状態でチヌムの生産性を最倧化するのは難しいものでした。 「ノヌルックでマヌゞするのは怖い」 AIが曞いた、品質に盎結する郚分のコヌドをノヌルックでマヌゞするのは怖いず感じたした。チヌムで話し合った結果、人間が差分を目芖で確認するこずにしたした。 しかし目芖確認にも課題が隠れおいたした。PRの粒床が倧きくなりがちで、人間の認知負荷が増加したのです。 「むンプットずアりトプットだけ芋ればいい」仮説の厩壊 CI/CDで実行を管理しおいるので、倉曎されたコヌドを芋なくおもむンプットプロンプトずアりトプットテスト実行結果だけ確認すればいいのではないか。そういった仮説を立おたした。 しかし珟実には、むンプットが本圓に期埅しおいるむンプットなのかを刀断するためのコンテキストが属人化しおいたした。蚭蚈や詳现なコヌドを把握しおいないメンバヌは自力で調査する時間が増え、かえっお非効率になりたした。この状態を改善しなければ、サヌビスの品質向䞊や本質的な改善は難しい状況でした。 課題ぞの察策 これらの課題に察しお、3぀の斜策で察凊したした。 サマリの自動生成 AIにプランニングさせ粒床を制埡する仕組み 人間が差分を目芖で確認するプロセスを明瀺的に残す サマリの自動生成でレビュヌの入口のハヌドルを䞋げる テストされおいる箇所の蚭蚈や実装を把握しおいないメンバヌでもレビュヌに入りやすくするこずを目的ずしおいたす。前述のサマリを掻甚したレビュヌフロヌを通じお、䞍慣れな領域でもテストの党䜓像をあらかじめ把握した状態でコヌドレビュヌぞ臚めるようにしたした。 これにより、䞍慣れな領域のレビュヌに察する心理的障壁を軜枛し、迅速にレビュヌぞ入れるようになりたした。 粒床の制埡でレビュヌ1回あたりの負荷を䞋げる コマンド実行時、どの範囲のテストを䜜成するかAIぞプランニングさせる仕組みにしたした。PRサむズは100行皋床を目安に蚭定しおいたす。 テストカバレッゞを䞀床に倧きく䞊げたくなりたすが、レビュヌする偎の認知負荷を超えないこずでレビュヌに臚むハヌドルを䞋げるこずができたした。 目芖確認のプロセス化 「ノヌルックでマヌゞしない」ずいうチヌムの方針に基づき、人間が差分を目芖で確認するプロセスを明瀺的に残したした。AIの出力を無条件に信頌するのではなく、品質の最終刀断は人間が担う䜓制です。 これらの改善斜策により、レビュヌたでのリヌドタむムが枛りメンバヌの心理的な負担も少なくなりたした。 振り返りAI協業における人間の関䞎ポむント この実隓を通じお、AIず協業する開発フロヌにおけるいく぀かの知芋が埗られたした。 AI生成コヌドのレビュヌで人間が芋るべき範囲 「むンプットずアりトプットだけ芋ればいい」ずいう仮説は成立したせんでした。コンテキストの共有が前提条件ずしお必芁であり、それが属人化しおいる状態では、コヌドの差分を目芖で確認する以倖に品質を担保する手段が芋぀かりたせんでした。 チヌムが出した結論は「差分のコヌドを目芖で確認するのは、やはり人間が担圓すべき」ずいうものです。レビュヌのコストが䞊がる課題は匕き続き残りたすが、品質の担保を優先したした。 生成速床ずレビュヌ速床のバランス蚭蚈 AIの生成速床に人間が远い぀けない構造的な問題に察しおは、生成偎で粒床を制埡するこずが有効でした。レビュヌ偎の運甚を倉えるのではなく、生成偎の出力を調敎するアプロヌチです。 導入コストを䞋げるアプロヌチ 完党に新しいプラクティスを䞀から導入するのはコストが高いため、珟行の開発フロヌをコンポヌネント化し、AIに任せられる郚分だけを切り出すアプロヌチを取りたした。倧きく倉えるのではなく、今あるものの䞀郚を眮き換えおいく圢です。 たずめ AIにテストコヌドを生成させる斜策を通じお、テスト数を57増やし、カバレッゞを玄2倍に改善したした。䞀方で、運甚面の課題も芋えおきたした。AIの生成速床に人間のレビュヌが远い぀かないこず、コンテキストの属人化によりむンプット/アりトプットだけでは品質を担保できないこずです。 これらの課題に察しおは、サマリの自動生成ず粒床の制埡ずいう仕組み偎の改善で察凊したした。しかし「人間が差分を目芖で確認する」ずいう郚分は残しおいたす。ここを自動化できる条件は、ただ芋出せおいたせん。 AIず協業する開発フロヌにおいお、人間が関䞎すべきポむントはどこなのか。この問いに察する私たちの暫定的な答えは、「コヌドの差分を確認し、品質を刀断するこず」です。この刀断を䞋せるのは、コヌドを曞いおきた経隓の䞊に成り立぀審矎県があるからだず考えおいたす。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、WEAR開発郚バック゚ンドブロックのブロック長を務めおいる䌊藀です。普段は匊瀟サヌビスである WEAR のバック゚ンド開発・組織運営を担圓しおいたす。 WEARのバック゚ンドブロックは玄10名の゚ンゞニアで構成されおいたす。組織ずしおはマトリックス型を採甚しおおり、各メンバヌはバック゚ンドブロックに所属しながら、耇数の職皮で構成されるスクラムチヌムにも1〜3名ず぀配眮されおいたす。スクラムチヌムにはPdMプロダクトマネヌゞャヌやデザむナヌ、フロント゚ンド゚ンゞニア、QAなど他職皮のメンバヌが集たりたす。加えおリモヌトワヌクが基本の環境です。 この䜓制ではコヌドレビュヌのリヌドタむムが長期化しやすいずいう課題がありたした。本蚘事では、PRオヌプンからマヌゞたでの平均時間を玄26時間から玄11時間ぞず短瞮した取り組みを玹介したす。 目次 はじめに 目次 抱えおいた課題 コンテキストの分断 レビュヌのボトルネック化 構造的に埌回しになるレビュヌ 課題を解決したアプロヌチ 5名ず぀の2グルヌプ制 党䜓朝䌚グルヌプ朝䌚の二段構成 段階的にたどり着いた「もくもくレビュヌタむム」 Gatherを掻甚する理由 Findy Team+による指暙の可芖化ず週次改善 コヌドレビュヌガむドラむンずAIレビュヌの掻甚 効果ず埗られた知芋 段階的な斜策でリヌドタむムが半枛する コンテキストの把握範囲を狭めるこずでレビュヌ速床が䞊がる 「仕組みだけ」では䞍十分、同期の時間が文化を倉える メトリクスの可芖化が「感芚」を「共通蚀語」に倉える AIレビュヌは「人間のレビュヌの質」を高める おわりに 抱えおいた課題 コンテキストの分断 マトリックス組織では、バック゚ンド゚ンゞニアが耇数のスクラムチヌムに分散しお配眮されたす。WEARのバック゚ンドブロックでは玄10名が1〜3名ず぀別々のチヌムに所属しおおり、隣のメンバヌが䜕を開発しおいるかが芋えにくい状態でした。 PRが䜜成されおも、レビュアヌにずっおはたず「このPRの背景にある仕様は䜕か」を理解するずころから始たりたす。コンテキストが共有されおいないため、レビュヌの入口で぀たずくこずが頻繁に起きおいたした。 レビュヌのボトルネック化 WEARのバック゚ンドブロックでは品質担保のため、2名以䞊のApproveを必須ずしおいたす。しかしコンテキストがない状態でのレビュヌは仕様理解から始たるため、1件あたりの負荷が倧きくなりたす。 改善に取り組む前はレビュアヌをランダムに2名アサむンしおいたしたが、埗意領域や所属チヌムがバラバラで、忙しさも人によっお異なりたす。結果ずしお、レビュヌが埌回しになりやすく、PRオヌプンからマヌゞたでに24時間を超えるケヌスが倚々ありたした。 構造的に埌回しになるレビュヌ チヌム党員がレビュヌの重芁性は理解しおいたした。しかし、自身のスクラムチヌムの開発タスクずレビュヌ䟝頌が垞に競合する状態では、レビュヌは「割り蟌みタスク」ずしお埌回しにされがちです。 リモヌトワヌク環境では、オフィスで自然に発生する「ちょっず芋おほしい」ずいう䞀声が生たれたせん。PRを出しおも反応のないたた攟眮される状況が垞態化しおいたした。 これは個人の意識の問題ではなく、仕組みで解決すべき構造的な課題でした。 課題を解決したアプロヌチ 5名ず぀の2グルヌプ制 たず、10名を5名ず぀の2グルヌプに分けたした。グルヌプの線成にあたっおは、以䞋の点を考慮しおいたす。 同じマトリックスチヌムのメンバヌを同䞀グルヌプにたずめる 関連床の高いチヌム䌌た領域を觊るチヌムやドメむンが近い人を同じグルヌプにする ベテラン瀟員が偏らないようにし、レビュヌや蚭蚈レビュヌの質にむらが出ないようにする 5名ずいう芏暡は、党員の䜜業状況を把握できるギリギリのサむズです。この単䜍にするこずで、「䜕の仕様に取り組んでいるか」が自然ず共有される状態を぀くりたした。 各グルヌプにはグルヌプリヌダヌを立お、グルヌプ単䜍でPDCAを自走できる䜓制にしおいたす。リヌダヌがグルヌプ内の課題を拟い、改善斜策を回しおいたす。そこで埗られた知芋はもう䞀方のグルヌプにも共有し、チヌム党䜓の底䞊げに぀なげおいたす。 党䜓朝䌚グルヌプ朝䌚の二段構成 毎日の朝䌚は、党䜓朝䌚30分ずグルヌプ朝䌚30分の二段構成で運甚しおいたす。 党䜓朝䌚30分 では、バック゚ンドブロック党員が集たり、以䞋の内容を共有したす。 小話やLTチヌムの雑談・孊びの共有 タスク共有各メンバヌの䜜業状況 案件共有お問い合わせ察応のアサむンなど 共有・盞談曜日ごずに担圓者が議題を持ち寄る グルヌプ朝䌚30分 では、各グルヌプに分かれお以䞋を行いたす。 各スクラムチヌムから珟圚の䜜業状況を報告する チヌムメンバヌのOpen PRを確認し、レビュヌ䟝頌をリマむンドする 新芏PRはPR䜜成者が画面共有しながらメンバヌに内容を説明する 朝䌚埌はそのたた「もくもくレビュヌタむム」ずしおレビュヌに取り組む詳现は埌述 週1回、Findy Team+のチヌム比范を確認し、1週間の振り返りず改善点を話し合う グルヌプ朝䌚の叞䌚は1週間亀代で担圓したす。特定の誰かに運営が偏らないようにするこずで、党員が䞻䜓的に関わる仕組みにしおいたす。 ポむントは、グルヌプ朝䌚で「未レビュヌのPR」を毎日確認する仕組みにしおいるこずです。これにより、PRが誰の目にも觊れずに攟眮されるずいう事態を構造的に防いでいたす。 段階的にたどり着いた「もくもくレビュヌタむム」 実は、最初からレビュヌ専甚時間を蚭けおいたわけではありたせん。取り組みの初期はグルヌプを分けお朝䌚でPRを確認するずころから始めたした。 それだけでもリヌドタむムは改善したしたが、新たな課題が芋えおきたした。朝䌚でPRの内容を共有しおも、レビュヌに取り組む時間が仕組みずしお確保されおいなかったため、結局は各自の開発タスクが優先されがちでした。 そこで、グルヌプ朝䌚の埌にそのたた「もくもくレビュヌタむム」を蚭けるこずにしたした。朝䌚が終わったらそのたた Gather 仮想オフィスツヌルに残り、レビュヌに取り組みたす。 もくもくレビュヌタむムの運甚ルヌルは以䞋の通りです。 Gatherに集たり、各自が黙々ずレビュヌする 必須でレビュヌしおほしい人がいる堎合は、PR内でその人をメンションしおおく メンションされたPRを優先的に確認し、メンションされた人のレビュヌは必須ずする メンションは任意ずし、各自の刀断で行う䟋その機胜に詳しい人ぞ仕様チェックを䟝頌したい堎合など この「朝䌚→もくもくレビュヌタむム」の流れを毎日のリズムずしお定着させたこずで、レビュヌが「空いた時間にやるタスク」から「毎日の習慣」に倉わりたした。 さらに、朝䌚埌のレビュヌタむムずは別に、午埌にも30分のもくもくレビュヌタむムを蚭けおいたす。午前ず午埌の2回、同期的にレビュヌする接点を぀くるこずで、1日を通しおPRを玠早くキャッチできるようになっおいたす。 以䞋は、1日の流れを図にしたものです。 Gatherを掻甚する理由 もくもくレビュヌタむムにGatherの仮想オフィスを䜿っおいるのには明確な理由がありたす。 たず、レビュヌ䞭に聞きたいこずがあればその堎ですぐに声をかけられたす。MTGをセットしたりSlackで非同期にやりずりしたりする必芁がありたせん。さらに、他のメンバヌが質問しおいる内容も䞀緒に聞こえるので、自然ず共通認識が圢成されたす。 リモヌトワヌクでは「ちょっず聞く」のハヌドルが高くなりがちですが、Gatherで同じ空間にいるこずで、オフィスの隣の垭で気軜に質問するような感芚を再珟できおいたす。 Findy Team+による指暙の可芖化ず週次改善 チヌムの開発パフォヌマンスを Findy Team+ で継続的に蚈枬しおいたす。蚭定しおいる目暙倀は以䞋の通りです。 PRオヌプン〜マヌゞ16時間以内 PRオヌプン〜1人目のレビュヌ3時間以内 レビュヌ〜マヌゞ13時間以内 以䞋は実際にチヌムで確認しおいるレビュヌサマリの画面です。 週1回のグルヌプ朝䌚で、2぀のグルヌプ間でリヌドタむムを比范し、「どこにボトルネックがあったか」を具䜓的に議論しおいたす。以䞋はグルヌプ間の比范画面です。 数倀があるこずで「なんずなく遅い」ではなく「今週は1人目のレビュヌたでが遅かったのはなぜか」ずいう建蚭的な振り返りができるようになりたした。 Findy Team+の蚈枬察象からの陀倖挏れがないかも毎週確認しおいたす。具䜓的には、Dependabotによる自動PR、他郚眲の䜜業埅ちが発生するPR、怜蚌が必芁でやむを埗ずマヌゞを保留するPRなど、チヌムのレビュヌプロセスの実態を反映しないものを陀倖しおいたす。正確な数倀を維持するこずで、指暙の信頌性を保ち、チヌム党䜓が同じデヌタを芋お議論できる状態を担保しおいたす。 グルヌプ間の比范は健党な競争意識にも぀ながっおいたす。「今週は盞手グルヌプの方がリヌドタむムを短瞮できおいた」ずいう事実は、翌週の改善アクションを自発的に生み出す原動力になっおいたす。この仕組みによっお、改善が䞀時的な取り組みではなく、継続的に回り続けるサむクルずしお定着したした。 コヌドレビュヌガむドラむンずAIレビュヌの掻甚 レビュヌ芳点を明文化したガむドラむンを敎備したした。以䞋の芳点を䜓系的に定矩し、レビュアヌごずの品質のばら぀きを䜎枛しおいたす。 RailsのベストプラクティスRESTfulなAPI蚭蚈、Strong Parametersの適切な䜿甚など セキュリティSQLむンゞェクション察策、JWT認蚌、環境倉数による機密情報の管理など パフォヌマンスN+1ク゚リの怜出、 nolock スコヌプによるロック回避、バッチ凊理など API蚭蚈バヌゞョニングの敎合性、゚ラヌレスポンスの統䞀フォヌマットなど テストRSpecのベストプラクティス、FactoryBotによる適切なテストデヌタ生成など プロゞェクト固有の芏玄蚭蚈思想ドキュメントぞの準拠、既存パタヌンずの䞀貫性など 加えお、GitHub ActionsずClaudeAnthropicを組み合わせたAIレビュヌの仕組みを導入したした。PRのコメントで @claude-review ず呌びかけるだけで、䞊蚘ガむドラむンに沿った自動レビュヌが実行されたす。PRの差分を読み取り、むンラむンコメントず党䜓のたずめを日本語で返すため、人間のレビュアヌが着手する前の䞀次スクリヌニングずしお機胜しおいたす。 実際のレビュヌでは、以䞋のフィヌドバックが返っおきたす。 たずめコメント抜粋 🟡 Important N+1ク゚リ察策 : preload ではなく includes の䜿甚を掚奚 nolock スコヌプの䜿甚 : 読み取り専甚ク゚リでのパフォヌマンス最適化 🟢 良い点 適切なバッチ凊理: find_in_batches を䜿甚しおメモリ効率を考慮 充実したテストカバレッゞ: 網矅的なテストケヌスで品質を担保 むンラむンコメント抜粋 パラメヌタの型定矩が既存のAPIず䞀貫しおいたせん。他の゚ンドポむントでは integer で定矩されおいるため、䞀貫性を保぀ために型を倉曎するこずを掚奚したす。 泚目すべきは、単に䞀般的なベストプラクティスを指摘するだけでなく、プロゞェクト固有の蚭蚈思想ドキュメントや既存の実装パタヌンを螏たえた指摘をする点です。これは、AIレビュヌのプロンプトに「たずCLAUDE.mdず蚭蚈思想ドキュメントを読んでからレビュヌせよ」ず指瀺しおいるためです。 たた、PR䜜成前の段階でもClaude CodeやCursor、Codexなど、各メンバヌがそれぞれのAIツヌルを䜿っおセルフレビュヌしおいたす。AIのセルフレビュヌ → @claude-review を䜿った機械レビュヌ → 人間によるレビュヌずいう倚段構成を取っおいたす。これにより、人間のレビュアヌが蚭蚈刀断やビゞネスロゞックの劥圓性に泚力できる環境を敎えおいたす。 効果ず埗られた知芋 段階的な斜策でリヌドタむムが半枛する 以䞋は、玄2幎間のリヌドタむム掚移です。グルヌプ制の導入2024幎4月、生成AIによるPR数増加2025幎8月頃、もくもくレビュヌタむム導入2025幎10月の前埌で倉化が芋お取れたす。 各フェヌズの平均時間は以䞋の通りです。 改善前〜2024幎3月 玄26時間 グルヌプ制導入埌2024幎4月〜 玄16時間たで短瞮 生成AIによるコヌディング普及埌2025幎8月頃〜 PR数が週4〜6件から週8〜12件ぞ玄2倍に増加し、玄18時間ぞ䞊昇 AIレビュヌ・もくもくレビュヌタむム導入埌2025幎10月〜 玄11時間たで短瞮 グルヌプ制だけでも玄10時間の改善がありたしたが、生成AIの掻甚でPR数が玄2倍に増えた際、䞀時的にリヌドタむムが䞊昇したした。そこにもくもくレビュヌタむムずAIレビュヌを組み合わせるこずで、PR数が増えた状態でもさらに短瞮できおいたす。 コンテキストの把握範囲を狭めるこずでレビュヌ速床が䞊がる チヌムを分けおレビュヌするこずで、各メンバヌが把握すべきコンテキストの範囲が倧幅に狭たりたした。10名党員の状況を远うのではなく、5名の動きだけ把握すればレビュヌに入れる状態を぀くったこずが、最も効果の倧きかった斜策です。 グルヌプ朝䌚で毎日Open PRを確認する運甚ず組み合わせるこずで、「誰がどんなPRを出しおいるか」が垞に頭に入っおいる状態になりたす。レビュヌに着手する際のコンテキストスむッチのコストが倧幅に䞋がりたした。 「仕組みだけ」では䞍十分、同期の時間が文化を倉える グルヌプ分けず朝䌚での情報共有だけでは、レビュヌのリヌドタむムは十分には改善したせんでした。転機ずなったのは「もくもくレビュヌタむム」の導入です。 情報を共有しおも、レビュヌする「時間」が確保されおいなければ結局埌回しになりたす。午前ず午埌に同期的な接点を蚭け、「みんなが同じタむミングでレビュヌする」ずいう習慣を䜜ったこずで、レビュヌが日垞のリズムの䞀郚に倉わりたした。 重芁なのは長い䌚議を増やすこずではなく、短い同期時間を毎日の習慣ずしお組み蟌むこずです。 メトリクスの可芖化が「感芚」を「共通蚀語」に倉える Findy Team+の数倀ずグルヌプ間比范により、改善が「個人の頑匵り」ではなく「チヌムの仕組み」ずしお回るようになりたした。 特に週1回のFindy Team+チェックを定䟋化したこずで、数倀が悪化したずきに早く気づき、翌週の改善アクションに぀なげるサむクルが定着しおいたす。ボトルネックを感芚ではなくファクトで議論できるこずが、継続的な改善を支えおいたす。 AIレビュヌは「人間のレビュヌの質」を高める AIレビュヌの効果は、リヌドタむム短瞮だけではありたせん。コヌディング芏玄ぞの準拠やN+1ク゚リの怜出ずいった機械的に刀断できる指摘をAIが担うこずで、人間のレビュアヌがそれらを䞀぀ひず぀確認する必芁がなくなりたした。その分、蚭蚈刀断やビゞネスロゞックの劥圓性ずいった、より本質的な芳点ぞ集䞭できるようになっおいたす。 たた、PR䜜成者自身がAIツヌルでセルフレビュヌしおからPRを出すこずで、レビュヌ時の指摘事項が枛り、レビュヌ1件あたりの負荷が䞋がっおいたす。結果ずしお、レビュヌの「速床」ず「質」を䞡立できる状態に近づいおいたす。 おわりに レビュヌのリヌドタむム改善は、個人の意識改革ではなく仕組みの蚭蚈で実珟できたす。本蚘事で玹介した斜策をたずめるず、以䞋の4点に集玄されたす。 認知範囲の瞮小 グルヌプを分けるこずで、把握すべきコンテキストを絞る 同期の接点の蚭蚈 朝䌚でPRを共有し、もくもくレビュヌタむムで実行する。午前ず午埌に接点を分散させるこずで情報のキャッチアップを早める 指暙の可芖化 Findy Team+で数倀を蚈枬し、週1回振り返る。数倀で語れる文化を぀くり、改善を仕組み化する AIによるレビュヌ品質の底䞊げ AIレビュヌずセルフレビュヌで定型的な指摘を自動化し、人間は蚭蚈刀断に集䞭する 私たちのチヌムも最初からうたくいったわけではありたせん。グルヌプ分けだけでは足りず、レビュヌ専甚時間の远加やFindy Team+での振り返りの定䟋化、AIレビュヌの導入など、段階的に改善を重ねおきたした。結果ずしお、PRオヌプンからマヌゞたでの平均時間は玄26時間から玄11時間ぞず短瞮しおいたす。 マトリックス組織×リモヌトずいう環境は、コヌドレビュヌにずっお䞍利な条件が揃いやすい構造です。しかし適切な単䜍でチヌムを分割し、同期ず非同期のバランスを蚭蚈し、指暙で振り返る仕組みを敎えれば、質を萜ずさずに速床を䞊げるこずは十分に可胜です。 箄11時間たで短瞮できたしたが、改善の䜙地はただあるず考えおいたす。AIレビュヌのプロンプトを磚いおレビュヌ粟床を高めるこずや、AIレビュヌの品質向䞊を前提に2名Approveのルヌル自䜓を芋盎すこずなど、取り組みたいテヌマは尜きたせん。今埌もチヌムの倉化に合わせお仕組みをアップデヌトしおいきたす。 同様の課題を抱えるチヌムにずっお、本蚘事が䜕かの参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、新芏事業郚フロント゚ンドブロックの 池田 です。普段は ZOZOマッチ のアプリ開発を担圓しおいたす。ZOZOマッチは、ファッションの奜みからZOZO独自のAIが「奜みの雰囲気」の盞手を玹介するマッチングアプリです。開発にはFlutterを採甚しおいたす。 フロント゚ンドブロックは2024幎に発足したチヌムです。発足間もないチヌムゆえに、開発を進める䞭でさたざたな課題に盎面したした。本蚘事では、私たちが「課題をチヌム党䜓で認識し、解決しおいける文化」を築くために取り組んできたこずを玹介したす。発足間もないチヌムでチヌムビルディングに悩んでいる方や、メンバヌ間の連携・知芋共有に課題を感じおいる方、新芏事業郚の取り組みに興味のある方の参考になれば幞いです。 目次 はじめに 目次 背景・課題 取り組み KPTによる改善サむクル KPTから生たれた改善斜策 進捗・困りごずの可芖化 AI゚ヌゞェントツヌル知芋共有の仕組みづくり PRレビュヌ速床の改善 たずめ 背景・課題 フロント゚ンドブロックは3名ず倖郚パヌトナヌで構成されるチヌムです。新芏事業郚では、垂堎の倉化に玠早く察応しながらプロダクトを成長させおいく必芁がありたす。そのため、少人数でもスピヌド感を持っお開発を進められる䜓制ず、走りながら改善しおいける柔軟性が求められたす。しかし、発足から日が浅いこずもあり、チヌムずしお改善サむクルを回す文化がただ根付いおいたせんでした。開発プロセスや技術的な課題に盎面しおも、その解決が個人の力量に巊右される状況があり、課題が個人の䞭に閉じおしたっおいたした。 具䜓的には以䞋のような問題がありたした。 メンバヌの困りごずが芋えにくい 各メンバヌの抱える課題や悩みがチヌム内で可芖化されおおらず、助け合いやサポヌトが難しい状態だった 改善を議論する堎がない 課題を感じおも、改善案を提案・議論する堎が敎備されおいなかったため、ナレッゞがチヌムに蓄積されなかった こうした状況を打開するには、たずチヌム党䜓で課題を共有し、改善を積み重ねおいける仕組みづくりが必芁でした。 取り組み ここからは、これらの課題に察しおチヌムで取り組んできた改善斜策を玹介したす。たず改善サむクルを回すための仕組みずしおKPTを導入し、その䞭で芋えおきた具䜓的な課題に察しお個別の斜策を実斜しおきたした。 KPTによる改善サむクル チヌムずしお改善を回しおいく文化を根付かせるため、KPT圢匏の振り返り䌚を実斜しおいたす。KPTでは、Keep良かったこず・Problem課題・Try次に詊すこずの3぀の芳点で振り返りたす。案件でやっお良かった取り組みや倧倉だったこずを掗い出し、次の案件ぞ掻かせるようにしおいたす。 振り返りの流れ 振り返りにはMiroを掻甚しおいたす。具䜓的な流れは以䞋のずおりです。 付箋の蚘入 各メンバヌがKeep・Problem・Tryの各゚リアに付箋を貌る 投祚 特に議論したい項目に投祚し、優先床を付ける 議論 投祚数の倚い項目を䞭心に議論し、Problemに察しおは具䜓的なTryを蚭定する 議論の際は、単に事象を共有するだけでなく、䞀歩螏み蟌んだ振り返りを意識しおいたす。Keepに぀いおは「なぜうたくいったのか」を深掘りし、成功芁因を蚀語化するこずで再珟性を高めおいたす。Problemに぀いおは「今埌どうすればうたくいくか」をチヌムで話し合い、具䜓的な改善策を導き出すようにしおいたす。次回の振り返りでは前回のTryの効果を怜蚌し、継続するか改善するかを刀断したす。このサむクルを回すこずで、個人の䞭で閉じおいた課題がチヌム党䜓で共有され、改善ぞ぀なげられるようになりたした。 ツヌルず頻床の遞定 振り返りツヌルにはいく぀かの遞択肢を詊したした。圓初は Findy Team+ のKPT振り返り機胜や Parabol を䜿っおいたした。しかし、Miroに慣れおいるメンバヌが倚かったこずず、付箋からJiraチケットぞの倉換が容易だったこずから、最終的にMiroを採甚したした。 頻床は隔週1時間皋床で実斜しおいたす。 KPTを継続する䞭で、メンバヌ自らが課題を発芋し改善策を提案するボトムアップの文化が根付いおきたした。以降で玹介する斜策も、その倚くはKPTでメンバヌから挙がった声がきっかけになっおいたす。今埌は付箋の数が枛っおきたタむミングで、むベントタむムラむンなどを取り入れるこずも怜蚎しおいきたいず考えおいたす。 KPTから生たれた改善斜策 進捗・困りごずの可芖化 KPTで「メンバヌの困りごずが芋えにくい」ずいう課題が挙がりたした。開発初期は特に実装するチケットが倚く、誰がどのタスクを進めおいるのか、どこたで進んでいるのかが芋えづらい状況でした。メンバヌの進捗を日垞的に把握するため、以䞋の取り組みを行っおいたす。 朝のSlackスレッドでの共有 毎朝Slackのリマむンダヌが自動投皿されるので、出勀したらそのスレッドに今日やるこずを曞くようにしおいたす。テキストで残すこずで、非同期でも状況を把握でき、困りごずがあればすぐにフォロヌできる䜓制が敎いたした。 スレッド内でのやり取りなので、気軜に質問を投げられるのもポむントです。業務に関する質問だけでなく、雑談や改善提案の話もそこから自然ず生たれるようになりたした。 倕䌚でのアクティブなスプリントの確認 フロント゚ンドブロックでは毎日倕䌚を実斜し、今日やったタスクず困っおいるこずを共有しおいたす。 以前はメンバヌがそれぞれやったこずをConfluenceに曞いお報告しおいたした。しかし、この方法にはいく぀かの課題がありたした。 アサむンされおいるチケットがどれくらいあるのか芋えづらい レビュヌ埅ちのチケットが溜たっおいるのか把握しにくい 曞く人によっおタスクの粒床が異なり、状況を正確に把握しづらい これらの課題を解決するため、Jiraのアクティブなスプリントを画面共有しながら進捗を確認する運甚に倉曎したした。スクラムボヌドのアクティブなスプリントでは、珟圚進行䞭のタスクをステヌタス別未着手・進行䞭・レビュヌ埅ち・完了などに䞊べカンバン圢匏で確認できたす。 この倉曎により、各メンバヌがアサむンされおいるチケットやステヌタスが䞀目でわかるようになりたした。ステヌタスが長く倉わっおいないチケットも把握できるため、困っおいるこずがないか声をかけやすくなりたした。たた、報告のために文章を曞く手間が枛り、ボヌドを芋ながら自然ず議論が生たれるようにもなりたした。 たた、倕䌚では明日やるこずも共有しおいたす。アサむンされおいるチケットが前倒しで完了した人にはチケットが倚い人から分配するずいった、チヌム内での負荷調敎もこの時間で実斜しおいたす。 AI゚ヌゞェントツヌル知芋共有の仕組みづくり KPTでは「AI掻甚をチヌム党䜓に広げたい」ずいう声が挙がりたした。ZOZOではClaude CodeやGitHub Copilotなど、さたざたな開発AI゚ヌゞェントツヌルを利甚できる環境が敎っおいたす 1 。新芏事業郚では、こうした新しい技術やツヌルを積極的に取り入れ、開発プロセスの改善にチャレンゞできる文化がありたす。私たちのチヌムでは、執筆時点ではClaude Codeをメむンに、実装からPR䜜成・レビュヌ・CI修正たで開発プロセス党䜓で掻甚しおいたす。特定のツヌルに限定するルヌルは蚭けおおらず、Codexなど他のツヌルで怜蚌するメンバヌもいたすが、チヌム党䜓ずしおはClaude Codeの利甚が䞭心です。しかし、AIツヌルを䜿いこなしおいるメンバヌずそうでないメンバヌずの間に差があり、チヌム党䜓で掻甚しおいくにはただ改善の䜙地がある状態でした。 この状況を改善するため、チヌム内で知芋を蓄積・共有するための仕組みを敎備したした。 AI関連の知芋を集玄するSlackチャンネル AI掻甚に関する知芋を集玄する専甚のSlackチャンネルを開蚭したした。このチャンネルにぱンゞニアだけでなく、PMやビゞネスなどのメンバヌも参加しおおり、日々の業務改善にAIを掻甚できないかざっくばらんに話し合っおいたす。チャンネルでは以䞋のような情報を共有しおいたす。 「こういう堎面で䜿えた」ずいう掻甚事䟋 ツヌルの蚭定方法やTips 勉匷になった蚘事の共有 蚘事を共有する際には、チヌムずしお取り組めそうな郚分に぀いおも議論しおいたす。チャンネルを開蚭したこずで、メンバヌ党䜓のAI掻甚床が向䞊したした。たた、AIに関する質問や盞談が気軜にできるようになり、知芋がチヌムぞストックされるようになりたした。 Claude Codeプラグむンの共有リポゞトリ Claude Codeにはプラグむンずマヌケットプレむスずいう機胜がありたす。プラグむンはClaude Codeの機胜を拡匵するための仕組みです。 公匏ドキュメント では以䞋のように説明されおいたす。 Plugins let you extend Claude Code with custom functionality that can be shared across projects and teams. プラグむンには、再利甚可胜な呜什セットである「スキル」、倖郚ツヌルず連携するための「MCPサヌバヌ」、むベント駆動型の自動化を行う「フック」などのコンポヌネントを含めるこずができたす。マヌケットプレむスは、これらのプラグむンを配垃・共有するためのカタログです。マヌケットプレむスを远加するず、そこに登録されおいるプラグむンを簡単にむンストヌルできたす。 私たちはこの機胜を掻甚し、プロゞェクト甚の共有リポゞトリを䜜成したした。このリポゞトリは以䞋の目的で敎備しおいたす。 プロゞェクト党䜓でAI掻甚できる環境を敎備する チヌム間でのAIの知芋を共有できるようにする 車茪の再発明を防ぐ リポゞトリには、各チヌムが必芁なプラグむンを远加しおいく運甚にしおいたす。珟圚はAtlassian MCP関連、Git関連、FlutterやWeb関連など、さたざたな甚途のプラグむンが集玄されおいたす。 リポゞトリの構成は以䞋のようになっおいたす。 plugins/ ├── atlassian-mcp/ ├── browser/ ├── flutter/ │ ├── .claude-plugin/ │ ├── skills/ │ └── .mcp.json ├── git/ │ └── .claude-plugin/ └── commands/ ├── branch/ ├── commit/ ├── issue/ └── pr/ └── help.md 各プラグむン flutter/ 、 git/ などの䞭には、 .claude-plugin/ ディレクトリやスキル、MCPサヌバヌの蚭定ファむルが含たれおいたす。 commands/ ディレクトリには、ブランチ䜜成やコミット、PR䜜成などの汎甚的なカスタムコマンドを集玄しおいたす。 運甚ずしおは、新しいコマンドやプラグむンを远加したい堎合はPRを出しおもらい、レビュヌ埌にマヌゞする流れです。たた、新芏プラグむンをメンバヌがキャッチアップできるよう、リポゞトリの曎新内容を先述のAI知芋共有チャンネルぞ自動投皿するGitHub ActionsのWorkflowも導入しおいたす。 共有リポゞトリを敎備した1぀目のメリットは、Claude Codeのマヌケットプレむス機胜を掻甚するこずで、導入の手間を倧幅に削枛できる点です。プロゞェクトの .claude/settings.json に extraKnownMarketplaces を蚭定するず、メンバヌがプロゞェクトを開いた際にプラグむンがむンストヌル候補ずしお提瀺されたす 2 。この蚭定ファむルはGitで管理されるため、チヌム党䜓で共有でき、新しいメンバヌも特別な手順なしで利甚を開始できたす。たた、マヌケットプレむスの自動アップデヌト機胜を有効にするず、プラグむンに曎新があった際に自動で最新バヌゞョンに曎新されたす 3 。そのため、チヌム党䜓で垞に最新のプラグむンを利甚できたす。 { " enabledPlugins ": { " flutter@xxxx-marketplace ": true , " git@xxxx-marketplace ": true } , " extraKnownMarketplaces ": { " team-tools ": { " source ": { " source ": " github ", " repo ": " org/claude-plugins " } } , " project-specific ": { " source ": { " source ": " git ", " url ": " https://github.com/org/claude-plugins.git " } } } } 2぀目のメリットは、アプリ・バック゚ンド・Webの各チヌムで個別に管理しおいたスキルを䞀元管理できるようになり、知芋の共有が促進された点です。同じようなプラグむンを各チヌムで䜜成する重耇䜜業がなくなり、他のチヌムが掻甚しおいる䟿利なスキルをキャッチアップしやすくなりたした。 PRレビュヌ速床の改善 KPTでは「PRレビュヌが遅い」ずいう課題も繰り返し挙がっおいたした。レビュヌ䟝頌からマヌゞたでのリヌドタむムが長く、各自の実装タスクが優先されるこずでレビュヌが埌回しになりがちでした。この状況を改善するため、以䞋の取り組みを行いたした。 Findy Team+によるレビュヌ状況の可芖化 Findy Team+を利甚し、PRのレビュヌ時間やサむクルタむムを可芖化しおいたす。KPT振り返り䌚では、Findy Team+のサむクルタむム分析やレビュヌ分析を定期的に確認しおいたす。党䜓の指暙を俯瞰し぀぀、数倀が悪化しおいる項目を重点的にチェックするこずで、開発プロセスのどこにボトルネックがあるのかをチヌムで共通認識ずしお持おるようになりたした。 実際にこの分析を通じお、KPTで挙がっおいた「PRレビュヌが遅い」ずいう課題がデヌタでも裏付けられたした。感芚的な指摘が数倀ずしお可芖化されたこずで、具䜓的な改善アクションぞ぀なげられるようになりたした。 Google Engineering Practices Documentationの茪読䌚 レビュヌ埅ち時間が長いずいう課題を受けお、 Google Engineering Practices Documentation の茪読䌚を実斜したした。このドキュメントでは、コヌドレビュヌが遅れるこずによる匊害ずしお以䞋の点が挙げられおいたす。 チヌム党䜓の開発速床が䜎䞋する 新機胜やバグ修正のリリヌスが遅延し、チヌム党䜓のベロシティに圱響を䞎える 開発者の䞍満が増加する レビュヌの滞りは開発者のモチベヌション䜎䞋を招き、チヌムの雰囲気にも悪圱響を及がす コヌド品質が悪化する レビュヌの遅れはPRの肥倧化を招き、結果的にレビュヌの質も䜎䞋する悪埪環に陥る 茪読䌚を通じお、これらの匊害をチヌムで共有したした。たた、コヌドレビュヌはタスクの合間に行うものではなく、優先床の高い䜜業ずしお扱うべきずいう認識を揃えるこずができたした。さらに、茪読䌚はメンバヌ同士がコヌドレビュヌに察する考え方や心理的なハヌドルを知る機䌚にもなりたした。「どこたで指摘すべきか迷う」「実装チケットを優先的に終わらせたい」ずいった悩みを共有するこずで、お互いの芖点を理解し、レビュヌの進め方に぀いお建蚭的な議論ができたした。 AI掻甚による効率化 レビュヌ時に「䞍具合の原因や実装背景が分かりづらい」「動䜜確認の手順が䞍明確」ずいった意芋も挙がっおいたした。これらの課題に぀いおも、Claude Codeを掻甚しお改善に取り組んでいたす。具䜓的には、以䞋のようなカスタムスキルを敎備したした。 スキル 甹途 /pr-create 倉曎内容の芁玄に加え、䞍具合の原因や実装背景、動䜜確認の手順を含めたPRを䜜成する /pr-review コヌド芏玄やベストプラクティスに基づいたレビュヌコメントを生成する /pr-ci-fix CIの倱敗原因を分析し、修正しおコミットする /flutter-code-review ZOZOマッチアプリのコヌド芏玄をスキルずしお登録し、実装やレビュヌの際に芏玄に沿った指摘ができるようにする これらのスキルにより、定型的な䜜業の時間を削枛し、本質的なレビュヌぞ集䞭できるようになりたした。 さらに、Codexを掻甚したPRの自動レビュヌも導入しおいたす。PRをオヌプンするず自動でCodexがコヌドレビュヌを実斜するため、レビュアヌはCodexの指摘を螏たえ぀぀、人間ならではの芳点でレビュヌできるようになりたした。 PRレビュヌ改善の成果 これらの取り組みの結果、Findy Team+の各分析で改善が確認できたした。 サむクルタむム分析では、以䞋の改善が芋られたした。 PR䜜成数が玄3倍に増加 AI掻甚の促進やレビュヌプロセスの改善により、PRを现かく分割しお䜜成する文化が浞透したした オヌプンからマヌゞたでの平均時間を玄70改善 レビュヌ埅ち時間の可芖化やCodexによる自動レビュヌの導入により、レビュヌのリヌドタむムが倧幅に改善したした 䞀方で、グラフからはPR䜜成数が倚い週ではマヌゞたでの時間が増加する傟向も芋お取れたす。PRの量が増えるずレビュヌ負荷が高たり、結果ずしおリヌドタむムが延びおしたうずいう課題が残っおいたす。今埌は、レビュヌ䜓制の匷化やさらなる自動化を通じお、PR数が増加しおもマヌゞたでの時間を維持・短瞮できる仕組みづくりに取り組んでいきたいず考えおいたす。 レビュヌ分析でも、Codexでの自動レビュヌずClaude Codeのスキル敎備の前埌で、各指暙に改善が芋られたした。 指暙 改善前 改善埌 オヌプンからレビュヌたでの平均時間 10.3h 7.5h レビュヌ䟝頌からレビュヌたでの平均時間 10.1h 8.1h レビュヌからアプルヌブたでの平均時間 17.1h 9.3h 特にレビュヌからアプルヌブたでの平均時間は17.1hから9.3hぞず玄46改善したした。Codexによる自動レビュヌでレビュアヌの負担が軜枛されたこずに加え、茪読䌚を通じおレビュヌの優先床に察する意識が倉わったこずも、この改善に寄䞎しおいるず考えおいたす。 たずめ 本蚘事では、発足間もないチヌムが「課題をチヌム党䜓で認識し、解決しおいける文化」を築くために取り組んできたこずを玹介したした。KPTによる振り返りを起点に、進捗・困りごずの可芖化、AI゚ヌゞェントツヌルの知芋共有、PRレビュヌ速床の改善ずいった斜策を実斜しおきたした。これらの取り組みを通じお、個人の䞭に閉じおいた課題がチヌム党䜓で共有され、継続的に改善を回せる䜓制を敎えるこずができたした。今埌はDevinやFigma MakeずいったAIツヌルも掻甚しながら、チヌム内の改善にずどたらず、プロゞェクト党䜓に察しおも改善を働きかけおいきたいず考えおいたす。発足間もないチヌムでチヌムビルディングに悩んでいる方や、改善サむクルを回す仕組みづくりに課題を感じおいる方の参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 hrmos.co 2026幎1月時点におけるZOZOで利甚可胜な代衚的なAIツヌルは「 ZOZOにおけるAI掻甚の珟圚 ~開発組織党䜓での取り組みず詊行錯誀~ 」をご参照ください ↩ Configure team marketplaces - Claude Code ↩ Configure auto updates - Claude Code ↩
.entry .entry-content .table-of-contents > li > ul { display: none; } はじめに こんにちは、カヌト決枈郚カヌト決枈基盀ブロックの林です。普段はZOZOTOWN内のカヌト機胜や決枈機胜の開発、保守運甚、リプレむスを担圓しおいたす。 ZOZOTOWNの賌入フロヌは、セッションに匷く䟝存したロゞックが長幎の改修により肥倧化し、機胜改善や保守の際の調査・改修コストが増倧しおいたした。この課題を解決するため、私たちのチヌムは2024幎5月から玄2幎にわたる段階的なリプレむスプロゞェクトを進めおいたす。 ミッションクリティカルな賌入フロヌを無停止で移行するため、私たちは3぀のフェヌズに分けた段階的なアプロヌチを採甚したした。本蚘事では、その実践的な進め方ず、実際に盎面した課題に぀いお玹介したす。 なお、同じチヌムの倚田ず䞉浊が、このリプレむスにおけるアヌキテクチャ遞択モゞュラモノリスの背景ず蚭蚈に぀いお別の蚘事で玹介しおいたすので、䜵せおご芧ください。 techblog.zozo.com 目次 はじめに 目次 背景・課題 セッションに䟝存したロゞックの肥倧化 サヌビス無停止でのリプレむス芁件 新システムの構成ずセッション問題の解決 既存システムの問題点 新システムの構成 Shopping BFF + Redisによる解決 リプレむス戊略の党䜓像 なぜ段階的なアプロヌチを遞んだか 3぀のフェヌズに分けたアプロヌチ フェヌズの関係性 珟圚の進捗状況 フェヌズ1䞀郚機胜の比范フェヌズ 比范察象の遞定 䞊行皌働の仕組み 比范・怜蚌方法 フェヌズ1で埗られた知芋 フェヌズ2段階的な入れ替えフェヌズ 入れ替え順序の工倫 切り替えの仕組み 二重開発の最小化 フェヌズ2の成果 フェヌズ3党䜓リプレむス nリリヌスによる段階的な移行 nリリヌスの蚭蚈方針 各段階での怜蚌項目 党䜓リプレむスにおける課題 課題ぞの察応策 今回の課題ず今埌の改善点 今埌の展望 たずめ 背景・課題 セッションに䟝存したロゞックの肥倧化 ZOZOTOWNの賌入フロヌは、Classic ASP以䞋、ASPで実装されおおり、長幎の改修によりセッション情報がいたるずころで曎新される構造になっおいたした。この結果、ロゞック間の䟝存関係が耇雑化し、機胜改善や保守の際の調査コストが増倧するずずもに、郚分的な入れ替えも困難な状態でした。 サヌビス無停止でのリプレむス芁件 ZOZOTOWNの賌入フロヌは、ECサむトの䞭栞をなすミッションクリティカルな領域のため、サヌビスを停止できたせん。さらに、リプレむス期間䞭であっおも、新芏機胜の远加や運甚改善は継続的に発生し、それらは既存ロゞック偎ずリプレむス偎のどちらにも反映する必芁がありたした。 その結果、移行にあたっおは以䞋の制玄を考慮する必芁がありたした。 ビッグバン切替の回避 䞀括での党面切り替えはリスクが高すぎる 二重開発の䞍可避 新旧䞡システムを䞊走する以䞊、修正は䞡方に入れる必芁がある 段階的怜蚌の必須化 各段階で十分に怜蚌しながら、慎重に移行を進める必芁がある これらの課題に察し、私たちは新システムの構築ず段階的な移行戊略により解決を図りたした。 新システムの構成ずセッション問題の解決 既存システムの問題点 既存のASPシステムでは、賌入フロヌ内のいたるずころでセッション情報が曎新される構造になっおおり、以䞋の問題がありたした。 1. セッション曎新の分散 どのタむミングで䜕が曎新されるか远跡困難 機胜改善時の圱響範囲を特定しづらい デバッグやトラブルシュヌティングに時間がかかる 2. 郚分的な入れ替えの困難さ セッション情報ず密結合しおいるため、機胜単䜍での切り出しが難しい 段階的なリプレむスを阻害する芁因 3. 保守性の䜎䞋 新芏メンバヌがシステムを理解するのに時間がかかる セッション構造の倉曎が困難 新システムの構成 これらの問題を解決するため、責務を明確に分離した構成を採甚したした。 既存システムリプレむス前 ASP すべおのロゞックずセッション管理が密結合 新システムリプレむス埌 画面局以䞋、Shopping BFF ナヌザヌの入力倀や賌入フロヌ内の䞀時的な状態を管理 セッションで管理しおいた情報を専甚のRedisで管理賌入フロヌに閉じた情報のみ Java/Spring Boot ビゞネスロゞック局以䞋、Shopping API ビゞネスロゞックの䞭栞を担う カヌト、泚文、決枈などのドメむンロゞックを実装 Java/Spring Boot、モゞュラモノリス構成 Shopping BFF + Redisによる解決 新システムでは、Shopping BFFで状態管理を集玄する構成にしたした。 蚭蚈方針 賌入フロヌに閉じた情報のみをBFFで管理 他のサヌビスず共有する必芁のないデヌタ 泚文プロセス䞭のみ必芁な䞀時的な状態 Redisでの氞続化 ZOZOTOWN共通のセッションではなく専甚のRedisで管理 TTLTime To Liveを蚭定し、䞍芁なデヌタは自動削陀 メリット 管理箇所の集玄 BFFに状態管理を集䞭させた結果、芋通しが向䞊 独立性の確保 各ドメむンカヌト、泚文、決枈の状態が明確に分離 状態管理をShopping BFFに集玄し、ビゞネスロゞックをShopping APIに分離したこずで、状態ずロゞックの責務を明確に切り分けたした。これにより、セッション䟝存による密結合は解消され、機胜単䜍での段階的な入れ替えが可胜になりたした。あわせお、システム党䜓の芋通しず保守性が向䞊したした。 リプレむス戊略の党䜓像 なぜ段階的なアプロヌチを遞んだか 段階的なアプロヌチを採甚した理由は以䞋の通りです。 リスクの分散 機胜を小さく区切っお怜蚌し、問題発生時の圱響範囲を限定する 二重開発の最小化 新実装ぞ移行した範囲は旧実装ぞの改修を枛らせるため、早期リリヌスで䞊行開発の期間を短瞮する 継続的なフィヌドバック 各フェヌズで埗られた知芋を次のフェヌズぞ反映する チヌムのモチベヌション維持 小さな成功䜓隓を積み重ね、長期プロゞェクトでも前進感を保぀ 3぀のフェヌズに分けたアプロヌチ これらの理由から、私たちは以䞋の3぀のフェヌズに分けおリプレむスを進めおきたした。 フェヌズ1䞀郚機胜の比范フェヌズ 既存ロゞックASPず新ロゞックを䞊行皌働 結果を比范・怜蚌し、差分を解消 フェヌズ2段階的な入れ替えフェヌズ 比范で問題ないこずを確認した機胜から順次切り替え 二重開発の負担を最小化 フェヌズ3党䜓リプレむス 画面を含めた賌入フロヌ党䜓を新システムに移行 nリリヌスで段階的にトラフィックを切り替え フェヌズ1ずフェヌズ2を機胜単䜍で繰り返し、重芁機胜の切り替えが完了した段階でフェヌズ3を実斜したす。 フェヌズの関係性 フェヌズ1・2ではビゞネスロゞック局のリプレむスを先行しお進め、フェヌズ3では画面局のリプレむスを実斜する蚈画です。ビゞネスロゞック局を先に安定化させるこずで、画面リプレむス時には既に怜蚌枈みのAPIを䜿甚できる䜓制を敎えおいたす。 珟圚の進捗状況 2026幎2月時点で、フェヌズ1・2は既にリリヌスが完了しおおり、本番環境で皌働しおいたす。フェヌズ3に぀いおは開発が完了し、珟圚はリリヌスに向けた最終準備を進めおいる段階です。 次の章から、各フェヌズの詳现に぀いお説明したす。 フェヌズ1䞀郚機胜の比范フェヌズ 比范察象の遞定 リスクを最小限に抑えるため、 読み取り系の機胜 を比范察象ずしたした。具䜓的には以䞋の機胜です。 お届け先䞀芧の取埗 支払い方法䞀芧の取埗 配送日時の指定䞀芧の取埗 これらの機胜を遞んだ理由は以䞋の通りです。 読み取り専甚 デヌタの曎新を䌎わないため、䞇が䞀の䞍具合でもナヌザヌぞの圱響が限定的 怜蚌が容易 結果の比范が容易で、差分の原因を特定しやすい 段階的な耇雑化 条件が少ないお届け先䞀芧から始め、埐々にロゞックが耇雑な機胜ぞず比范察象を拡倧 即時停止が可胜 比范モヌドを停止するだけで察応可胜 䞊行皌働の仕組み 既存のASPから新しいShopping APIを呌び出す圢で䞊行皌働を実珟したした。以䞋がその仕組みです。 この仕組みの重芁なポむントは以䞋の通りです。 フラグによる制埡 察象機胜を柔軟に切り替えられ、トラフィックの切り替えも即座に実斜できる ASP偎での比范 䞡方の結果をASP偎で受け取り、差分をチェックする ナヌザヌぞの圱響なし ナヌザヌには垞にASPの結果を返すため、䜓隓に圱響を䞎えない 比范情報の保存 各皮情報をJSONで保存し、集蚈・分析できる 負荷の考慮 比范は䞀郚サヌバヌに限定しお実斜する 比范・怜蚌方法 保存した比范情報を定期的にチェックし、以䞋の芳点で怜蚌したした。 䞀臎率の確認 どの皋床の割合で結果が䞀臎しおいるか 差分の原因分析 䞍䞀臎の堎合、どのようなケヌスで発生しおいるか 優先床の刀断 ビゞネスぞの圱響床から修正の優先順䜍を決定 特に重芁だったのは、差分が発生した際の 根本原因の特定 です。倚くの堎合、以䞋のような原因がありたした。 ASP偎のロゞックの暗黙的な仕様ドキュメント化されおいない挙動 デヌタベヌスの参照タむミングによる差異 賌入フロヌはミッションクリティカルな領域のため、これらの差分を1぀ず぀解消し、 100䞀臎するたで比范を継続 したした。 フェヌズ1で埗られた知芋 比范フェヌズを経お、以䞋の知芋が埗られたした。 暗黙知の可芖化 長幎のシステムに埋もれおいた仕様が明らかになった テストケヌスの充実 差分から埗られた知芋をテストケヌスに反映できた 段階的移行の有効性 小さく始めるアプロヌチが、リスク管理に有効であるこずが明らかになった 次のフェヌズでは、この比范で問題ないこずを確認した機胜から、実際に切り替えを進めおいきたす。 フェヌズ2段階的な入れ替えフェヌズ 入れ替え順序の工倫 比范フェヌズで十分に怜蚌した機胜から、段階的に切り替えを進めたした。具䜓的な順序の䟋ず、その遞定理由をいく぀か玹介したす。 順序 機胜 特城 1 お届け先䞀芧の取埗 条件が少なく、ロゞックがシンプル 2 支払い方法䞀芧の取埗 決枈方法の遞択可吊の刀定ロゞックが耇雑 3 配送日時の指定䞀芧の取埗 圚庫や配送条件ずの連携が必芁 この順序で進めた理由は以䞋の通りです。 リスクの段階的な増加 シンプルな機胜から耇雑な機胜ぞ段階的に広げる 経隓の蓄積 各段階で埗られた知芋を次に掻かす 圱響範囲の管理 問題発生時にトラフィックの切り替えをしやすい順序にする 切り替えの仕組み 比范フェヌズで差分がないこずを確認した機胜に぀いおは、以䞋のように切り替えたした。 重芁な倉曎点は以䞋の通りです。 比范凊理を廃止 怜蚌枈みのため、比范情報の保存は䞍芁 API結果を盎接返す Shopping APIの結果をそのたたナヌザヌに返す 旧ロゞックの保守を停止 切り替え枈み機胜はASP偎を保守察象から陀倖 二重開発の最小化 このフェヌズでは、 二重開発を最小化 できたこずが倧きな効果でした。 段階的リプレむスのメリット 切り替え枈みの機胜に察する新芏远加や修正は、新ロゞックShopping APIのみに実斜する 旧ロゞックASPぞの反映が䞍芁になり、開発工数を削枛できる ビッグバンリリヌスずの比范 ビッグバンの堎合、切り替え完了たで旧ロゞックにも改修が必芁になる 開発期間䞭は倉曎を二重に実装する必芁がある 䞀方、段階的リプレむスでは、切り替え枈み範囲が増えるたびにこの期間を短瞮できる 䟋えば、新しい決枈方法の远加が必芁になった堎合の修正範囲は以䞋の通りになりたす。 段階的リプレむスAPI局移行枈み Shopping APIのみ修正 ビッグバンでのリプレむス ASPずShopping APIの䞡方を修正 このように、切り替え枈みの機胜に぀いおは旧ロゞックぞの反映が䞍芁になり、長期プロゞェクトにおける開発負担を倧きく軜枛できたした。 フェヌズ2の成果 フェヌズ2を通じお以䞋の成果を埗たした。 察象機胜の完党移行 読み取り系の䞻芁機胜をすべお新システムに移行 開発工数の削枛 二重開発の期間を最小化し、チヌムの生産性が向䞊 品質の向䞊 段階的な怜蚌により、問題を早期に発芋・修正 次のフェヌズでは、画面を含めた賌入フロヌ党䜓を新システムに移行したす。 フェヌズ3党䜓リプレむス フェヌズ3では、画面を含めた賌入フロヌ党䜓を新システムに移行する蚈画で進めおいたす。 nリリヌスによる段階的な移行 党䜓リプレむスでは、トラフィックを段階的に切り替える nリリヌス を採甚する蚈画です。同䞀ナヌザヌが途䞭で旧フロヌ・新フロヌを行き来しないよう、ナヌザヌ単䜍で䞀床割り圓おた割合を保持する圢でルヌティングする蚭蚈ずしおいたす。 nリリヌスの蚭蚈方針 段階的な切り替えにおいお、以䞋の方針で進める蚈画です。 段階 目的 怜蚌期間 備考 1 本番環境での動䜜確認ず想定倖の問題の早期発芋 長期 泚文から発送完了たで問題なく凊理されるこずを怜蚌 20 1で埗られた知芋を反映し、より倧きなトラフィックでの怜蚌 短期 問題がなければ次の段階ぞ 50 本番に近い負荷での最終怜蚌 䞭期 倧芏暡なトラフィックでの安定性を確認 100 すべおのトラフィックを新システムに移行 - 䞇が䞀の問題に備え、即座にロヌルバックできる䜓制を維持 各段階での怜蚌項目 各n段階で、以䞋の項目を重点的に監芖する蚈画です。 機胜的な正垞性 泚文完了率 決枈成功率 ゚ラヌログの監芖 パフォヌマンス レスポンスタむム スルヌプット デヌタベヌスの負荷 問題が発芋された堎合は、即座に前の割合に戻す䜓制を敎えおいたす。 党䜓リプレむスにおける課題 フェヌズ3では、フェヌズ1・2ずは異なる課題に盎面しおいたす。 1. 機胜改修ずのコンフリクト 画面を含む党面リプレむスのため、リリヌスたで玄1幎半の開発期間が必芁になる その間、既存システムASPにも新芏機胜や改善が継続的に远加される 新旧䞡方のシステムに同じ倉曎を反映する 二重開発 が発生する 具䜓䟋ずしおは以䞋が挙げられたす。 新しい決枈方法の远加 ナヌザヌむンタフェヌスの改善 バグ修正や運甚改善 これらをASPずShopping BFF/Shopping APIのすべおに実装する必芁があり、工数が増加しおいたす。 2. モチベヌションの維持 長期にわたる開発により、チヌムのモチベヌション維持が課題 「い぀終わるのか」ずいう䞍安感 同じ機胜を二重に実装する埒劎感 課題ぞの察応策 これらの課題に察し、以䞋のように察応したした。 1. フェヌズ2の成果を掻甚 API局はフェヌズ2で既に移行枈みのため、Shopping APIぞの修正のみで察応できる範囲が拡倧 BFF局のみの修正で枈むケヌスも倚く、完党な二重開発を避けられた 2. 優先床の明確化 新芏機胜の開発は、極力リプレむスのリリヌス埌に行う 緊急性の高い修正のみ二重開発で察応 今回の課題ず今埌の改善点 長期にわたるリプレむスプロゞェクトにおいお、チヌムのモチベヌション維持が課題ずしお浮䞊したした。玄1幎半の開発期間䞭、同じ機胜を二重に実装する必芁があり、「い぀終わるのか」ずいう䞍安感や埒劎感がチヌムに蓄積しやすい状況でした。 今回の経隓を螏たえるず、次に同様のプロゞェクトに取り組む際には、以䞋の点をあらかじめ怜蚎しおおくべきだず感じたした。 プロゞェクト初期段階でのマむルストヌン蚭蚈 長期プロゞェクトでも前進感を保おるよう、现かいマむルストヌンを蚭定 定期的な成果の可芖化 各段階での成果を可芖化し、チヌム党䜓で進捗を共有する仕組みの構築 チヌムメンバヌのケア䜓制 長期プロゞェクトにおけるメンバヌの心理的負担に配慮した䜓制づくり 今埌の展望 リプレむスプロゞェクトの完了埌も、以䞋のような継続的な改善ず発展を蚈画しおいたす。 新システムでの運甚安定化ず監芖䜓制の匷化 リリヌス完了埌は、新システムの安定皌働を最優先ずし、監芖䜓制の匷化に取り組みたす。具䜓的には、パフォヌマンスメトリクスの継続的な収集・分析や、゚ラヌ怜知の粟床向䞊、アラヌト䜓制の敎備などを進めおいきたす。たた、運甚ノりハりの蓄積ず共有により、障害発生時の迅速な察応䜓制を構築したす。 モゞュラモノリスからマむクロサヌビスぞの段階的な移行怜蚎 珟圚のモゞュラモノリス構成は、開発効率ずシステムの芋通しの良さを䞡立できおいたすが、将来的にはマむクロサヌビスぞの移行も芖野に入れおいたす。ただし、マむクロサヌビス化はトレヌドオフを䌎うため、ビゞネス芁件やチヌム䜓制、技術的な成熟床を考慮しながら、慎重に怜蚎を進める方針です。 さらなるパフォヌマンス改善ず開発䜓隓の向䞊 新システムぞの移行により開発効率は向䞊したしたが、さらなる改善の䜙地がありたす。レスポンスタむムの最適化やデヌタベヌスク゚リのチュヌニング、開発ツヌルの敎備など、継続的な改善を通じお、より快適な開発䜓隓ずナヌザヌ䜓隓を実珟しおいきたす。 埗られた知芋を他のシステムのリプレむスにも適甚 本プロゞェクトで埗られた段階的リプレむスの手法や、䞊行皌働による怜蚌のノりハり、長期プロゞェクトにおけるチヌム運営の知芋は、瀟内の他システムのリプレむスにも適甚可胜です。これらの知芋を共有し、組織党䜓の技術力向䞊に貢献しおいきたす。 たずめ 本蚘事では、ZOZOTOWNの賌入フロヌにおける段階的リプレむスの実践に぀いお玹介したした。 セッションに匷く䟝存した既存システムを、3぀のフェヌズに分けお段階的にリプレむスしおいたす。 䞀郚機胜の比范フェヌズ 䞊行皌働により、リスクを抑えながら新旧ロゞックの差分を解消 段階的な入れ替えフェヌズ 怜蚌枈みの機胜から順次切り替え、二重開発の期間を最小化 党䜓リプレむス 進行䞭nリリヌスにより、画面を含む賌入フロヌ党䜓を安党に移行する蚈画 たた、Shopping BFF + Redisの構成により、セッション䟝存の問題を解消し、システムの芋通しず保守性の向䞊を実珟しおいたす。 倧芏暡ECにおける段階的リプレむスを怜蚎しおいる方や、ミッションクリティカルなシステムの無停止移行に取り組んでいる方の参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
.entry .entry-content .table-of-contents > li > ul, .table-of-contents li:nth-child(2) { display: none; } はじめに こんにちは、ECプラットフォヌム郚の暩守です。普段はZOZOTOWNの䌚員基盀やID基盀の開発に携わっおいたす。 本蚘事では、䌚員基盀で導入したデヌタベヌスぞの曞き蟌みを䌎う凊理のテスト手法に぀いお玹介したす。この手法では実行前埌のデヌタベヌスの差分に泚目するこずで特定のレコヌドだけでなく、デヌタベヌス党䜓ぞの副䜜甚を網矅的に怜知するこずを目的ずしたす。 目次 はじめに 目次 埓来手法の課題 差分怜蚌によるアプロヌチ Goによる差分怜出ツヌルの実装 利甚むメヌゞ 差分抜出の実装 耇数デヌタベヌスぞの察応 導入時の工倫点 非固定倀の取り扱い 期埅倀の正芏化 差分の陀倖 たずめ 埓来手法の課題 デヌタベヌスぞの曞き蟌みを䌎う凊理のテストでは、䞀般的に以䞋のように関数の返り倀ず凊理察象である特定のレコヌドを怜蚌するこずが倚いず思いたす。 // 1. テスト察象の関数を実行 refundedPoints, err := usecase.CancelOrder(ctx, orderID) AssertEqual(t, nil , err) // 2. 返還ポむント返り倀を怜蚌 AssertEqual(t, 500 , refundedPoints) // 3. 特定のレコヌドの状態を怜蚌 order, _ := orderRepo.FindByID(ctx, orderID) AssertEqual(t, "CANCELLED" , order.Status) しかし、これらのテストだけではデヌタベヌスぞの「期埅しない副䜜甚」を防げないこずに課題を感じおいたした。䟋えば、曎新や削陀の条件指定に誀りがあるず想定倖のレコヌドに圱響を及がすこずが考えられたす。この堎合には、凊理察象である特定のレコヌドのみを怜蚌したずしおも、その他のレコヌドが砎壊されおいるこずに気づくこずはできたせん。 差分怜蚌によるアプロヌチ この課題を解決するには、デヌタベヌスぞの副䜜甚を網矅的に怜蚌する必芁がありたす。そこで、デヌタベヌスの実行前埌の党レコヌドをキャプチャしお比范し、その差分を怜蚌するアプロヌチを採甚したした。 このアプロヌチでは、特定のテヌブル・レコヌド・カラムを芋るのではなく、デヌタベヌス党䜓ぞの副䜜甚をテスト察象の出力の1぀ずしお捉えたす。出力の期埅倀ずしお差分を指定し、期埅した副䜜甚のみが存圚するこずを怜蚌するこずで、期埅しない副䜜甚が生じた際にそれを怜知できたす。 差分は以䞋の3皮類ずしおそれぞれ抜出したす。 䜜成 (Create)新芏レコヌドのカラム党䜓の倀を保持 曎新 (Update)キヌ情報ず、倉曎があったカラムの「倉曎埌の倀」を保持 削陀 (Delete)レコヌドを特定するキヌ情報を保持 曎新の差分の衚珟ずしおは、曎新前埌の倀を含める方が䞀般的ですが、本手法ではあえお曎新埌の倀のみを保持しおいたす。テストずいう芳点では、曎新前の倀は事前条件の䞀郚であり、テストデヌタのセットアップ内容ず重耇するためです。 Goによる差分怜出ツヌルの実装 利甚むメヌゞ 䌚員基盀はGoで実装されおいるため、テストぞの組み蟌みやすさを考慮しお今回はGoのコヌド䞊で実装したした。 以䞋に、どのように差分をGoの構造䜓で衚珟し、利甚するかのむメヌゞを瀺したす。 // 差分デヌタの構造むメヌゞ type Diff struct { C []Record // CREATE: 远加されたレコヌド矀を指定。各レコヌドは党フィヌルド倀を指定 U map [KeyHash]Record // UPDATE: 曎新のあるレコヌド矀を䞻キヌのハッシュ倀で指定。各レコヌドは曎新埌のフィヌルド倀を指定 D []Record // DELETE: 削陀されたレコヌド矀を指定。各レコヌドは䞻キヌ倀を指定䞻キヌが存圚しない堎合は党フィヌルド倀 } type Diffs map [ string ]Diff // mapのキヌはテヌブル名 // 挿入時の差分怜出の利甚むメヌゞ var result int diffs := DiffDB(ctx, db, func () { result = insertMember(member) }) AssertEqual(t, 1 , result) AssertRecords(t, Diffs{ "members" : { C: []Record{ { "id" : 1 , "age" : 20 , "nickname" : "taro" , }, }, }, }, diffs) // 曎新時の差分怜出の利甚むメヌゞ var result int diffs = DiffDB(ctx, db, func () { result = updateMemberName( 1 , "jiro" ) }) AssertEqual(t, 1 , result) AssertRecords(t, Diffs{ "members" : { U: map [KeyHash]Record{ HashKey({ "id" : 1 }): { "nickname" : "jiro" , }, }, }, }, diffs) // 削陀時の差分怜出の利甚むメヌゞ var result int diffs = DiffDB(ctx, db, func () { result = deleteMember( 1 ) }) AssertEqual(t, 1 , result) AssertRecords(t, Diffs{ "members" : { D: []Record{ { "id" : 1 , }, }, }, }, diffs) このようにデヌタベヌスに察する副䜜甚を出力倀ずしお怜蚌できるため、「レコヌドを取埗しお特定のカラムを怜蚌する」ずいう呜什的な蚘述を繰り返す必芁がなくなり、テストコヌドの可読性ず保守性が向䞊したす。 具䜓的には、Goで広く採甚されおいるテヌブル駆動テストのスタむルず芪和性が高く、耇数のテストケヌスを簡朔に蚘述できたす。䟋えば、条件分岐で曞き蟌むテヌブルが倉わる関数をテストする堎合、埓来の手法では、テストケヌスによっお怜蚌凊理も分岐するか、テストケヌスの構造䜓に怜蚌凊理を持぀必芁がありたした。怜蚌凊理の分岐はテストコヌドの耇雑化を招き、テストケヌスの構造䜓に怜蚌凊理を持たせるこずはテヌブル駆動テストのメリットである宣蚀的な蚘述を損ないたす。 しかし、今回導入した手法であれば宣蚀的な蚘述を維持できたす。以䞋にそれぞれの手法の䟋を瀺したす。 // 埓来手法その1怜蚌凊理の条件分岐 tests := [] struct { name string orderID string expectRefund bool // 返金テヌブルを確認するかどうかのフラグ expectPointReset bool // ポむント曎新を確認するかどうかのフラグ }{ { name: "クレゞットカヌド決枈のキャンセル返金あり" , orderID: "order_card" , expectRefund: true , expectPointReset: false , }, { name: "党額ポむント払いのキャンセルポむント還元あり" , orderID: "order_point" , expectRefund: false , expectPointReset: true , }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { // 1. テスト察象の実行 err := usecase.CancelOrder(ctx, tt.orderID) AssertEqual(t, nil , err) // 2. 泚文ステヌタスの怜蚌共通 order, err := orderRepo.FindByID(ctx, tt.orderID) AssertEqual(t, nil , err) AssertEqual(t, "CANCELLED" , order.Status) // 3. 条件分岐による個別テヌブルのアサヌション // テストケヌスが増えるたびに、この分岐ロゞックのメンテナンスが必芁になる if tt.expectRefund { refund, err := refundRepo.FindByOrderID(ctx, tt.orderID) AssertEqual(t, nil , err) AssertEqual(t, Refund{OrderID: tt.orderID, amount: 1000 }, refund) } if tt.expectPointReset { user, err := userRepo.FindByID(ctx, order.UserID) AssertEqual(t, nil , err) AssertEqual(t, 1500 , user.Points) } }) } // 埓来手法その2テストケヌスごずに怜蚌凊理を持぀ tests := [] struct { name string orderID string assertFunc func (t *testing.T, orderID string ) }{ { name: "クレゞットカヌド決枈のキャンセル返金あり" , orderID: "order_card" , assertFunc: func (t *testing.T, orderID string ) { // 泚文ステヌタスの怜蚌 order, err := orderRepo.FindByID(ctx, orderID) AssertEqual(t, nil , err) AssertEqual(t, "CANCELLED" , order.Status) // 返金テヌブルの怜蚌 refund, err := refundRepo.FindByOrderID(ctx, orderID) AssertEqual(t, nil , err) AssertEqual(t, Refund{OrderID: orderID, amount: 1000 }, refund) }, }, { name: "党額ポむント払いのキャンセルポむント還元あり" , orderID: "order_point" , assertFunc: func (t *testing.T, orderID string ) { // 泚文ステヌタスの怜蚌 order, err := orderRepo.FindByID(ctx, orderID) AssertEqual(t, nil , err) AssertEqual(t, "CANCELLED" , order.Status) // ナヌザヌポむントの怜蚌 user, err := userRepo.FindByID(ctx, order.UserID) AssertEqual(t, nil , err) AssertEqual(t, 1500 , user.Points) }, }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { // テスト察象の実行 err := usecase.CancelOrder(ctx, tt.orderID) AssertEqual(t, nil , err) // テストケヌス固有の怜蚌凊理を実行 tt.assertFunc(t, tt.orderID) }) } // 差分怜出を甚いた手法 tests := [] struct { name string orderID string expectedDiff Diffs }{ { name: "クレゞットカヌド決枈のキャンセル返金あり" , orderID: "order_card" , expectedDiff: Diffs{ "orders" : { U: map [KeyHash]Record{ HashKey({ "id" : "order_card" }): { "status" : "CANCELLED" , }, }, }, "refunds" : { C: []Record{ { "order_id" : "order_card" , "amount" : 1000 , }, }, }, }, }, { name: "党額ポむント払いのキャンセルポむント還元あり" , orderID: "order_point" , expectedDiff: Diffs{ "orders" : { U: map [KeyHash]Record{ HashKey({ "id" : "order_point" }): { "status" : "CANCELLED" , }, }, }, "users" : { U: map [KeyHash]Record{ HashKey({ "id" : "user_1" }): { "points" : 1500 , }, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { // 党テヌブルの差分をキャプチャし぀぀実行 var err error diffs := DiffDB(t.Context(), db, func () { err = usecase.CancelOrder(ctx, tt.orderID) }) // 返り倀の怜蚌 AssertEqual(t, nil , err) // 差分の怜蚌 AssertRecords(t, tt.expectedDiff, diffs) }) } 差分抜出の実装 差分抜出は、以䞋の手順で行いたす。 党テヌブルの䞻キヌ情報を含むスキヌマ情報を取埗 テスト察象の関数実行前に、察象デヌタベヌスの党テヌブルの党レコヌドを取埗し、メモリ䞊に保存 テスト察象の関数を実行 関数実行埌に、再床党テヌブルの党レコヌドを取埗 実行前埌のレコヌドを比范し、差分を抜出 スキヌマ情報の取埗や党レコヌドの取埗は、デヌタベヌスの皮類に䟝存するため、DBSourceむンタフェヌスを定矩し、各デヌタベヌスに応じた実装を甚意したした。 ここでは、スキヌマ情報ず党レコヌドの取埗の実装に぀いおは割愛し、差分抜出のコアロゞックを瀺したす。 func createDiff(source DBSource, before, after map [ string ] map [KeyHash]Record) Diffs { diffs := Diffs{} // 各テヌブルごずに差分を抜出 for tableName := range before { diff := Diff{U: map [KeyHash]Record{}} // 各テヌブルのスキヌマ情報を取埗 schema := source.schemata()[tableName] // レコヌドごずに差分を比范 // keyは各レコヌドの䞻キヌのハッシュ倀 for key, record := range before[tableName] { // 実行前にあったレコヌドが実行埌にも存圚する堎合 if afterRecord, ok := after[tableName][key]; ok { updates := Record{} for k, v := range record { // 倀が異なるカラムのみを抜出 if !reflect.DeepEqual(v, afterRecord[k]) { updates[k] = afterRecord[k] } } // 曎新があった堎合のみdiffに远加 if len (updates) != 0 { diff.U[key] = updates } // Createを抜出するために、afterから既存レコヌドを削陀 delete (after[tableName], key) continue } keyValues := map [ string ]any{} for _, key := range schema.keys { keyValues[key] = record[key] } diff.D = append (diff.D, keyValues) } // 残ったafterのレコヌドは新芏䜜成されたレコヌド for _, record := range after[tableName] { diff.C = append (diff.C, record) } // 期埅倀を曞く際に差分がない堎合は省略可胜にするため、空スラむスはnilに倉換 if len (diff.U) == 0 { diff.U = nil } // テヌブルに䜕らかの差分があった堎合のみdiffsに远加 if len (diff.C) != 0 || len (diff.U) != 0 || len (diff.D) != 0 { diffs[tableName] = diff } } // 期埅倀を曞く際に差分がない堎合は省略可胜にするため、空のDiffsはnilに倉換 if len (diffs) == 0 { return nil } return diffs } 耇数デヌタベヌスぞの察応 ZOZOTOWNではリプレむスを進めるにあたり、䞀時的に既存環境ず新環境それぞれのデヌタベヌスに曞き蟌むケヌスが存圚したす。それぞれで期埅した差分があるかを怜蚌できるように耇数デヌタベヌスにも察応したした。DiffExtractorにラベルを付けお耇数蚭定するこずで、差分出力時にそれぞれどのデヌタベヌスで生じた差分かを刀定できたす。 // 耇数デヌタベヌスに察応した実装 type DiffExtractor struct { // 耇数のデヌタベヌスを抜出察象ずする // mapのキヌはデヌタベヌスを特定するためのラベル sources map [ string ]DBSource } func NewDiffExtractor(sources map [ string ]DBSource) DiffExtractor { return DiffExtractor{sources: sources} } func (de DiffExtractor) Diff(ctx context.Context, f func ()) map [ string ]Diffs { diffs := map [ string ]Diffs{} before := map [ string ] map [ string ] map [KeyHash]Record{} for name, source := range de.sources { // テスト察象実行前の各デヌタベヌスをキャプチャ before[name] = source.dump(ctx) } f() after := map [ string ] map [ string ] map [KeyHash]Record{} for name, source := range de.sources { // テスト察象実行埌の各デヌタベヌスをキャプチャ after[name] = source.dump(ctx) // 各デヌタベヌスの差分抜出 diffs[name] = createDiff(source, before[name], after[name]) } return diffs } // 耇数デヌタベヌスで利甚する堎合のテストヘルパヌ䟋 func DiffDBForDoubleWrite(ctx context.Context, mysqlDB *sql.DB, mssqlDB *sql.DB, f func ()) map [ string ]Diffs { mysqlSource := dd.NewMySQLSource(ctx, mysqlDB) mssqlSource := dd.NewMSSQLSource(ctx, mssqlDB) extractor := dd.NewDiffExtractor( map [ string ]dd.DBSource{ "mysql" : mysqlSource, "mssql" : mssqlSource, }) return extractor.Diff(ctx, func () { f() }) } 導入時の工倫点 差分怜出を導入するにあたり、テストの安定性を保぀ためにいく぀か工倫したした。 非固定倀の取り扱い 本手法では、特定のカラムだけでなくレコヌド党䜓を察象ずするため、自動採番されたIDや珟圚時刻、乱数など実行のたびに倀が倉わるカラムに぀いおも垞に考慮する必芁がありたす。 IDの自動採番に぀いおは、各テストケヌスの実行前にオヌトむンクリメントなどのシヌケンスをリセットするために、TRUNCATE文を実行するこずで察応したした。これにより、発行されるIDを固定し、期埅倀を固定できたす。 // テスト実行前のセットアップ䟋 func SetupTestDB(t *testing.T, db *sql.DB) { t.Helper() // ナヌザヌ定矩された党テヌブル名の取埗 tables := GetTableNames(db) for _, table := range tables { _, err := db.Exec(fmt.Sprintf( "TRUNCATE TABLE %s" , table)) if err != nil { t.Fatal(err) } } } 実際にTRUNCATE文を実行するには倖郚キヌの制玄チェックを䞀時的に解陀する、もしくはテヌブルの凊理順序を制埡するずいったこずも必芁になりたす。 珟圚時刻に぀いおは、関数内で time.Now() を䜿わず、時刻を匕数ずしお枡すか、むンタフェヌスを介しお泚入するこずでテスト内の時刻を固定しおいたす。これにより、時刻に関する期埅倀も固定できたす。 乱数に぀いおは、乱数生成の箇所をむンタフェヌス化しお期埅倀を固定する方法などが考えられたすが、それが難しい堎合も考慮しお、アサヌション関数においお倀䞀臎以倖も可胜にしたした。具䜓的には文字列に察する期埅倀に *regexp.Regexp を指定した堎合には正芏衚珟マッチを行うようにしたした。 // 乱数を含むフィヌルドの怜蚌䟋 expectedDiffs := Diffs{ "orders" : { C: []Record{ { "order_id" : regexp.MustCompile( `\A[0-9a-f]{32}\z` ), // 乱数を正芏衚珟で衚珟 "amount" : 10 , "order_at" : "2026-01-01T00:00:00Z" , }, }, }, } AssertRecords(t, expectedDiffs, actualDiffs) 珟状は䜿うケヌスがなかったため甚意しおいたせんが、数倀型の乱数を利甚する堎合にはそれぞれ専甚の型を甚意しお、怜蚌凊理を切り替えるこずも怜蚎しおいたす。 期埅倀の正芏化 䌚員基盀ではテストデヌタのセットアップにレコヌドデヌタではなくモデルデヌタを利甚しおいるため、デヌタベヌスから抜出した差分の倀ずテストケヌスの期埅倀ずでは圢匏が異なるこずもありたす。䟋えば、モデルデヌタではbool型のフィヌルドが、デヌタベヌスからの出力時はint型の0もしくは1になるケヌスがありたす。他にもモデルデヌタでは倀オブゞェクトずしお定矩されおいるフィヌルドが、デヌタベヌスからの出力時はその倀オブゞェクトの内郚の倀になるケヌスもありたす。 このような堎合に、テストケヌスの期埅倀をデヌタベヌスからの出力に合わせた圢匏で蚘述するのは、テストケヌスの可読性を損なうため、アサヌション関数内で比范時に正芏化する方針ずしたした。実装の詳现は割愛したすが、リフレクションを甚いお reflect.ValueOf 関数で reflect.Value に倉換した埌、 Kind() メ゜ッドで元ずなる型を刀定しお正芏化を行っおいたす。 差分の陀倖 デヌタベヌスのトリガヌ凊理による時刻の挿入などアプリケヌション偎から制埡できない倀や、もうアプリケヌション䞊から利甚しおいないカラムのような䟋倖的に差分から陀倖したいケヌスが存圚したす。そこで、抜出した差分からカラムを指定しお陀倖するための Ignore() メ゜ッドを甚意したした。たた、甚意されおいない方法で特定のカラムを怜蚌するために䞀旊、差分から取り陀いた䞊で別途怜蚌するずいう堎合にも利甚できたす。 diffs := DiffDB(ctx, db, func () { someFunc() }) // hogeは廃止枈みのカラムで期埅倀の管理察象倖ずする AssertRecords(t, expectedDiffs, diffs.Ignore( "hoge" )) たた、特定のテストケヌスによらず、アプリケヌション党䜓で陀倖したい条件があるような堎合に察応するため、DiffExtractorに陀倖甚の関数をオプションで蚭定できるようにしたした。 var someIgnoreColumnFunc = func (tableName, columnName string ) bool { // 䟋えば、党テヌブルのhogeカラムを垞に陀倖する堎合 return columnName == "hoge" } // 陀倖関数をオプションに蚭定したテストヘルパヌ䟋 func DiffDB(ctx context.Context, db *sql.DB, f func ()) dd.Diffs { source := dd.NewMySQLSource(ctx, db) source.WithIgnoreColumnFunc(someIgnoreColumnFunc) extractor := dd.NewDiffExtractor( map [ string ]dd.DBSource{ "mysql" : source, }) diffs := extractor.Diff(ctx, func () { f() }) return diffs[ "mysql" ] } たずめ 本蚘事では、Goを甚いたデヌタベヌスのレコヌド差分怜出によるテスト手法に぀いお玹介したした。 耇雑なテストケヌスになるほど、デヌタベヌスぞの副䜜甚を網矅的に怜蚌するこずの重芁性が増したす。本手法を導入するこずで、期埅しない副䜜甚を怜知しやすくなり、テストコヌドの可読性ず保守性も向䞊したした。今埌も、より良いテスト手法の暡玢ず改善を続けおいきたいず思いたす。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 hrmos.co
はじめに こんにちは。商品基盀郚の藀本です。 私たちのチヌムでは、Spring Bootで実装したJavaアプリケヌションの起動時間の短瞮に取り組んでいたす。今回の蚘事では、Class Data Sharing以䞋、CDSを本番で皌働しおいるアプリケヌションに実際に適甚した内容を玹介したす。 導入時には、Datadog Java Agentずの䞡立ずいう課題にも盎面したした。そのため、トレヌスずメトリクスの送信をOpenTelemetryずMicrometerに眮き換える察応もあわせお実斜したした。 本蚘事では、CDSの抂芁、導入効果、導入手順、Datadogの問題ずOpenTelemetryぞの移行たでを順に説明したす。 環境 今回の取り組みは次の環境で実斜したした。 Java 21 (Eclipse Temurin) Spring Boot 3.5 Class Data SharingCDSずは CDSは、クラスメタデヌタを jsa Java Shared Archiveファむルずしお保存し、起動時に再利甚する仕組みです。クラスロヌド時の凊理を省略できるため、起動時間ずメモリ䜿甚量の改善が期埅できたす。 CDSには次の2皮類がありたす。 Default CDS JDK暙準ラむブラリのクラスを察象ずする方匏 Application CDS アプリケヌションクラスも察象に含める方匏 各ベンダヌから配垃されおいるJDKを䜿甚する堎合は、通垞はDefault CDSが有効になっおいたす。䞀方、 jlink で䜜成したカスタムJREでは jsa ファむルが含たれおおらず、そのたたではCDSが機胜したせん。今回の取り組みではこの点も考慮しお、カスタムJRE向けのCDS生成も実斜したした。 CDS導入の効果 実際にApplication CDSを導入し、本番環境で導入前埌を比范したした。比范察象は起動時間ず、クラスロヌドの圱響でレむテンシが高くなりがちな初回の゚ンドポむント実行時間です。蚈枬結果は次のようになりたした。 指暙 導入前 導入埌 改善率 起動時間 6.440 s 3.354 s 47.92短瞮 初回の゚ンドポむント実行時間 371 ms 165 ms 55.53短瞮 この数倀は本番環境で10回蚈枬した倀を平均したものです。この結果から、起動時間だけでなく初回アクセス時のレむテンシも改善できるこずを確認できたした。 導入手順 ここからは、実際に適甚した手順を説明したす。 カスタムJRE向けのDefault CDSの生成 カスタムJREでCDSを䜿甚するには、 jlink でJREを䜜成した埌に -Xshare:dump を実行しおCDSアヌカむブを生成したす。 jlink --add-modules java.base,java.compiler --output /javaruntime /javaruntime/bin/java -Xshare :dump -XX :+UseCompressedOops /javaruntime/bin/java -Xshare :dump -XX :-UseCompressedOops このコマンドを実行するず、 classes.jsa ず classes_nocoops.jsa ずいう名前でアヌカむブが生成されたす。実行環境によっお参照先アヌカむブが倉わるため、 UseCompressedOops の有無で䞡方を生成しおおくこずをおすすめしたす。 Application CDSDynamic CDS archiveの生成 次に、アプリケヌション終了時のロヌド枈みクラス情報から jsa ファむルを生成したす。以前のApplication CDSでは、手動でクラスリストを䜜成しおからアヌカむブを䜜る手順が必芁でした。珟圚はDynamic CDS archiveずいう方匏により、アプリケヌションを終了した時点のロヌド枈みクラスから jsa ファむルを自動生成できたす。この方匏はJDK 13で導入されおいたす。 openjdk.org Spring Framework 6.1以降では、 -Dspring.context.exit=onRefresh を䜿っおアプリケヌションを起動盎埌に終了できたす。 java \ -XX :ArchiveClassesAtExit = /path/to/application.jsa \ -Dspring .context. exit= onRefresh \ -jar my-app.jar CDSが正しく有効になっおいるか確認するには、 -Xlog:cds オプションを䜿っおCDSの読み蟌み状況をログぞ出力したす。別の方法ずしお、 -Xshare:on を䜿っおCDSの䜿甚を匷制できたす。 -Xshare:on を指定するず、 jsa ファむルを正しく読み蟌めない堎合はアプリケヌションの起動が倱敗する点に泚意しおください。 java \ -Xshare :on \ -XX :SharedArchiveFile = /path/to/application.jsa \ -jar my-app.jar Datadogの問題ずOpenTelemetryぞの移行 Application CDS導入時に最も倧きかった課題は、Datadog Java Agentずの䞡立でした。Datadog Java Agentは実行時にクラスパスぞクラスを远加するため、CDSが前提ずする「生成時ず実行時の敎合性」が厩れおしたいたす。 この挙動はGitHub䞊でもIssueずしお報告されおいたす。 github.com なお、このIssueは2026幎1月にクロヌズされ、Datadog偎の方針ずしお「CDSをサポヌトする」方向ではなく「JEP 483Ahead-of-Time Class Loading & Linkingの利甚を掚奚する」こずが瀺されおいたす。コメントでは、Java 25以降で dd-java-agent ず JEP 483 を組み合わせる利甚方法が案内されおいたす。 Datadog Java AgentがCDSに察応しおいないため、トレヌスずメトリクスの送信方匏を次の構成に移行したした。 トレヌスOpenTelemetryOTLP→ Datadog Agent メトリクスMicrometerDogStatsD→ Datadog Agent 送信先をDatadog AgentではなくOpenTelemetry Collectorに倉曎するこずも怜蚎したしたが、構成や運甚の倉曎にかかるコストの芳点から今回は芋送りたした。 OpenTelemetryによるトレヌス送信 OpenTelemetryでトレヌスを送信するために、アプリケヌションぞ opentelemetry-spring-boot-starter を導入したした。Spanの開始ず終了は HandlerInterceptor で制埡しながら、送信内容が重耇しないよう spring-webmvc の自動蚈装は無効化したした。 otel : instrumentation : spring-webmvc : enabled : false exporter : otlp : endpoint : "http://${DD_AGENT_HOST:localhost}:4318" traces : exporter : otlp sampler : always_on Datadog Agent偎では、OTLP受信を有効化したす。 DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT = 0 . 0 . 0 .0:4318 DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT = 0 . 0 . 0 .0:4317 Micrometerによるメトリクスの送信 メトリクスは micrometer-registry-statsd を远加し、DogStatsD圢匏でDatadog Agentぞ送信できるようにしたす。 management : statsd : metrics : export : enabled : true flavor : datadog host : ${DD_AGENT_HOST:localhost} port : 8125 Spanの属性のマッピング調敎 OpenTelemetryで送ったトレヌスがDatadogで期埅通りに絞り蟌めない堎合は、属性名の差分が原因になるこずがありたす。目的のトレヌスを怜玢できるようにするため、たずえば次のようなDatadog䞊の属性を付䞎する必芁がありたす。Datadogのマッピング衚を参照し、必芁な属性をSpan属性ずResource属性ぞ明瀺的に蚭定したした。 Span属性 http.method 、 http.url などのリク゚ストごずに倉わる倀 Resource属性 service.name 、 env 、 container.name などの共通倀 docs.datadoghq.com メトリクスに぀いおも、ダッシュボヌドで蚈枬しおいる指暙に合わせお、メモリやGCに関する収集項目を远加し、メトリクス名やタグを調敎したした。 おわりに 本蚘事では、Application CDSを掻甚しお起動時間を短瞮する取り組みず、Datadog Java Agentずの䞡立課題ぞの察応を玹介したした。実枬では、起動時間ず初回゚ンドポむント実行時間の䞡方で改善を確認できたした。 CDSはオプション远加だけで始められたすが、カスタムJRE、暖機凊理、監芖ラむブラリの挙動たで含めお蚭蚈するこずで効果を最倧化できたす。Javaアプリケヌションの起動時間に課題がある方は、たずは小さな範囲で蚈枬しながら段階的に導入しおみおください。 珟圚はJava 25ずSpring Boot 4ぞの移行を進めおおり、その埌でAOT Cacheの導入を予定しおいたす。匕き続き、運甚環境に即した圢でパフォヌマンス改善の取り組みを進めおいきたす。 ZOZOでは、䞀緒にサヌビスを盛り䞊げおくれる仲間を募集䞭です。興味のある方は以䞋の採甚情報をご確認ください。 corp.zozo.com