品質って、どこで作るんだっけ?と思いを馳せてみた

プロダクト開発に取り組む中で本番インシデントに向き合うとき、改めて「品質ってやっぱりテストの瞬間ではなく、もっと前の工程から作り込んでいくものだよな」と考えていました。

トラブルが発生した際に、起きている現象へ迅速に対処したり、事前チェックの項目を増やしたりして次を防ごうとする姿勢はもちろん大切です。ただ、テストで検知する網を広げることに留まらず、「そもそもそのバグはどの工程で作り込まれたのか?」を特定し、根本から防ぐ仕組みを作っていくことが大事だと、感じています。

発生した個別の不具合をテストで捕まえるだけでなく、それが「どの工程で生まれたのか」に目を向けることで、同じような構造の課題(同型の再発パターン)をまとめて防ぐ仕組みへ昇華させていく。そんなアプローチについて、頭の整理も兼ねて書いてみます。


テストは品質を「確認する」もの。ただし、早い段階のテストは設計を良くする手段でもある

インシデントを防ぐ方法を考えると、「出荷前やリリース直前の検査(QA)をより手厚くしよう」という発想になりやすいです。リリース直前のテストは、あくまで「今の状態(品質)を可視化・確認する作業」である、という側面は確かにあります。

一方で、設計や実装の早い段階で書くテスト(TDD や契約テストなど)は、単なる「後追いの検査」ではなく、設計の穴を早く見つけ、品質を作り込む手段にもなります。つまり大事なのは「テストをするか否か」ではなく、品質を後工程に押し付けないこと。検査も、設計も、実装も、それぞれの工程の中で品質を育てていく、という見方のほうが自然かなと思います。

製造業の「トヨタ生産方式」でもよく知られる「品質は工程で作り込む(Built-in Quality)」という考え方があります。検査の段階で不良品を弾くだけでなく、製造の各ステップの中で最初から不良を出さない仕組みを作ろう、という思想です。ソフトウェア開発でも、比喩として同じことが言えると感じています。

仕様の考慮漏れや設計上のボトルネックといった課題は、実際のコードを書く前の段階ですでに生まれていることが多いです。テストで不具合を検知して直すアプローチも大切ですが、「そもそも仕様検討や設計の段階で、どうすればその不具合を作り込まずに済んだか?」と発生工程まで遡って振り返るほうが、より根本的な解決に近づけそうです。

インシデント対応のあとも、ポストモーテムで「いつ・どの判断で・どの工程にバグが入り込んだか」を言語化しておくと、この振り返りが次の改善につながりやすくなります。そのためには、個人を責めるのではなく仕組みを直す、非難しない(blameless)ポストモーテムが前提になる、とも思います。


個人の「注意深さ」だけに頼らず「安全なガードレール」をつくる

トラブルが起きたとき、「次から気をつけよう」とルールを増やすことも、経験上よくある話です。ただ、確認作業やルールをどんどん増やしていくと、どうしても開発時の負担が増えて、スムーズな開発がしにくくなってしまいます。

テストでの検知に留まらず、バグが生まれた工程にアプローチして同型の課題を根本から解決するには、「気をつける」という人間の注意力だけに頼るのではなく、「意識しなくても自然と安全に作れる状態(ガードレール)」を整備することがポイントなのかなと思います。

たとえば、「各自がレビュー時に気をつける」と注意喚起するよりも、設計の標準パターンを揃えたり、コードの静的解析や CI で危険な記述を自動検知したりするアプローチです。ポカヨケ(ヒューマンエラー防止策)の考え方を取り入れて、ミスが起きにくい枠組みを整えていく。

もちろんガードレール自体の設計・運用にもコストはかかります。それでも、属人的な注意力に依存し続けるより、チーム全体で再現性のある安全を作れるほうが、長期的には楽になることが多いと感じています。

開発者が変に身構えることなく、安心してのびのび開発できる環境を目指すほうが、結果としてチーム全体が幸せになれる気がします。


AIのおかげで、上流での品質づくりがやりやすくなってきた

「上流工程でしっかり品質を作り込もう」と言うと、人間が検討したりレビューしたりするコストが増えて、デリバリー全体が遅くなるんじゃないか、と心配になるかもしれません。

最近は、この課題を AI がかなり助けてくれるようになってきた、とも感じています。

設計書や仕様案の段階で、AI にセキュリティ、非機能要件、コーナーケースなどの観点から下書きレビューやチェックリスト生成をしてもらう。人間が手作業で漏れを探す負担を減らしつつ、AI と対話しながら仕様の穴を埋めていく、という使い方です。

現時点では、上流検討の質を上げる最初の一手として AI を使うのが現実的だと感じています。出力をそのまま採用するのではなく、人間が確認しながら仕様を磨いていく——そこまでを、まずはチームの目標にしてみる、くらいのイメージです。

過去のインシデントから学んだパターンを、AI のチェック観点やレビューボットに組み込んでおくのも有効です。ただ、そこも陳腐化しうるので、定期的な見直しがセットになると、より信頼できるガードレールになるはずです。


品質に向き合うことが、結果として開発のスピード感にもつながる

ソフトウェア工学では、後工程ほど修正コストが跳ね上がりやすい、という知見(いわゆる cost of change)は長く語られてきました。バリー・ボームの研究などでは、本番リリース後に見つかった課題の修正コストが、要件定義や設計段階より大幅に高くなる、という傾向が示されています。倍数はプロジェクトや文脈によって大きく変わるので、数字そのものより「手戻りを早く防ぐほどトータルコストを抑えやすい」という方向性のほうが、実務では参考になりやすいかなと思います。

また、DevOps の研究で有名な DORA(DevOps Research and Assessment)のレポートでは、デリバリー速度が高いハイパフォーマーなチームほど、変更失敗率が低く、トラブルからの復旧も速い、という結果が示されています。これは「上流だけ頑張れば速くなる」と直接証明しているわけではありませんが、速度と安定性は両立しうることは示唆しています。ハイパフォーマーは、小さな変更、自動テスト、CI/CD、観測可能性などもセットでやっている、というのが DORA 的な読み方です。

上流で品質を作り込むだけでなく、本番環境での観測・モニタリングや段階的リリースも、品質を後から確認・影響を限定する仕組みとして重要です。作り込みと検知・限定の両方を揃えることで、品質全体のバランスが取りやすくなります。

「スピードか、品質か」という二者択一ではなく、各工程で品質を作り込み、手戻りやインシデント対応を減らすことこそが、一因として、結果としてプロダクトを届けるスピードを最大化してくれる、という理解のほうがしっくりきます。


今後インシデントに向き合うときは、「どう検出するか」だけでなく「どの工程でバグが作り込まれ、どこで防ぐチャンスがあったか」という視点も大切にしていきたいです。

「品質は前工程で作る」という原点を意識しつつ、AI のような現代のツールも補助として賢く頼りながら、みんなが楽に安全に開発できる仕組み(ガードレール)へと還元していく。品質保証は特定の担当者や最終テストだけで完結するものではなく、仕様を考えて設計し、コードを書くプロセス全体の中でチームみんなで育てていくものなんだよね、という共通認識を深めていけたら素敵だなと思っています。

組織と個人の営みはモデリングできる?「AQALモデル」で見つけた組織デバッグのデザインパターン(仮説)

1. 導入:すべては「エンジニアの育成をどう構造で捉えるか」から始まった

組織の中でなんらかのマネジメントをしていると、「何か問題がありそうだが、現状をどう整理しようかな?」という領域に遭遇することがあるかと思います。

僕の始まりは、「組織における育成(成長)を、場当たり的ではなく、どうにか構造として捉えて分析できないか?」という悩みでした。技術スタック(外面)だけでなく、マインドセット(内面)やチームへの影響力も絡み合う「育成」という領域は、どうしても感覚的になりがちで、モヤモヤしたからです。

そんな中、ネットで色々調べているうちに出会ったのが、インテグラル理論の「AQAL(アカル)モデル」というフレームワークでした。

AQAL モデルとは?

ケン・ウィルバーという哲学者が提唱した、現実のあらゆる事象を多角的に捉えるための包括的なフレームワークです。 事象を「個人 / 集合」「内面(主観)/ 外面(客観)」の「4つの象限(Quadrants)」に分解して捉える視点が基本となっています。さらに、それぞれの象限の中に、垂直方向の発達段階を表す「レベル(Levels)」や、多角的な能力の軸を表す「ライン(Lines)」といった要素が組み込まれています。

最初からこれが「汎用的な型」であることは知っていたので、さっそく自分の悩みである「育成」に当てはめてみました。結果として、構造化してみるとなんとなくそれっぽいし、有用そうだなと感じました。ただ、このときはまだ、自分の頭の中で「他のドメイン(領域)にも同じように綺麗に当てはめられる」という具体的な感覚や確信までは持てていませんでした。

しかし、この要素を、使い慣れた「UMLのクラス図を組み立てるような感覚」で脳内で構造化して整理してみた瞬間、頭の中で点と点が線で繋がりました。

「あー、なるほど。これは対象ドメインを分析するための『汎用的なデザインパターン』として考えたらいいのかもな」

UMLという補助線(思考の型)を頭の中に引いたことで、AQALモデルの捉え方を自分なりに認識できたのです。オブジェクトの構造や依存関係として捉え直せば、育成という領域を超えて、プロダクト品質の低下や技術負債の放置、インシデントの原因分析など、「組織と個人の営みの中で生まれる問題領域(ドメイン)」にも活用できるのではないか、という期待に変わっていきました。今回は、そんな個人的な思考実験の備忘録のシェアになります。


2. 脳内に展開する「組織と人間の4つの象限」

僕たちが普段、複雑なドメインをクラスに切り分けるように、人間社会のドメインをAQALモデルに沿って「4つの領域」に整理してみます。

  1. 【左上:個人の内面(心理・認知)】 メンバーの本音、モチベーション、認知の偏り(バイアス)、役割の自覚(プレイヤーかリーダーか)など。
  2. 【右上:個人の外面(行動・事実)】 実際の作業時間、スキルの習熟度、手順のスキップ回数、具体的なアウトプットなど。
  3. 【左下:集合の内面(文化・関係性)】 チームの心理的安全性、暗黙の了解・不文律、チーム全体で共有されている「目的」の納得度など。
  4. 【右下:集合の外面(環境・システム)】 コードの構造(レガシーかモダンか)、CI/CDや各種ツール、組織の評価制度やリソースの投資状況など。

これらは独立しているのではなく、「右下(環境)や左下(文化)の構造が、左上(人間の思考)に影響を与え、それが右上(行動)のバグを引き起こす」という依存関係(因果のドミノ倒し)があるのではないか、という仮説です。


3. 視点(主語)が変わると、ドミノの1枚目(アプローチ先)が変わる

このデザインパターンの実用的なメリットは、「いま分析している問題の『主語(視点)』を誰(何)にするかによって、ドミノの最初の一枚(アプローチすべき場所)がガラリと切り替わる」という点にあります。

「技術負債の放置」という同じドメイン(問題領域)を例に、主語を切り替えて考えてみます。

パターンA:主語が「組織」のとき ➔ ドミノの1枚目は【下半分】

  • ドメインの症状:チーム全体として「技術負債の返済がいつも後回しになり、コードベースが徐々にレガシー化して開発スピードが落ちている」という課題。
  • ドミノの起点(アプローチ先):下半分(右下の環境、または左下の文化)
  • なぜそこを見るのか?:主語が組織(マネジメント側)である以上、メンバー個人に対して「意識を高く持とう(左上)」と内面にアプローチしたり、「今日からリファクタリングをして(右上)」と行動の強制をしても、なかなか定着しません。
  • そもそも「スプリント内に負債返済の公式な枠がない(右下)」とか、「納期さえ守ればコードの綺麗さは評価されない(右下)」という仕組みのバグが、メンバーの「負債を触るのが怖い、後回しにしよう(左上)」という思考に繋がっている可能性があります。そのため、マネジメント視点であれば、まずは「公式な返済時間を確保する(右下のアップデート)」といった環境側を書き換えて、メンバーの行動を安全に誘導するのが、アプローチの候補になります。

パターンB:主語が「個人」のとき ➔ ドミノの1枚目は【上半分】

  • ドメインの症状:組織の仕組み(右下)も納期優先のままで、チームの空気(左下)も負債を放置しがち。変えるのが難しい壁(下半分)に囲まれている中で、「一メンバーである自分は、この技術負債に対して何ができるか?」という個人の課題。
  • ドミノの起点(アプローチ先):上半分(自分の内面:左上 ➔ 自分の行動:右上)
  • なぜそこを見るのか?:主語が「自分(個人)」になった瞬間、コントロールできない組織の仕組み(下半分)をドミノの1枚目に据えてしまうと、「会社が悪いから何もできない」と思考停止してしまいがちです。個人が主語のとき、動かせる最初の一枚は常に「自分の内面・マインドセット(左上)」になります。
  • まずは、自分の「どうせ言っても変わらない」という諦めのバイアス(左上)を自覚し、「まずは自分が触る範囲だけでも綺麗にしよう」「負債を放置するリスクを言語化してみよう」と、自分の『役割認識や当事者意識(左上)』のドミノを倒すのがスタートになります。
  • 心(左上)が動けば、「ボーイスカウトルールでコードを少しずつ綺麗にする(右上)」や「負債がどれだけ開発速度を落としているかをファクトベースで提案する(右上)」という行動メソッドが走り出します。個人の上半分(思考と行動)を動かし続けることで、結果として「じゃあ仕組み(右下)を変えようか」と、下半分(組織)のドミノを逆流させて動かしていく、草の根からのアプローチが見えてきます。

このように、「いま、誰(組織か自分か)を主語にして課題を解決しようとしているのか」によって、見るべき領域を上半分(自分)か下半分(組織)かに一瞬で切り替えられる。これこそが、このモデリングの個人的に面白いと思っている視点の切り替えです。


4. AI時代の「ぼっちモデリング」

組織課題について「みんなでホワイトボードを囲んで構造化しよう」と言うのは簡単ですが、現実には複数人を巻き込むのはハードルが高いという場面も多いと思います。

ですが、現代にはAIという壁打ち相手がいます。

AIに現場の症状をそのまま投げ込み、「これをAQALモデルの4象限で分類して、どこに因果のドミノの起点があるか推測して」と聞くだけでも、整理の大きな助けになります。

🚨 AI壁打ちにおける落とし穴と対策

ただし、ここで一つ気をつけたい落とし穴があります。AIは少ない情報からでも「一発でそれっぽい、最もらしい正解(因果関係)」を出してしまいがちです。人間はそれを見ると、深く検証する前に思考停止し、誤った判断で手を打ってしまう危険があります。

これを防ぐための工夫として、AIには「大枠の領域を絞った上で、さらにその領域の中で、具体的な複数の仮説を、その『前提条件(アサーション)』と『後続のドミノ倒しのシミュレーション』とセットで同時に明示させて」と命じるのが有効なアプローチかもしれません。

例えば、主語が組織(下半分)なら、以下のような具体的な仮説のバリエーションを、前提・文脈付きで並べさせます。

  • 仮説①:【右下の仕組み・スケジュール】が原因のパターン
    • 前提条件:日々の開発スケジュールが詰まっており、負債返済に充てられる公式な時間がほとんどない、等
    • 後続ドミノ:開発時間に追われ、負債の調査やコードレビューが荒くなる ➔ さらにレガシーコードが増大する
  • 仮説②:【右下のツール・インフラ】が原因のパターン
    • 前提条件:コードの依存関係や影響範囲を自動で可視化する仕組みがなく、手動での調査に頼っている、等
    • 後続ドミノ:影響範囲の調査だけでミーティング(同期コミュニケーション)が長引く ➔ 「これ、チャットやツールで自動化できないのかな」と疲弊する
  • 仮説③:【左下の文化・共通認識】が原因のパターン
    • 前提条件:チーム内で「技術負債」の定義や、それを返す目的が形骸化しており、義務感だけで語られている、等
    • 後続ドミノ:振り返りの場が単なる負債への愚痴の言い合いになる ➔ チームの前進感がなくなりモチベーションが下がる

🛠️ 【思考実験】「前提条件」が、人間の直感的な違和感と訂正を引き出す

実際、このような「技術負債がなかなか解消されない、調査ミーティングばかりが長引いて価値を感じない」というモヤモヤ(例え話です)をこのプロンプトでAIにデバッグさせてみたとします。

AIが提示した「前提条件」と「後続ドミノ」のバリエーションを眺めた結果、自分の脳の中に「直感的な違和感」が芽生える余地が生まれます。

「仮説①(スケジュールの限界)は合っている気がする。ただ、仮説③(目的の形骸化)も同時に当てはまっているかもしれない。でも、単純にスケジュール(負債返済の期間)を延ばしたとしても、根本解決にはならず不満は出続ける気がするな。……なぜなら、そもそも人間が手動で依存関係を書き、レビューする時間が足りなくて、結果としてリファクタリングの計画自体が荒くなっているからだ。あ、計画の精度が悪いんだ。いや、違う。人間が手動でコードを追いかける時間が足りないんだ。……ということは、時間が足りないんじゃない。負債の可視化やレビューの一部をAIで高速化する『投資(右下)』をしていないことこそが、すべての元凶(ドミノの最初の1枚)かもしれないな」

一発で「精度を高めろ」「時間を延ばせ」と言われるのではなく、AIに「前提+後続付きの複数選択肢」を出させるからこそ、人間に『直感的な違和感から気づき、訂正し、追加情報を与える余地』が残ります。 そして実態に合わせて人間がプロパティをアジャイルに修正していく。これこそが、社会ドメインを誤らずにデバッグするための作法なのかもしれないな、と考えています。


5. これから試して、検証してみたいこと

ここまで色々と書いてしまいましたが、すべては「ネットで見つけた理論を、使い慣れたドメイン分析の感覚にマッピングしてみたら面白そう」という、検証前の私個人の脳内アップデート(仮説)に過ぎません。

「すべての象限が同時に作用し合っている(相互作用)」というAQAL本来の思想からすると、私の「ドミノ倒し(因果関係)」という汎化の仕方はシンプルに落とし込みすぎている部分もあるかもしれません。

ただ、「何か問題がありそうだけど、何が課題か特定できない」というモヤモヤした状態のときに、いきなりチームでホワイトボードを囲んで大げさに始めるのではなく、まずは自分1人でAIと壁打ちしながら、この4つの枠で現状を整理してみる。 そうやって少しずつ手元で感覚を掴んでいくためのツールとしては、意外と扱いやすいのではないか、と思っています。

また、このAQALモデルという捉え方を調べていくうちに、もう一つ面白い使い方ができるかもしれないな、という気がしています。

今回はシンプルに「上半分=個人」「下半分=組織」として考えましたが、この「主語(中心)」は切り替えることができるのではないか、という仮説です。 例えば、主語を「1人の個人」から「1つのチーム」に一段引き上げて捉え直してみます。そうすると、上の象限は「チーム(私たち)としての内面や行動」になり、下の象限はそれを取り囲む「組織(会社全体)の文化やシステム」という見え方に変わります。

この視点の切り替えができるようになると、組織内での誰かの発言を聴いたときに、「あ、この人は組織(下半分)の仕組みのことを言っているな」「この人は個人ではなく、チーム(上半分)としての動きの話をしているな」ということが、脳内で整理しやすくなるのではないか、という期待があります。

まずは自分の身の回りのモヤモヤをAIとデバッグすることから始めて、この「AQALの視点」の真偽や、組織のコミュニケーションへの活かし方を少しずつ検証していこうと思います。


📢 おまけ

最後に少しだけ宣伝をさせてください。私が所属する株式会社HERPでは、まさに今回お話ししたような「組織と個人の営み」の複雑なドメインに、ご自身の現在に合わせて等身大でチャレンジできる2つのEMポジションをオープンしています。

カジュアル面談も大歓迎ですので、もし少しでも興味を持っていただけたら、お気軽にご連絡いただけると嬉しいです!

Auth0 × Next.js 15 セッション有効期限と "謎の期限切れ" にハマらないための備忘録

Auth0 を利用して Web アプリケーションを開発していますが、セッションの有効期限についてわかったことがいろいろとあるので今後のためにも忘れないようにまとめておきたいと思います。 本記事では、Next.js 15 と @auth0/nextjs-auth0 を利用する前提で、意識すべきセッションの構造と、それぞれの有効期限について備忘録としてまとめます。

1. 意識すべきセッション

  • 場所: ブラウザの appSession Cookie(暗号化)。
  • 役割: 「Next.js サーバーから見て、このブラウザが誰であるか」を識別するもの。
  • 期限切れの影響: 画面をリロードした際などに「未ログイン」と判定されます。
  • Tips: Cookie の 4KB 制限に引っかかる場合やその他の要件次第では ストア先をDBに変更 することも可能です。

② アクセストークン(Access Token: AT)とリフレッシュトークン(Refresh Token: RT)

  • 場所: Next.js のサーバーサイドセッション内に保存。
  • 重要ポイント:
    • 外部 API サーバにて AT を検証するために jose などのライブラリを使って検証する場合、Auth0 で API を作成し、その識別子を audience に指定しないと Opaque Token (不透明なトークン)となり、検証することができません。
    • AT が切れても getAccessToken() によって RT があれば SDK が裏で更新してくれます。しかし、更新された AT については Next.js 15 の App Router 環境では Middleware で getAccessToken(req, res) を実行して明示的にセッションを更新しない限り、Cookie は自動的に保存されません(公式のBest Practice参照)。
  • 場所: Auth0 側のドメイン(xxx.auth0.com)の Cookie。
  • 役割: 「Auth0(認証基盤)自体にログインしているか」の状態。
  • 期限切れの影響: アプリのセッションが切れて Auth0 へ飛ばされた際、ここが生きていれば ID/パスワード入力なしでアプリに戻れますが、ここも切れていると再度ログイン画面で入力を求められます。

なぜ Google 認証は毎回アカウント選択が出るのか?

Auth0 のセッションが生きていても、例えばソーシャルログインで Google 認証を選択した際に画面が出るのは 「上流のプロバイダー(Google)のセッション」 が別途存在するからです。 特に、Auth0 からのリクエストに prompt=select_account が含まれていると、Google は必ずアカウント選択画面を表示します。これは意図しないアカウントでの自動ログインを防ぐための挙動です。

2. 各期限の設定と作用の詳細

それぞれの有効期限をどう設定すべきか、Auth0 ダッシュボード上の項目と照らし合わせて確認します。

Auth0 セッションの期限(Tenant Settings / Applications)

Auth0 には「Inactivity Timeout(非活動タイムアウト)」と「Require Log In After(絶対タイムアウト)」の2つがあります。 これらはテナント共通の設定になります。

  • Inactivity Timeout: 最後にアクセスしてから何日間有効か(例: 3日間)。
  • Absolute Timeout: ログインしてから最大何日間有効か(例: 30日間)。
  • ポイント: これがアプリのセッションより短いと、アプリ側で「セッションを維持したい」と思っても、Auth0 側で弾かれる原因になります。

アクセストークンとリフレッシュトークンの期限(API Settings)

以下は API で設定します

  • Access Token Lifetime: 通常は 3600秒(1時間) 程度が推奨されます。短く保ち、漏洩リスクを抑えます。 以下はアプリケーションで設定します
  • Refresh Token Rotation: 有効化を強く推奨。RTを使用するたびに新しいRTを発行する仕組みです。
  • RTの期限: 数日間〜数週間。さらに、セキュリティ要件に応じて「Absolute Lifetime」を設定します。

Next.js セッションの期限(SDK構成)

セッションの期限の設定はこちらを参照してください。

  • rolling: アクセスがあるたびにセッションの有効期限を更新するかどうか。
  • inactivityDuration: 最後にアクセスしてから「何秒間」セッションを維持するか。これを短くすると、放置したユーザーがすぐにログアウトされます。
  • absoluteDuration: アクティビティに関係なく、最初のログインから最大で「何秒間」セッションを維持するか。これを過ぎると強制的に再認証が必要です。

3. 有効期限のバランス

これら3つの期限は、以下の関係性を保つとなぜかログアウトしている、といった事態を防ぎ、一貫性のあるユーザー体験を提供できます。

Auth0セッション ≧ アプリセッション ≧ リフレッシュトークン

  1. RTはアプリセッションより短くしない: RTが先に切れると、アプリはログイン中(Cookieがある)なのにAPIが叩けない、という「中途半端な状態」が発生します。
  2. Auth0セッションは長めに: アプリのセッションが不慮の事態で切れても、Auth0側のセッションが生きていれば、ユーザーは「ログインボタン」を押すだけで、パスワード入力を省略して復帰できます。また、アプリAでログイン状態中にアプリBを訪問した際にサイレントログインによって自然とログイン状態になっている、という体験も提供できるようになります。

まとめ

これら3つのセッションを意識できていると、ログイン周りのトラブルシューティングがスムーズになります。

ファイルを含む集約ルートをリポジトリにストアする際の工夫

DDD を実践していると、集約ルートの中にファイルが含まれるケースに遭遇することがあります。例えば、ユーザープロフィールに画像がある場合や、記事に添付ファイルがある場合などです。この記事では、ファイルを含む集約ルートをリポジトリに保存する際の課題と、その解決策について考えていきます。

前提

  • DDD の集約ルートの詳細な説明は割愛します
  • ファイルの保存先は Amazon S3 とします
  • tsoa を用いて OpenAPI Specification を自動生成します

課題

ファイルを含む集約ルートをリポジトリに保存する際、以下が悩ましいポイントです。

multipart リクエストとスキーマ定義の再利用性

ファイルを含む Command を実行する場合、HTTP 通信では multipart リクエストになります。これにより、tsoa のスキーマ定義の再利用が難しくなります。 例えば、 title, content, attachments という項目がある場合 tsoa では application/json と multipart/form-data では以下のような違いがあります:

application/json の場合、モデルとして定義できます

interface CreateCommand {
  title: string
  content: string
  attachments: AttachmentFile[] // AttachmentFile の定義は割愛
}

multipart/form-data の場合、コントローラーに定義します

export class ArticleController extends Controller {
  @Post("post")
  public async post(
      @FormField() title: string,
      @FormField() content: string,
      @UploadedFiles() attachments: Multer.File[],
  ): Promise<void> {
      ...
  }
}

例えば、Controller から Application 層にリクエストの内容を渡すときにドメインオブジェクトを構築して転送するような DTO がある場合、 モデルで定義していると DTO の引数の型はそのまま再利用することができますが、multipart/form-data の場合パラメータデコレーターになっているため型が再利用することができません。 今回の例のようなパラメータ数だと割り切ることもできますがもう少し数が多いと割り切るのも難しくなってきますね。

メモリが不足する可能性

特に複数のファイルを扱う場合、サーバーのメモリが圧迫されるためサーバー側で OutOfMemory のリスクが気になります。

集約境界の問題

ファイルを別の集約ルートとして扱う方法も考えられますが、これでは集約を跨いだ更新が必要になります。集約ルートは強い整合性を維持する範囲にしたいため、ファイルが集約の一部である場合はできるだけ同じ集約内で管理したいところです。

解決策

これらの課題に対処するため、以下のようなアプローチを考えてみました。

1. ファイル単位のアップロードコマンドのエンドポイントを用意

まず、ファイルを1つずつアップロードするための専用 Command のエンドポイントを用意します:

export class ArticleController extends Controller {
    @Post('file/upload')
    public async uploadFile(
        @UploadedFile() file: Multer.File,
    ): Promise<{file_id: string}> {
      ...
    }

2. メタデータの活用

ファイルアップロード時に、ドメインオブジェクトの内容をメタデータとしてS3に保存します。これにより、後でファイルを取得する際にドメインオブジェクトを再構築できます。

    public async uploadFile(
        file: AttachmentFile, // アップロードされた添付ファイルの Entity
    ): Promise<void> {
        const sourceKey = keys.temp(file.id);

        try {
            await this.s3.putObject(sourceKey, file.body.buffer, {
                filename: encodeURIComponent(file.body.filename),
                size: file.body.size.toString(),
                mimetype: file.body.mimetype,
            });
        } catch (error) {
          ...
        }
    }

メタデータを利用することでアップロードしたファイルの情報を集約ルートのリポジトリで保存する必要がなくなります。 集約ルートのリポジトリはドメインオブジェクト全体をストアするために使いたいので、今回のような集約ルート全体のオブジェクトが構築できない段階での一部のドメインオブジェクトだけのストアはリポジトリに不要な複雑さをもたらすために避けました。 また、アップロードされたファイルは、まず一時ディレクトリにS3で保存します。この一時ディレクトリのファイルは、S3のライフサイクルポリシーにより定期的に削除されるよう設定します(例:24時間後に削除)。

3. 集約ルート登録時のプロセス

集約ルートを登録する際のプロセスは以下のようになります:

  • 事前にファイルをアップロードし、各ファイルのエンティティID(fileId)を取得しておく
  • 集約ルート登録のCommandでは、ファイルの実体ではなくエンティティIDを送信する
interface CreateArticleCommand {
  title: string;
  content: string;
  attachmentIds: string[]; // ファイルのエンティティID
}
    public async execute({
        id,
        title,
        content,
        attachmentIds,
    }: CreateCommand): Promise<void> {
        const attachments = attachmentIds.map(
            // 一時ディレクトリから正式なディレクトリに昇格
            async (id) => await this.storageAdapter.promoteAttachment(fileId)
        );

        const model = Article.create({
            id,
            title,
            content,
            attachments,
        });

        await this.articleRepository.store(model);

        ...
    }

まとめ

もしファイルが集約ルートの一部になる場合はこのような方法をすると上記で挙げたような課題はクリアするかなと思いました。

今回は省略していますが、例で挙げたドメインオブジェクトが特定のユーザーにのみ関連づけられているものであれば S3 のオブジェクトキーにも user_id をつけたり、AttachmentId の値オブジェクトのプロパティに user_id を持たせて Article を生成する際に user_id の同一チェックをすることで誤って他のユーザーのファイルを関連づけさせるリスクを減らすことができると思います。

僕が現在所属する株式会社 HERP の HERP Careers (β版) の開発チーム ではこのように DDD としてどういう設計が良いのか議論しながら お仕事をしていますのでもし僕たちに興味を持っていただいた方は会話の機会をいただけると嬉しいです!

新規事業開発における効率的な E2E テスト戦略 - スピードと品質のバランスを取る

はじめに

新規事業のプロダクト開発において、開発速度と品質保証は常にトレードオフの関係にあります。 特に 0→1 の探索フェーズでは、限られたリソースを製品価値の検証に集中させたいという要求がある一方で、将来の成長を見据えた技術的な健全性も維持する必要があります。

本記事では、Playwright と Cucumber を活用して、開発速度を落とすことなく必要な品質を担保する取り組みに一定の成果を感じましたので1つの事例として紹介します。

新規事業の状況

新規事業におけるプロダクト開発は以下のような状況だと思います。

1. 製品価値の検証が最優先

開発リソースの大部分を以下に投資する必要があります。

  • 製品として市場に価値を提供できるかの検証
  • ユーザーフィードバックに基づく機能改善
  • 新しい仮説に基づく機能開発

2. 頻繁な仕様変更

探索フェーズならではの特徴として

  • 機能やデータモデルの頻繁な変更
  • ユーザーフィードバックに基づく機能の追加・改修・削除

3. 継続的な技術的改善

製品の成長に備えて以下のことも必要になります。

  • 新しい知見の導入に伴うリファクタリング
  • renovateを活用した定期的なライブラリアップデート

状況から見える課題

このような状況下では、短期的な成果へのプレッシャーやチーム体制、プロジェクトマネジメントなど様々な要因から継続的な技術改善が軽視され、それにより以下のリスクが発生しやすくなると考えています。

1. リファクタリングを保留することのリスク

発生頻度や影響度から意図的にリスクを受容しリファクタリングをしないということもありますが、もし考慮不足のままリファクタリングを保留すると以下のようなリスクがあると考えています。

コードベースが徐々に複雑化・肥大化し、新機能の追加や既存機能の修正に多大な時間とコストがかかるようになります。 また、コードの可読性が低下することで、バグの混入リスクが高まり、それを発見・修正することも困難になります。 開発効率が低下することで、チームメンバーのモチベーションにも悪影響を及ぼす可能性があります。 最終的には、システム全体の再構築が必要になるほど深刻な状況に陥るリスクがあり、事業継続性にも影響を与えかねません。

2. ライブラリアップデートを保留することのリスク

既知のセキュリティ脆弱性にさらされ続けるリスクが高まります。 また、パッケージのバージョン差が徐々に広がり、将来的に大規模なアップデートが必要になった際、互換性の問題や予期せぬエラーが発生する可能性が高くなります。 結果として、開発チームは多大な時間とリソースを費やしてアップデート作業を行う必要に迫られ、本来の開発業務に支障をきたす恐れがあります。

課題解決の方針

これらの課題に対して、「製品全体のシナリオが正常動作していることを E2E テストで効率的に確認できる状態を作る」という方針を定めました。

1. E2E テストによる製品全体の検証

E2E テストでは、実際のユーザーの利用シナリオに沿って製品全体の動作を検証します。 重要な業務フローを自動テストで実行することで、効率的に品質確認を行います。 これにより、手動での確認作業を最小限に抑えながら、エンドツーエンドでの動作保証を実現します。

2. スモークテストとしての位置づけ

E2E テストは、製品が「壊れていない」ことを確認するスモークテストとして位置づけています。 テストの範囲は致命的な問題の早期発見に焦点を当て、表示崩れなどの軽微な問題は許容することで、効率的なテスト運用を実現します。

3. テストコードの保守性重視

テストコードの保守性を重視し、「テストコードだけ壊れた」ということが少なくなるよう Page Object パターンや DOM 構造に依存しないなどの配慮をすることでリファクタリングへの耐性を確保します。

具体的なソリューション

E2E テストの実装

E2E テストの実装には、Cucumber と Playwright を組み合わせたアプローチを採用しています。 Cucumber を用いたシナリオテストでは、独立した一連のユースケースをシナリオとして定義しています。 Gherkin 記法を使用することで、非エンジニアでも理解しやすいテストシナリオを作成できます。 シナリオの実行には、Playwright を使用してテストスクリプトを実装しています。

Feature: 商品検索
  Scenario: 商品を検索して詳細を確認
    Given ログイン済みのユーザーとして
    When 商品一覧ページを表示する
    And "テスト商品"で検索する
    Then 検索結果が表示される
Given('ログイン済みのユーザーとして', async () => {
    const session = await generateSession(subject);
    page = await signin(browser, session);
});

When('商品一覧ページを表示する', async () => {
    const po = ProductListPage.goto(page);
    page = po.page
});

When('"テスト商品"で検索する', async () => {
    const po = ProductListPage.of(page);
    await po.search("テスト商品")
    page = po.page
});

Then('検索結果が1件以上表示される', async () => {
    const po = ProductListPage.of(page);
    expect(await po.getSearchResults()).toBeGreaterThan(0);
});

また、シナリオテストでカバーしきれない個別のユースケースについては、Playwright を直接使用したテストを作成しています。 これにより、より細かな品質担保が必要な部分もカバーできています。 テストコードは以下のような構成で管理しています。

e2e/
├── features/     # Cucumberシナリオ
├── page-object/  # 再利用可能なPage Object
└── test/         # 個別のユースケーステスト

E2E テストはスモークテストとして、リファクタリングや renovate によるライブラリ更新後に製品が壊れていないことを確認する目的で実行します。 テストはローカル開発環境でのコード変更時や、CI 環境でのプルリクエスト作成時に自動的に実行され、開発者は早い段階で問題を検知することができます。

Page Object パターンの活用

テストコードのリファクタリングへの耐性を高めるため、Page Object パターンを採用し、要素の特定方法(Locator)を隠蔽して、振る舞いのみを公開するインターフェースを提供しています。

export class ProductListPage {
  constructor(private page: Page) {}

  async search(keyword: string) {
    await this.page.getByRole('searchbox').fill(keyword);
    await this.page.getByRole('button', { name: '検索' }).click();
  }

  async getSearchResults() {
    return this.page.getByRole('listitem').count();
  }
}

この Page Object は、Cucumber のシナリオテストと Playwright の個別ユースケーステストの両方から利用され、テストコードの重複を防いでいます。 また、DOM 構造への依存度を下げるため、単純な Locator の代わりに、getByRole や getByLabel などのセマンティックな要素特定方法を優先して使用しています。 これにより、HTML の構造変更に強いテストコードを実現しています。

現状の課題

E2E テストを運用していく中で、二つの重要な課題を抱えています。

一つ目は、テストケースの増加に伴う実行時間の長期化です。 新機能の追加や既存機能の改善のたびに、それを E2E テストで検証すべきか慎重に判断する必要があります。 特に、Unit Test や Integration Test で十分カバーできるものまで E2E テストに含めてしまうと、テスト実行時間が不必要に長くなってしまいます。

二つ目は、本番環境特有の問題への対応です。 開発環境や検証環境では再現できない問題が本番環境で発生するリスクは常に存在します。 現在まで重大な障害は発生していないものの、実際のユーザー環境での予期せぬ挙動や、大規模アクセス時の問題などを、事前に検知できない可能性があります。

まとめ:スピードと品質のバランスを取る

1年間の運用を通じて、Playwright と Cucumber を組み合わせた E2E テスト戦略は、新規事業開発において効果的に機能していることが確認できました。 特に、大規模なリファクタリングやライブラリの継続的なアップデートを、品質を担保しながら実施できている点は大きな成果です。

Page Object パターンの採用により、テストコードの保守性も高く保てており、製品の変更に柔軟に対応できています。 「壊れていないことの確認」に焦点を絞ったスモークテストとしての位置づけも、新規事業開発におけるスピードと品質のバランスを取る上で適切でした。

しかし、この成功は E2E テストだけによるものではありません。 プログラムの結合度を下げ、凝集度を高めるよう設計することで、変更の影響範囲を局所化しています。 また、機能の複雑度や重要度に応じて適切な Unit Test を実装し、プログラムの品質を多層的に担保しています。 これらの取り組みが相互に補完し合うことで、品質の安定化が図れたのかと考えています。

E2E テストについてはテストケースの増加に伴う実行時間の管理や、本番環境特有の課題への対応など、改善の余地は残されています。 今後は、これらの課題に取り組みながら、より効率的なテスト戦略を追求していきたいと考えています。

新規事業開発に興味を持っていただけましたら、ぜひ HERP の 採用ページ をご覧ください。 プロダクト開発の裏側についても、もっと詳しくお話できればと思います。

循環的複雑度の計測

背景

テストコードもない複雑なレガシーコードに対して変更する場合、常にデグレードのリスクが伴います。エンジニアの知見や経験によってそれを回避することも可能ですが、チームとして取り組む上では仕組みでそのリスクを軽減する必要があります。

以前、チームでレガシーコードに対して修正したとき、PR でレビューしたにも関わらず、デグレードが発生しました。原因を分析していくと、対象のメソッドは変更前から循環的複雑度が50を超えており、変更後はさらに数値が上がっていましたが、そのことをチームとして認識できていない状況でした。その他にも因子はありましたが、実装中からバグの混入確率を減らすことが期待できる仕組みの1つとして循環的複雑度に着目しました。

実際にバグ分析からプロセスを改善したときの方針と内容を以下に記載します。

方針

内部品質が低下することにより、バグの混入確率が上昇するため、内部品質を定量的に評価できる指標の1つとして循環的複雑度を用いる。 変更したコード、新しく追加したコードに対してバグの混入確率を循環的複雑度で評価し、定量的な指標をもとに適切に対処する仕組みを構築する。

循環的複雑度が低いことは内部品質が高いことの必要条件である。しかし、必要十分条件ではないことを留意する

期待する効果

循環的複雑度を把握することにより、現在より以下の効果(=バグの混入確率低下)を期待する

  • レビュイー自身がレビュー前に循環的複雑度が高いコードであることを認識することにより、事前に循環的複雑度を下げるためのリファクタリングや単体テストとして網羅率が十分であるか確認できる
  • レビュアーが循環的複雑度が高いコードであることを認識することにより、循環的複雑度を下げるための内部設計として改善の余地がないか、バグが混入していることを前提としたコードの確認ができる

改善するプロセス

今後、循環的複雑度を把握し、バグの混入確率を下げるための取り組みとして以下を実施する。

  • Pull Request 時に修正前後のメソッド、新規のメソッドに対して循環的複雑度を計測する
  • Merge する条件に循環的複雑度を追加する
    • 修正前が30未満の場合、修正後が30以上の場合は NG とする (修正後が30未満であれば前回より増加しても需要する)
    • 修正前が30以上の場合、修正後が前回を超える場合は理由を記載する
      • x.x.x 以降は NG とする
  • 新規が30以上の場合は NG とする

技術的負債の種類と対処

技術的負債の種類と対処

開発チームの会話の中で 技術的負債 というワードを口に出したり、聞いたりすることがよくあるかと思います。 その技術的負債がイコール悪というコンテキストで会話していることに違和感を持っていました。

ある日 More Effective Agile ~“ソフトウェアリーダー"になるための28の道標 を読んでいると、技術的負債にはさまざまな分類法があり、その中で著者が有益と考えている以下の分類法が紹介されており、持っていた違和感の正体がわかりました。

意図的な技術的負債(短期)

戦術的または戦略的な理由による技術的負債。たとえば、時間的制約のあるリリースを期限までにデプロイするなど。

意図的な技術的負債(長期)

戦略的な理由による技術的負債。たとえば、最初からマルチプラットフォーム対応の設計と構築を行うのではなく、最初は1つのプラットフォームだけをサポートするなど。

不慮の技術的負債(悪意)

いいかげんなソフトウェア開発の習慣のせいで意図せずに発生する技術的負債。この種の技術的負債は将来と現在の作業を失速させるため、回避すべきである。

不慮の技術的負債(善意)

ソフトウェア開発がエラーと隣り合わせの性質であるために意図せずに発生する技術的負債。たとえば、「私たちの設計アプローチは思っていたほどうまくいかなかった」、「プラットフォームの新しいバージョンのせいで設計の重要な部分が無効になってしまった」など。

レガシーな技術的負債

新しいチームに引き継がれた古いコードベースの技術的負債。

推奨されるアプローチ

技術的負債の種類 推奨される対処法
意図的な技術的負債(短期) ビジネス上やむを得ない場合は技術的負債を引き受け、すぐに返済する
意図的な技術的負債(長期) 必要であれば技術的負債を引き受け、返済を開始する条件を定義する
不慮の技術的負債(悪意) 作業習慣の質を高めることで、最初から技術的負債を作らないようにする
不慮の技術的負債(善意) この技術的負債は本質的に避けようがない。技術的負債の影響を監視し、「利子の支払い」が高くなりすぎたら返済する
レガシーな技術的負債 技術的負債を徐々に減らす計画を立てる

チームとしてこの種類と対処法を共通理解し、技術的負債という言葉を使っていこうと思いました。