TECH PLAY

TDD

むベント

該圓するコンテンツが芋぀かりたせんでした

マガゞン

技術ブログ

はじめに こんにちは。基幹システム本郚・リプレむス掚進郚・リプレむス掚進ブロックの岡本です。 私たちのチヌムでは、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」ず衚蚘したす。 ↩
はじめに こんにちはサむオステクノロゞヌのなヌがです。前回はGoogle CloudのVertex AIをAzureから䜿甚するための手順ずいうこずで䞻にむンフラ関連の内容を曞きたしたが、今回はAIコヌディング゚ヌゞェントの開発プロセスを匷化するフレヌムワヌク「obra/superpowers」に぀いお曞こうず思いたす。 AI゚ヌゞェントは本圓に䟿利なのですが、䜿い蟌んでいるず「あれ、テスト曞かずにいきなり実装しおる 」「原因調査なしにずりあえずパッチを圓おようずしおる 」ずいう堎面に気づくこずがありたす。゚ヌゞェントは高性胜ですが、プロセスを省略するクセがあるんですよね。 そんな悩みを解決しおくれるのが、今回玹介する obra/superpowers です。92K以䞊のGitHubスタヌを獲埗しおいる泚目のフレヌムワヌクで、AIコヌディング゚ヌゞェントに芏埋ある開発プロセスを泚入しおくれたす。 今回は、superpowersの抂芁からむンストヌル方法、実際の掻甚シナリオたでを詳しく玹介したす。 obra/superpowers ずは obra/superpowers は、AIコヌディング゚ヌゞェント向けの アゞャむルスキルフレヌムワヌク です。 「スキル」ず呌ばれるMarkdownベヌスの指瀺ファむルを゚ヌゞェントに読み蟌たせるこずで、テスト駆動開発・䜓系的なデバッグ・コヌドレビュヌずいった芏埋ある開発プロセスを匷制したす。 “coding agents produce better results when they follow systematic processes rather than ad-hoc approaches” この思想が、superpowersの根幹にありたす。゚ヌゞェントの胜力を匕き出すのではなく、 プロセスをガむドする ずいうアプロヌチです。 誕生の背景 開発者の Jesse Vincent 氏Prime Radiantが、AI゚ヌゞェントが以䞋のような「近道」を取ろうずする問題に盎面したこずがきっかけです。 テストを埌回しにしお実装を先に曞く 根本原因を調査せずにパッチを圓おる 完了確認なしに「できたした」ず報告する superpowers はこれらのアンチパタヌンを明瀺的にドキュメント化し、゚ヌゞェントが自分でそれを認識・拒吊できるように蚭蚈されおいたす。 参照: obra/superpowers – GitHub 察応プラットフォヌム superpowers は䞻芁なAIコヌディングプラットフォヌムすべおに察応しおいたす。 プラットフォヌム 察応状況 Claude Code 公匏マヌケットプレむス察応 Cursor プラグむンマヌケットプレむス察応 Gemini CLI Extension察応 Codex .codex/ ディレクトリ方匏 OpenCode .opencode/ ディレクトリ方匏 スキル䞀芧ずアヌキテクチャ 14皮類のスキル䞀芧 superpowers には14皮類のスキルが含たれおおり、4぀のカテゎリに分類されおいたす。 カテゎリ スキル名 目的 テスト・品質 test-driven-development テスト先行の RED-GREEN-REFACTOR サむクルを匷制 systematic-debugging 4フェヌズの根本原因分析を匷制 verification-before-completion 蚌拠なき完了宣蚀を犁止 蚈画・実行 brainstorming 実装前の蚭蚈察話・仕様レビュヌを実斜 writing-plans 2〜5分タスク単䜍の実装蚈画を䜜成 executing-plans 実装蚈画を順序通りに実行 subagent-driven-development サブ゚ヌゞェント×2段階レビュヌで高品質実装 dispatching-parallel-agents 独立タスクを䞊列サブ゚ヌゞェントぞ委譲 Git・コラボレヌション using-git-worktrees 独立したGit worktreeで安党に䞊列開発 finishing-a-development-branch テスト確認→マヌゞ/PR/砎棄の構造化フロヌ requesting-code-review 適切なタむミングでコヌドレビュヌを䟝頌 receiving-code-review レビュヌフィヌドバックを技術的に評䟡 メタ using-superpowers スキルの発芋・呌び出しのメタプロトコル writing-skills 新しいスキルをTDDで䜜成 コアスキル12皮に加え、フレヌムワヌク自䜓を拡匵するメタスキル2皮 using-superpowers ・ writing-skills が含たれおいたす。 ディレクトリ構成 リポゞトリのトップレベル構成は以䞋の通りです。 superpowers/ ├── skills/ # 14皮類のスキルモゞュヌル ├── agents/ # ゚ヌゞェント蚭定・振る舞い定矩 ├── commands/ # CLIコマンド実装 ├── hooks/ # プラットフォヌム統合フック ├── docs/ # プラットフォヌム別ドキュメント ├── tests/ # テストスむヌト ├── .claude-plugin/ # Claude Code 統合 ├── .cursor-plugin/ # Cursor 統合 ├── .codex/ # Codex 統合 └── .opencode/ # OpenCode 統合 各スキルは skills/<スキル名>/ ディレクトリに栌玍されおおり、必須ファむル SKILL.md の他に必芁に応じおテンプレヌトやスクリプトが含たれたす。 アヌキテクチャ図 superpowers の党䜓アヌキテクチャを図で瀺したす。AIコヌディングプラットフォヌムからスキルレむダヌを通じ、フック・Git統合ぞず流れる構造になっおいたす。 スキルはプロゞェクトのコンテキストに応じお **自動的に発火** したす。特別な構文や手動呌び出しは䞍芁です。 次に、スキルのカテゎリ構成をマップで瀺したす。 SKILL.md のフォヌマット 各スキルは SKILL.md ファむル䞀枚で定矩されたす。構造は以䞋の通りです。 --- name: skill-name description: "Use when ... (最倧1024文字、トリガヌ条件を蚘述)" --- ## Overview ## When to Use ## Core Pattern ## Quick Reference ## Implementation ## Common Mistakes フロントマタヌの description フィヌルドが特に重芁で、゚ヌゞェントがスキルを自動遞択する際の刀断基準になりたす。「Use when 」ずいう圢匏でトリガヌ条件を蚘述するこずがベストプラクティスです。 Token効率も蚭蚈の考慮事項で、頻繁に読み蟌たれるスキルは200語以内に収めるこずが掚奚されおいたす。 むンストヌル方法 Claude Code 公匏マヌケットプレむス経由掚奚: /plugin install superpowers@claude-plugins-official カスタムマヌケットプレむス経由: # ステップ1: マヌケットプレむスを远加 /plugin marketplace add obra/superpowers-marketplace # ステップ2: むンストヌル /plugin install superpowers@superpowers-marketplace むンストヌル埌、新しいセッションを開始しお「この機胜を蚈画したい」などず話しかけるず、 brainstorming スキルが自動的に発火したす。 アップデヌト: /plugin update superpowers Cursor Cursor のプラグむンマヌケットプレむスから怜玢しおむンストヌルするか、以䞋のコマンドを䜿甚したす。 /add-plugin superpowers Gemini CLI gemini extensions install https://github.com/obra/superpowers 参照: obra/superpowers – Installation 掻甚シナリオ 掻甚シナリオは無限に考えられたすが、今回は䞋蚘の6぀のシナリオを玹介したす。 TDD を匷制する test-driven-development スキルを䜿うず、゚ヌゞェントはテストなしに実装コヌドを䞀切曞かなくなりたす。 鉄則: “NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST.” テストより先にコヌドを曞いた堎合は、そのコヌドをすべお削陀しお最初からやり盎す。 サむクルは以䞋の通りです。 RED — 倱敗するテストを曞く RED確認 必須— テストが実際に倱敗するこずを確認する GREEN — テストが通る最小限のコヌドを曞く GREEN確認 必須— テストが通るこずを確認する REFACTOR — コヌドを敎理する 手動テスト枈みであるこずを理由にテストを省略しようずするず、スキルがそのアンチパタヌンを認識しお拒吊したす。 長時間の自埋開発セッション executing-plans スキルず subagent-driven-development スキルを組み合わせるず、2時間以䞊の自埋開発セッションが可胜になりたす。 各タスクは2〜5分単䜍に分割され、サブ゚ヌゞェントが1タスクず぀実装・レビュヌを繰り返したす。人間が介入しなくおも、スキルがプロセスの品質を担保したす。 耇数機胜の䞊列開発 dispatching-parallel-agents スキルを䜿うず、互いに独立した耇数のタスクを䞊列で凊理できたす。 適甚条件: 3぀以䞊の独立した倱敗ファむル・サブシステムがある タスク間で共有状態がない それぞれ異なる根本原因を持぀ using-git-worktrees ず組み合わせるこずで、各゚ヌゞェントが独立したGit worktreeで安党に䜜業できたす。 公匏ドキュメントによるず、6件の倱敗を3぀の䞊列゚ヌゞェントで解決したケヌスでは、コンフリクトれロで解決できたずのこずです。 根本原因を特定しおからデバッグする systematic-debugging スキルは、゚ヌゞェントが「ずりあえずパッチを圓おる」近道を防ぎ、4フェヌズの䜓系的なデバッグを匷制したす。 4フェヌズ: Root Cause Investigation — ゚ラヌを読み蟌み、再珟手順を確認し、盎近の倉曎を調査する Pattern Analysis — 動いおいる䌌た箇所ず比范しお差分を特定する Hypothesis and Testing — 仮説を1぀立お、最小限の倉曎で怜蚌する Implementation — 根本原因のみを修正し、再発防止のテストを远加する 原因が特定できおいない段階で修正コヌドを曞き始めようずするず、スキルの Iron Law「NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST」に匕っかかり、Phase 1 に差し戻されたす。「盎ったず思ったらたた壊れた」ずいう状況を根本から防げたす。 実装前に蚭蚈を固める brainstorming スキルを䜿うず、゚ヌゞェントはコヌドを曞く前に蚭蚈察話フェヌズを実斜したす。 察話の流れ: 芁件の確認ず曖昧な点の掗い出し 耇数の実装アプロヌチを提瀺しお比范 トレヌドオフ速床・保守性・耇雑床を明瀺 合意した方針をドキュメント化 このスキルは writing-plans ず連携しおおり、察話が終わるず自動的に実装蚈画の䜜成に移行したす。「ずりあえず実装しおみお埌で考える」ずいう゚ヌゞェントの傟向を、蚭蚈ファヌストに切り替えたす。 1問ず぀䞁寧に芁件を深掘りし、合意が取れた段階で蚭蚈曞を䜜成→ writing-plans スキルぞ匕き枡したす。 コヌドレビュヌのサむクルを回す requesting-code-review ず receiving-code-review の2スキルを䜿うず、゚ヌゞェントがレビュヌの䟝頌から反映たで䞀貫しお察応したす。 レビュヌ䟝頌時 requesting-code-review : PR の目的・倉曎の抂芁・テスト状況を敎理しおレビュアヌに提瀺 レビュヌしやすい粒床に倉曎を分割 フィヌドバック受け取り時 receiving-code-review : 指摘内容を「必須修正」「提案」「議論」に分類 技術的な根拠をもずに修正の芁吊を刀断 反映した倉曎を明瀺しおレビュアヌに返答 感情的に反応したり、すべおの指摘を無条件に受け入れたりするのではなく、技術的な評䟡に基づいお察応したす。 技術的に䞍芁な倉曎はプッシュバックし、根拠のある修正のみを行いたす。 さいごに 今回は obra/superpowers に぀いお玹介したした。 AI゚ヌゞェントは非垞に高性胜ですが、プロセスを省略しがちずいう匱点がありたす。superpowers はそこに「芏埋」を泚入するこずで、゚ヌゞェントを本来の実力で動かし続けるための仕組みです。 特に test-driven-development スキルず systematic-debugging スキルは、゚ヌゞェントが近道を取ろうずする兞型的な堎面で真䟡を発揮したす。たずはこの2぀からむンストヌルしお詊しおみるこずをおすすめしたす。 興味を持った方は、ぜひ公匏リポゞトリや writing-skills スキルを䜿っお、自分だけのスキルを䜜っおみおください ご芧いただきありがずうございたす この投皿はお圹に立ちたしたか 圹に立った 圹に立たなかった 0人がこの投皿は圹に立ったず蚀っおいたす。 The post AIコヌディング゚ヌゞェントの匱点を補う「obra/superpowers」 first appeared on SIOS Tech Lab .
゚ピ゜ヌド玹介 Ep.1 – クリヌンアヌキテクチャずは Ep.2 – 認蚌方匏の実践的な玹介 Ep.3 – ER蚭蚈ず監査ログ Ep.4 – RepoScanner の実装ずテスト ← 今回はこちら Ep.5 – Copilot プロンプトを効率化 こんな方ぞ特におすすめ クリヌンアヌキテクチャの理屈は分かったけど、どこから曞き始めるのず疑問な方 クリヌンアヌキテクチャで小さな MVP を実装するワヌクフロヌに興味がある方 TDDテスト駆動開発を実務に取り入れお、壊れにくいコヌドを曞きたい方 抂芁 こんにちは。サむオステクノロゞヌのはらちゃんです シリヌズ4本目ずなる今回は、いよいよ埅望の実装線に突入したす。 ここで倧きな圹割を果たすのが TDDテスト駆動開発 です。テストファヌストで進めるこずで、䟝存性の切り離しや境界の明確化が自然ず行われたす。 — 本シリヌズでは、Copilotを掻甚し぀぀、クリヌンアヌキテクチャに沿っお小芏暡なプロダクト「RepoScanner」を蚭蚈・実装した経緯をたずめたす。 この゚ピ゜ヌドは、アプリのコア機胜である「リポゞトリ内のスナップショット䞀芧を取埗する機胜」に焊点を圓おたした。 さらに、「AIぞの指瀺の出し方」や「テストの質の倉化」に぀いおもお䌝えしたす。 実装前の準備 プロゞェクトの党䜓像 実装䜜業を開始する前に、たずは「RepoScanner」の構成を敎理しおおきたす。 目的 リポゞトリのメタデヌタや集蚈結果を効率よく取埗し、分析しやすくするこず。 芁件 スナップショット䞀芧のペヌゞングlimit / offset、最倧取埗数の制限。 クリヌンなナヌスケヌスを保぀「3぀の蚭蚈ルヌル」 RepoScannerにおけるUse Cases局では、以䞋のルヌルを培底したした。 入出力は DTO Data Transfer Object HTTPリク゚ストなどのオブゞェクトは、そのたた䜿わず単玔なデヌタクラスに倉換する。 䟝存はむンタヌフェヌスを介しお泚入DI DB凊理などは、具䜓的な実装ではなく「Repository」などの抜象的な型に䟝存させる。 副䜜甚の入り口を明瀺 「どこで倖郚APIを呌ぶ」など凊理の流れがナヌスケヌスから分かる状態を保぀。 このように責務を分離するこずで、ナヌスケヌスが玔粋なビゞネスロゞックだけに集䞭できる環境が敎いたす。 クリヌンアヌキテクチャによる蚭蚈 コヌドの保守性を高めるため、クリヌンアヌキテクチャに埓っお責務を明確に分離したした。 Domainå±€ SnapshotSummary などの゚ンティティ Use Caseså±€ ListSnapshotSummariesUseCase Interface Adapterså±€ HTTP ゚ンドポむントの制埡 Infrastructureå±€ SnapshotQueryRepository ここで重芁なのは、DBアクセスぞの䟝存を必ず「むンタヌフェヌス」経由にするこずです。 その結果、内偎のロゞックが倖偎の技術的な詳现を知らなくお枈む「䟝存性の逆転」が成立したす。 蚭蚈䞊の芁点 氞続化DBアクセスぞの䟝存は、必ずリポゞトリの「むンタヌフェヌス」を経由させたす。 これにより、内偎Use Cases局が倖偎Infrastructure局の技術詳现を知らない状態ずなり、䟝存の逆転を保ちたす。 → 詳现は ゚ピ゜ヌド1ぞ 階局ごずのテスト戊略 「どこからテストを曞けばいいか分からない」ずいう悩みは、クリヌンアヌキテクチャで解決したす。 なぜなら、各階局の圹割がはっきりしおいるため、テストの目的も自ずず定たるからです。 最優先: Use Cases局のナニットテスト ここでは「仕様ずしおの正しさ」を怜蚌したす。 limit の䞊限クリッピングや、 offset の負倀チェックなどが察象です。 このテストはフレヌムワヌク曎新や DB ドラむバ倉曎に圱響されない高速なフィヌドバックを埗るこずが可胜です。 䟋えば、以䞋のような正垞系のテストを甚意しおください。 /tests/use_cases/test_list_snapshot_summaries.py # 正垞範囲の limit がそのたたリポゞトリぞ枡されるこずを確認 class ListSnapshotSummariesUseCaseTest(unittest.TestCase): def test_execute_passes_limit_when_within_bounds(self) -> None: repository = FakeSnapshotQueryRepository() use_case = ListSnapshotSummariesUseCase(repository, max_read_limit=25) use_case.execute(limit=1, offset=0) self.assertEqual(1, repository.received_limit) use_case.execute(limit=25, offset=0) self.assertEqual(25, repository.received_limit) 倖郚䟝存はすべおモック化する ドメむンモデルに正しく仕事を任せおいるか確認 優先: Infrastructure局のテスト 次に、 「 技術的な正しさ 」 を怜蚌したす。 SQLの組み立おが正しいか、LIMIT / OFFSETのパラメヌタ順序が間違っおいないかずいったパラメヌタ順序や DBから取埗したデヌタを゚ンティティぞ正しくマッピングできおいるかを確認できたす。 /tests/frameworks&drivers/persistence/test_postgres_snapshot_query_repository.py # フィルタなしで LIMIT/OFFSET がパラメヌタに含たれ、`ORDER BY s.observed_at desc` が含たれる確認 class PostgresSnapshotQueryRepositoryTest(unittest.TestCase): def test_list_snapshot_summaries_without_filters(self) -> None: now = datetime(2026, 2, 24, tzinfo=UTC) cursor = _FakeCursor( rows=[ { "snapshot_id": UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), "fetch_run_id": UUID("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), "target_repo_id": UUID("cccccccc-cccc-cccc-cccc-cccccccccccc"), "owner": "owner", "name": "repo", "default_branch": "main", "requested_at": now, "observed_at": now, "head_sha": "deadbeef", "scan_scope": "full", "status": "succeeded", "trigger_type": "manual", } ] ) with patch( "application.backend.src.infrastructure.persistence.postgres_snapshot_query_repository.get_cursor", return_value=_CursorContext(cursor), ): repository = PostgresSnapshotQueryRepository() items = repository.list_snapshot_summaries( limit=10, offset=5, user_id=None, target_repo_id=None ) self.assertEqual([10, 5], cursor.executed_params) どのようなSQLたたはリク゚ストを組み立おたか怜蚌 むンタヌフェヌスの「型」ず「倉換」を確認 最終: 統合テストE2E 最埌に、GitHub Actionsでのデヌタ抜出からDB保存、そしおアプリでの読み取りたで、システム党䜓のフロヌが本番に近い環境で動䜜するかを確かめたす。 /tests/http/test_main_api.py # ナヌスケヌスのクリッピングを利甚しお 200 を返すこずを確認 class MainApiTest(unittest.TestCase): def test_list_snapshots_clamps_limit_and_returns_200(self) -> None: repository = _RecordingSnapshotRepository() cast(Any, main).list_snapshot_summaries_use_case = ListSnapshotSummariesUseCase( repository, max_read_limit=5 ) response = self.client.get("/snapshots?limit=999&offset=0") self.assertEqual(200, response.status_code) self.assertEqual(5, repository.received_limit) 本物のデヌタベヌスを䜿甚 セットアップずクリヌンアップの仕組みが必須 実装 TDDテスト駆動開発 進め方 TDDは、単にバグを防ぐためだけでなく、「蚭蚈を掗緎させるためのツヌル」です。 以䞋の3サむクルで進めたす。 Red: 倱敗 仕様の最小ケヌスを満たす「テストコヌド」を曞く。ただ実装がないので圓然゚ラヌ。 Green: 成功 そのテストが通るように、最小限の「実装コヌド」を曞く。 Refactor: リファクタリング テストが通る状態を保ったたた「蚭蚈ルヌル」に合わせおコヌドをきれいに敎理。 サむクルのむメヌゞ図です。 䟋えば、「1回の取埗䞊限は100件たで」ずいうルヌルはAPIの仕様ではなくドメむンの芏則です。これをナヌスケヌス局で確実に担保するこずで、フレヌムワヌクに䟝存しない堅牢なロゞックが完成したす。 【実践】スナップショット取埗ナヌスケヌス 前回蚭蚈した snapshot テヌブルに察しお、「スナップショットを取埗する」ずいうナヌスケヌスをTDDで䜜っおみたしょう。 Use Cases局のナニットテスト tests/use_cases/test_register_snapshot.py import unittest from uuid import UUID from datetime import UTC, datetime from application.backend.src.domain.entities.snapshot_summary import SnapshotSummary from application.backend.src.use_cases.list_snapshot_summaries import ListSnapshotSummariesUseCase class _FakeRepo: def __init__(self): self.called = False self.last_params = {} def list_snapshot_summaries(self, *, limit, offset, user_id, target_repo_id): self.called = True self.last_params = dict(limit=limit, offset=offset, user_id=user_id, target_repo_id=target_repo_id) now = datetime.now(UTC) return [SnapshotSummary( snapshot_id=UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), fetch_run_id=UUID("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), target_repo_id=UUID("cccccccc-cccc-cccc-cccc-cccccccccccc"), owner="owner", name="repo", default_branch="main", requested_at=now, observed_at=now, head_sha="deadbeef", scan_scope="full", status="succeeded", trigger_type="manual" )] class ListSnapshotSummariesUseCaseTDD(unittest.TestCase): def test_offset_negative_raises_and_repo_not_called(self): repo = _FakeRepo() uc = ListSnapshotSummariesUseCase(repo, max_read_limit=100) with self.assertRaises(ValueError): uc.execute(limit=10, offset=-1) self.assertFalse(repo.called) def test_limit_zero_becomes_one(self): repo = _FakeRepo() uc = ListSnapshotSummariesUseCase(repo, max_read_limit=100) uc.execute(limit=0, offset=0) self.assertEqual(1, repo.last_params["limit"]) def test_limit_clamped_to_max(self): repo = _FakeRepo() uc = ListSnapshotSummariesUseCase(repo, max_read_limit=25) uc.execute(limit=999, offset=0) self.assertEqual(25, repo.last_params["limit"]) def test_passes_filters_and_offset(self): repo = _FakeRepo() uc = ListSnapshotSummariesUseCase(repo, max_read_limit=100) user_id = UUID("11111111-2222-3333-4444-555555555555") target_repo_id = UUID("66666666-7777-8888-9999-aaaaaaaaaaaa") uc.execute(limit=10, offset=5, user_id=user_id, target_repo_id=target_repo_id) self.assertEqual(5, repo.last_params["offset"]) self.assertEqual(user_id, repo.last_params["user_id"]) self.assertEqual(target_repo_id, repo.last_params["target_repo_id"]) if __name__ == "__main__": unittest.main() 仕䞊がったら想定する゚ラヌかどうか、テストを走らせおみるこずをお勧めしたす。 Use Caseså±€ テストを曞いた埌で、初めお ListSnapshotSummariesUseCase クラスを実装したす。 python class ListSnapshotSummariesUseCase: def __init__(self, snapshot_query_repository: SnapshotQueryRepository, max_read_limit: int) -> None: self.snapshot_query_repository = snapshot_query_repository self.max_read_limit = max_read_limit def execute(self, *, limit: int, offset: int, user_id: UUID | None = None, target_repo_id: UUID | None = None) -> list[SnapshotSummary]: if offset < 0: raise ValueError("offset must be greater than or equal to 0") safe_limit = max(1, min(limit, self.max_read_limit)) return self.snapshot_query_repository.list_snapshot_summaries( limit=safe_limit, offset=offset, user_id=user_id, target_repo_id=target_repo_id ) ペむロヌド怜蚌をUse Cases局で蚘述 Interface Adapters局のバリデヌションだけに䟝存せず、ドメむンルヌルずしおテスト可胜に このように1぀テストを䜜成したら1぀実装するサむクルを䜜るず、意図したコヌド実装になりやすいです。 プロゞェクトによっお、1ファむルごずにするか党䜓のテストを先に䜜っおしたうかは調敎すべきだず感じたした。 Interface Adapterså±€ ナヌスケヌスの実装埌、モック化しおいたDBぞの具䜓的な接続凊理は、埌からむンタヌフェヌスの䞭身ずしお差し替えたす。 python class PostgresSnapshotQueryRepository(SnapshotQueryRepository): def list_snapshot_summaries(self, *, limit: int, offset: int, user_id: UUID | None, target_repo_id: UUID | None) -> list[SnapshotSummary]: conditions: list[sql.Composable] = [] params: list[object] = [] if user_id is not None: conditions.append(sql.SQL("fr.user_id = %s")) params.append(user_id) if target_repo_id is not None: conditions.append(sql.SQL("fr.target_repo_id = %s")) params.append(target_repo_id) # where_clause 組み立お、limit/offset を params に远加しお実行 Infrastructureå±€ さいごに、マむグレヌションは以䞋のように远加したす。 create table history_event ( history_event_id uuid primary key default gen_random_uuid(), user_id uuid not null references app_user(user_id) on delete restrict, fetch_run_id uuid references fetch_run(fetch_run_id) on delete set null, event_type text not null, happened_at timestamptz not null default now(), summary text not null ); create table operation_log ( operation_log_id uuid primary key default gen_random_uuid(), actor_user_id uuid references app_user(user_id) on delete set null, action text not null, target_type text not null, target_id text, happened_at timestamptz not null default now(), result text not null check (result in ('success', 'failure')), error_code text, trace_id text not null ); 監査・運甚芳点で operation_log を甚意 CI 実行の倱敗や暩限゚ラヌをすぐ远跡できる Copilotを掻甚するプロンプト術 クリヌンアヌキテクチャの骚栌を䜜る際、AIにれロからコヌドを曞かせるず、局の境界が曖昧になりがちです。 そこで、「型」ず「䟝存関係」を明瀺するこずで、䞀発で実甚的なコヌドを埗るこずができたす。 実際のプロンプト䟋 以䞋のように指瀺を出すず、Copilotはビゞネスロゞックを玔粋なPythonコヌドずしお抜出し、テストしやすい構造で出力しおくれたす。 RepoScanner のスナップショット䞀芧を返す `ListSnapshotSummariesUseCase` を実装しおください。 入力: limit, offset, optional filter 出力: SnapshotSummary のリストず total_count 既存゚ンティティ: SnapshotSummary(id, repo_name, observed_at, summary) 䟝存: SnapshotQueryRepository.list_snapshots(limit, offset, filter) -> (items, total) テストunittest スタむルを先に瀺しおください。 生成されたコヌド抜粋 プロンプトの指瀺通り、たずはUse Cases局のコヌドが生成されたす。 APIの怜蚌ずは別に、ビゞネスロゞックが玔粋なPythonコヌドずしお抜出され、テストしやすい構造になりたした。 application/backend/src/use_cases/list_snapshot_summaries.py class ListSnapshotSummariesUseCase: def __init__(self, repo: SnapshotQueryRepository, max_read_limit: int = 100): self.repo = repo self.max_read_limit = max_read_limit def execute(self, limit: int, offset: int = 0): if limit <= 0: raise ValueError("limit must be > 0") if offset < 0: raise ValueError("offset must be >= 0") limit = min(limit, self.max_read_limit) return self.repo.list_snapshots(limit=limit, offset=offset) Interface Adapters局はリク゚ストを受け取り、ナヌスケヌスを呌び出すだけの「薄い」局になりたす。 Python @app.get("/snapshots") def get_snapshots(limit: int = 20, offset: int = 0, repo=Depends(get_snapshot_repo)): items, total = ListSnapshotSummariesUseCase(repo).execute(limit=limit, offset=offset) return {"items": [i.to_dict() for i in items], "total": total} おたけ: GitHub Actions ゚ピ゜ヌド2では、実行環境の分離や運甚コストから、GitHub Actions による認蚌ず実行をメむンに据える決断をしたした。 認蚌たわりのワヌクフロヌ抜粋 permissions: contents: read pull-requests: read issues: read jobs: collect: steps: - name: Collect PR snapshot and persist env: GITHUB_TOKEN: ${{ github.token }} DATABASE_URL: ${{ secrets.DATABASE_URL }} permissions を明瀺し、 github.token 実行時トヌクンを掻甚 倖郚 DB ぞの曞き蟌みには DATABASE_URL ずいった修食枈みのシヌクレットを利甚 たずめ 今回は、「テストを先に曞くこずで、自然ず䟝存を切り離す蚭蚈になる」ずいう、TDDずクリヌンアヌキテクチャの盞性の良さが䌝わるご玹介をしたした。 「どうテストするか」を考えるTDDは蚭蚈を導く最匷のツヌル ナヌスケヌスは小さく、玔粋に䜜り、倖郚の仕組みに䟝存しない玔粋なビゞネスロゞックを保぀ AI には型ず䟝存関係を䌝えるこずで、開発効率が劇的に向䞊する ゚ピ゜ヌド5では、さらに䞀歩螏み蟌んだCopilot運甚術に぀いおお話ししたす。お楜しみに 参考 あらためおDTOを孊ぶ ご芧いただきありがずうございたす この投皿はお圹に立ちたしたか 圹に立った 圹に立たなかった 0人がこの投皿は圹に立ったず蚀っおいたす。 The post Copilot × Clean Architecture | 実装ずテスト first appeared on SIOS Tech Lab .

動画

曞籍