エンジニアの佐野です。バンドルカードではポチっとチャージという後払いの機能を利用する際に年齢確認が必須となりました。通信キャリアや銀行との連携等によって年齢確認ができるようになっています*1。今回はこの機能の開発を題材に普段開発でどのようなことを考えて開発し、本機能の開発ではどのようなフローを構築して進めていったかを書きます。
少し概要を書くと、本件についてはウォーターフォールモデル "のような" 開発フローで行いました。事業上の理由でビッグバンリリースが必要でした。要件をしっかり決めてステップバイステップで開発を行いすべての機能を同時にリリースする...案件の性質を考えるとウォーターフォールが開発フローの候補の1つだと思っていたためです。ただそのまま一般的に思われているウォーターフォールを導入するのではなく、その欠点や面倒な点を解消しつつ、認識齟齬なしに設計と実装を行い、納期を死守しつつ、バグを最小限に抑えて一撃で出すにはどうするのが最適かを考える必要がありました。本記事ではその開発の計画、DB設計やシーケンス図のような各フェーズの成果物、実装方法、テスト、リリースについて書ける範囲で書きます。本件のバックエンド側はやることは比較的単純(後述しますが、簡単に言うと連携先から生年月日を取得して突合する)なのですが、リリースが遅れたりリリース後にバグると収益にかなりのダメージがある案件でした。
自分がエンジニアということもありPdM、デザイナー、エンジニアといった製作サイドの話、特に開発計画とバックエンドの話がメインになりますが、製作サイド以外の部署 (事業開発、法務、セキュリティ、CS、データ分析チーム...etc)ももちろん存在しており、リーガルチェックや連携先を含む関連各社との交渉、LP 作成やオペレーション体制の構築などに尽力してくれました。書きっぷりから私が PJ を主導して成功に導いた...ことを大々的に書いているかのように見えるかもしれませんが、自分の視点から見た PJ の進め方と要所要所で考えていたことや実際に起きたことの記録だと思ってもらえるとよいです。
- 年齢確認についての前提知識
- ウォーターフォールを考える
- 開発フロー
- 各フェーズの説明
- まとめ
1. 年齢確認についての前提知識
バンドルカードはユーザ新規登録時に生年月日を入力することになっています。この時点では自己申告の生年月日です。ポチっとチャージを利用する際はその生年月日の正当性チェックを必須とするのが今回の案件です。正当性が認められたユーザは「年齢確認済」となります。ポチっとチャージを利用する際、そのユーザが年齢確認済かどうかをチェックします。年齢確認済でなければ通信キャリアや銀行との連携等による年齢確認を促します。通信キャリアや銀行と連携する際の模式図を描くと次のような形となります。
通信キャリアおよび銀行は一般的な OIDC もしくは独自の認証・認可の仕組みを持っています。年齢確認を実施する際、バンドルカードはユーザをそれらのサイトにリダイレクトさせ、個人情報の連携に同意していただきます。それによって各社に格納されているユーザの個人情報を取得し、新規ユーザ登録時に入力された生年月日と突合することで年齢確認を行います。冒頭にも書きましたが要件としてすべての連携先を同日にリリースし、アプリの強制アップデートで全ユーザに年齢確認を提供します。いろいろな事情がありカナリアリリースや連携先を1つずつ解放することはできない案件でした。
2. ウォーターフォールを考える
話がだいぶ逸れてしまうのですが、ウォーターフォールと呼ばれている開発モデルの歴史や誤解について語っておきます。記事の頭で「ただそのまま一般的に思われているウォーターフォールを導入するのではなく、その欠点や面倒な点を解消しつつ...」と書いたように、自分の経験上、ウォーターフォールで失敗した過去や、もちろん成功した過去もあるため、これを機にウォーターフォール含め開発フローを再考して今回の PJ に活かします。
ウォーターフォールやらアジャイルやら開発フローについては「どうやってもうまくいくときはうまくいくし、ダメなときはダメ」と冷めた目で見ていた節もあるのですが...ちょっと真面目に考えてみようと思いました。
2.1 多くの人がウォーターフォールと呼んでいるもの
ウォーターフォールというのはなんなのか、ですが、多くの人は以下の様な図をイメージするかもしれません。使っている言葉に揺れはありますが概ねこのような流れで行われます。谷底に開発(コーディング) を挟んで V 字で図示して V モデルと呼ぶこともあります。双方向矢印は左側の設計フェーズと対応するテストフェーズを紐付けています。
特徴としては以下の通り、あえて悪評のようなものをここでは羅列しておきます(後で回収します)。
- 前のフェーズへの手戻りは行わない (が、往々にして要件定義の変更が後から行われてしまい大規模な巻き戻りが発生することがある)
- 各フェーズで膨大な成果物が必要
- 多くの人が嫌っている (玄人感を出したいからかとりあえず否定するワナビーもいる)
私は SI に従事していたときは疲弊した記憶はあります。ただまったくダメというイメージはありません。
ここでウォーターフォールという言葉がこの世に登場したとき、それはこのようなものを指していません。
2.2 元祖ウォーターフォール
ウォーターフォールモデルの歴史を掘っていくと次の様な文献が見つかります。
- Production of Large Computer Programs HERBERT D. BENINGTON 1956
- MANAGING THE DEVELOPMENT OF LARGE SOFTWARE SYSTEMS Dr. Winston W. Royce 1970
- SOFTWARE REQUIREMENTS: ARE THEY REALLY A PROBLEM? T. E. Bell and T. A. Thayer TRW Defense and Space Systems Group Redondo Beach, California 1976
- SOFTWARE PROCESS MANAGEMENT: LESSONS LEARNED FROM HISTORY Barry W. Boehm 1987
時系列に並べています。1が1956年の論文で SAGE*2 を開発する際の技術について述べられています。そこで次のような図が出てきます。
Production of Large Computer Programs HERBERT D. BENINGTON 1956
計画 -> 仕様策定 -> コーディング ... -> システム評価 の順に進む。V 型のウォーターフォールの図のように、コーディング (実装)を谷底として各フェーズのテストが行われています。見たことある図に近いですね...。
With SAGE, we were faced with programs that were too large for one person to grasp entirely and also with the need to hire and train large numbers of people to become programmers.
一人では把握できない巨大なプログラムに直面したこと、そして大量のプログラマーを採用してトレーニングする必要があった、と書かれており、これがこの開発手法の背景にあったと思われます。
続いて1970年にロイス博士の論文 MANAGING THE DEVELOPMENT OF LARGE SOFTWARE SYSTEMS が登場します。この論文では次の様なことが述べられています。
- すべてのプログラムには解析・分析(要求分析のようなことを表しているのだと思う)とコーディングという2ステップが存在する。複雑さやサイズにかかわらずそれが基本である。
- 小規模プログラムではそれで十分だが大規模開発では追加のステップが必要である。(要件定義、設計、テスト...)
- テストフェーズが最後の方にあるのでそこで問題が発見されると大幅な設計変更、つまり巻き戻りが発生することがある。
- ↑を軽減するために予備プログラム (A preliminary program design phase プロトタイプのことかな...)を作る、ドキュメントをがっつり書く、2回作れ (Do It Twice)、テストの計画と管理をする、顧客を関与させる
みなさんが知っているウォーターフォールについて1956年の論文のような図を引き合いに出し、その問題点と解決策を提唱しています。この時点でプロトタイピングや反復開発の概念について言及しています。それを図にしたものが次です。かなり複雑に見えます。
MANAGING THE DEVELOPMENT OF LARGE SOFTWARE SYSTEMS Dr. Winston W. Royce 1970
そして3の1976年の論文 SOFTWARE REQUIREMENTS: ARE THEY REALLY A PROBLEM? で次の一節、
The same top-down approach to a series of requirements statements is explained, without the specialized military jargon, in an excellent paper by Royce [5]; he introduced the concept of the "waterfall" of development activities.
ここで初めてウォーターフォールという文字が登場します。引用の [5] はロイスの2の論文を指し示しています。ウォーターフォールという言葉が出現したとき、それはプロトタイピングや反復開発の概念があるスタイルのものを指しています。みなさんが思っているものと違うものが「ウォーターフォール」と呼ばれています。
ではなぜこんにちの Vモデルのようなものであったり、巻き戻りはしない、という考えのものがウォーターフォールと呼ばれるようになったのかですが正直わかりません (掘れていません)。1987年の4の論文では「3の論文では2の論文がウォーターフォールであると言っているが、実際は1なんじゃないの?」と書かれていて、たしかに実際にみなさんが思うウォーターフォールはこちらの方が近いと思います。しかし1の論文はウォーターフォールとは呼ばれていません。
とりあえずですが、ウォーターフォールについては歴史的観点から細かいことを言うと現代のウォーターフォールはウォーターフォールではないと言えるかもしれません。
2.3 元祖ウォーターフォールから得るヒント
物事を真似する、参考にするにはその背景や再現性を考慮する必要があります。「一人では把握できない巨大なプログラムに直面したこと、そして大量のプログラマーを採用してトレーニングする必要があった」ようなことが書かれていたとおり、ウォーターフォールは超大規模向けに考案されたというのが背景にあります。今回の案件はそのような規模ではありません。しかしいくつか取り入れるべきヒントがあります。
- 2回やれ
元祖では全フローをもう一回繰り返せ...ということを言っています。我々は全フローではなく、巻き戻るとダメージの大きい要件定義と一部の設計を2回やることにします。軌道修正するフェーズを最初から設けておきます。
- テスト計画と管理を行う
テスト計画を要件定義の次くらい、設計の前段くらいに作り上げ、それをもとにテスト時の人員と資材を計画しておきます。テスト項目表 (=期待動作一覧表) も並行して先に作っておきます。すべてのケースの期待動作を洗い出し、それを設計、実装、そしてもちろん QA テストの前段の成果物にするという思惑です。これはテストフェーズが最後の方にあるのが問題点である、というものを解消するためです。なるべく早い段階からバグの芽を摘み取れるようにテスト項目を早い段階から作り、それを正解の仕様としてテスト駆動で開発を進められるようにします。
これは当たり前...だと私は思っていますが、ちゃんとドキュメントを書きます。エンジニア向け、エンジニア以外のチーム向けを意識したものを成果物として出します。
ちなみにこのドキュメント業なのですが...たまにドキュメントや資料の類いは無駄、不要と言う人々が存在しており、私とは全く考えが合わないのですが、少なくとも顧客に提供して長く運用を回す前提の業務アプリケーションはドキュメンテーション必須だと思っています。エンジニアチームとしては設計レビュー、コードレビューで必要になります。例えばコードレビューですが、ドキュメント不要と言っている人はコードの何を見ているのか...というのは非常に気になります。ソースコードのみてくれを確認しているとは思いますが、コードが仕様通りに書かれているかどうかを確認するのはレビューのはずで、例えばある条件のときに true を返すテストコードとその関数の実装のセットがあったとして、それ自体が正しいのか?は前段の design doc がないと判断ができないはずです。これがそのままスルーされて外部結合テストのような後続のフェーズで見つかると面倒な巻き戻りが発生し、またドキュメントがないことでバグなのか仕様なのかがわからずドツボにハマります。
昔の同僚にも不要派がいて、彼らの話を聞くとドキュメントを永遠に更新し続けるのは無理だしソースコードとドキュメントに齟齬が出る、と言うのですが、それに対しては私は対策をしています。それは設計ドキュメントと運用ドキュメントは別にすること、ドキュメントに寿命を設けること、です。設計用ドキュメントですが、必要になるのはエンジニア以外が含まれるチームでの仕様の確認時、そしてエンジニアチームで行う設計レビューとコードレビュー時です。その寿命は最初のリリースが終わるまで、とします。つまり最初のリリースが終わるまでは仕様変更や巻き戻りによってドキュメントの更新はしますが、リリースが済んだらもうそれはお役御免とします。資料として残しはしますが、有効期限は最終更新日としてそれ以降の変更についてはシステムの方が正しい、とします。設計用ドキュメントは1stリリースまでの仕様確認用であり設計レビュー用であると割り切ります。
運用ドキュメントはいわゆる Runbook のようなものであったり、CS チーム用に作る問い合わせ指南書のようなものです。こちらは運用とともに育てていく前提で永遠に更新します。Runbook を更新したり書くのが面倒であればそれこそ Runbook が不要となるようにシステムを改善すれば良いです。コードを書いて直しましょう。そうすればいくらかの運用ドキュメントは消滅させることができます。
寿命についてはドキュメントを書くのがしんどいというのであればそのハードルを下げればよいという考え方です。受託開発であれば話は別ですが、納品が発生しない前提であれば軽い口調の書き方や多少の表記揺れやミスがあっても気にしなくてもよいはずです。
ミドルウェアの開発やライブラリの開発であったり、エンジニア同志の口頭の意思伝達で済ませているチームの場合は本当に自然言語のドキュメントが必要かどうかはわかりません。私は逆にそのような組織に属したことがないので...。
この節では私の主張が少し(だいぶ?)入りました...。
3. 開発フロー
さて、長ったらしいウォーターフォールの話から元に戻りまして...開発の条件は次の通りです。
- 製作サイドのメンバー数人 (元祖ウォーターフォールの想定のような人数規模ではない)
- ビッグバンリリースが必要
- リリースでバグがあったら会社として結構マズい
以下がその際に実際に行われたフローです。上述のヒントや先人達の与えてくれた教訓を盛り込み次のようになりました。ちなみに図自体は今作った後付けです。PJ発足時に「俺はこう考えている」というのをちゃんと示しておいた方がよかったかもしれない...。
3.1 考案した開発フロー
時系列は概ね左から右です。双方向矢印は工数として考慮済の手戻りを表しています。実装と設計を行き来しないわけがない、テストでバグが見つかって実装に手戻りが発生しないわけがない、というような...。
3.2 現実の開発フロー
ここで現実を書いておきます。計画通りにいったら苦労はしない。
赤入れした部分が計画崩れした部分や想定と変わってしまった部分です。
- 連携先との疎通テスト: 連携先との疎通確認は早々に終わらせてしまい、インフラ面でのハマりは早々に解消したかったのですが...。TCP/IPレベルでの疎通はある程度はできたもののアプリケーションレイヤでの疎通は QA テストのときにいきなり結合する形となってしまった。そしてそこで早めに疎通試験できていれば見つかったであろうバグを見つけるなど...。
- ER図とアプリ/サーバ間のAPI: これは元から手戻りはあるだろうと思っていたが本当にちょっとしたミスで手戻りがあった。APIが1本足りなかった、連携先から取得した生年月日は保存しない算段だったが保存が必要だった、etc...。図を見てわかるとおりシーケンス図は2回やったこともあり手戻りはほとんどなく、ここで「2回やれ」の効果が奇しくも出ることになった(2回やったから大丈夫だったと言い切ることはできないが)。
- 一部仕様変更: とある連携先だけ年齢確認時の挙動を変えることになった。こちらも幸いダメージは少なかったが...。
- リリース: ビッグバンリリースはいうことになっていたがとある連携先だけ先出しする形になった。ちなみにこれはいくつかの連携先の開発が遅れているので先に出せるところだけ...といったネガティブな理由ではなく、むしろ先に一部の連携先を開放してユーザにインセンティブを付与する+ユーザの反応や動きを先に分析したいという戦略上の要因です。
- 俺の離脱: そう俺が PJ から外れる。他案件の兼ね合いで。QAテストの前あたり。各社との連携の実装までは完了したが、後から追加で出現した年齢確認手段の要件定義〜設計〜実装、QAテスト以降のフェーズを残して俺がこの案件からいなくなった...。フェーズが巻き戻ることや想定外の要件が後から出てくるといったようなものは気にしていたのですが、メンバーチェンジ...まあそれもあるっちゃあるか、と思ってはいたがそしてそれが割とメインの自分...うーむ、これはさすがに考えていなかった...。自分が離れたあとのバグの発生がチャットに流れてくるとすまねえなあ...と、なんとも言えない気持ちになるなど...入れ替わったメンバーが完遂してくれました。
4. 各フェーズの説明
以下、それぞれのフェーズで何を行い、どのようなことを考えていたかを書きます。ここから先は多少はテクニックの話も出てきます。
4.1 要件定義 (1回目)
まずはやりたいことを PdM にまとめてもらいます。こちらについては PJ キックオフ前から経営層と PdM が水面下で進めていました。自分の進言としては、上のフロー図のとおり、連携先の仕様確認と画面設計のあとにこれをやり直す (再精査する)べきであると告げました。この時点ではどのような開発フローで進めるかはふわふわしていました。
4.2 画面設計 (1回目)
チーム内では仮UIと呼んでいました。こちらも後続のフェーズからフィードバックする前提のアイテムです。まずは素直に我々が実現したいこと (≠できること)を図にします。
4.3 連携先仕様調査
1回目の要件定義と仮UIこと画面設計をやりつつ連携先の仕様書を精読します。最初に仕様書読めば?という人がいるかもしれませんが先方の仕様書も膨大です。やりたいことを決める -> やりたいことに関連した箇所を読んで実際にできるかどうかを調べる、といった流れで要件定義と仮UIにフィードバックします。この仕様書からインフラ設計に必要な情報や非機能要件も読み取っておきます。また先方に質問を投げるなどして仕様の確認をする、結合テストする際の制約事項*3のスケジュールについても洗い出しておきます。
4.4 シーケンス図 (1回目)
仕様書の情報から1回目のシーケンス図を書きます。ここでは要件定義と画面設計にフィードバックするという目的があるのでそれを意識した書きっぷりにします。粒度としても細かいパラメータの表記は最低限として、PdM などエンジニア以外の職種にも理解してもらえる表記にします。以下のような成果物です。図そのものも重要ですが「※」マークで書いた箇所、「これを決める必要がある」「この画面が追加で必要である」「こういう遷移を考慮する必要がある」といった情報を洗い出します。
4.5 要件定義 (2回目)とスケジュール決め
仕様調査と仮UI、先ほどの1回目のシーケンス図をもとに再度要件定義を行います。ここでスケジュール線表も引きます。線を引くのが私、バックエンドの人間ということもあり、バックエンド観点のものになってしまうため、他メンバーともすり合わせをします。またここで引いたスケジュールは我々製作サイドの考えたスケジュールでもあるのでこれを PdM から経営層にぶつけてもらい、そちらともすり合わせを行います。
ところでものの本によると一流は納期を決めないらしいですが私は三流なので納期は決めます...。と、まあ人様の本を揶揄するつもりはなく、カンムのこの PJ では会社としてリリース日に大きな意味がありました。ということでこちらの案件については納期を決めて守る必要がありました。納期に大きな意味がなければ自分も、完成したら出します、くらいにするかもしれません。
4.6 画面設計 (2回目)
必要になった画面をデザイナーおよびフロント担当にフィードバックして画面遷移図を再描画します。
4.7 期待動作洗い出し
これがコアです*4。仕様書の読み込みからシーケンス図(1回目)を描き、要件定義、画面設計の精度が高まったらここで正常系、準正常系、異常系すべての期待動作を洗い出します。ある状態のユーザがある端末である操作を行ったときの、画面遷移、DBの状態変化まで洗い出しておきます。DB 設計はこの後段のフェーズで行うのでここでは「年齢確認済となる」というような表現になっています。
これは設計のインプットであり実装のインプットであり QA テストのテスト項目であり...すべての動作を網羅した仕様書になります。CS チームにも共有することで何が仕様で何がバグかを明らかにすることもできます。今確認すると150パターンほどありました。大変ではありますが事前に苦労しておくには十分価値のある大変さではあります。複雑な案件ではさらにパターンが増えるでしょう。
4.8 連携先 API クライアント&モック開発
こちらについては連携先の仕様書が手に入り次第単独で取りかかれるアイテムです。1回目の要件定義が完了すればやろうとしていることの概略はわかるので、それを実現するためのクライアントを早々に書き始めます。Testability を上げるため、またサーバ本体に組み込んだときに実際に連携先と接続しなくても動かせるようにモックできるようにしておきます。これはあたりありふれた手法でありここで改めて言うほどのことではないとは思いますが...。
// TokenRequester is the interface that wraps the token Request method type TokenRequester interface { Request(ctx context.Context, tokenEndpoint, code string) (*TokenResponse, error) } // TokenClient is client for token type TokenClient struct { ... } // NewTokenClient returns new TokenClient func NewTokenClient(config *TokenConfig) TokenRequester { return &TokenClient{ ... } } // Request requests token func (c *TokenClient) Request(ctx context.Context, tokenEndpoint, code string) (*TokenResponse, error) { ... }
4.9 インフラ設計
連携先の仕様書をインプットとしてインフラ設計を行います。先方から配られるクレデンシャルであったり、セキュリティ面などの非機能要件や接続要件をインフラに落とし込みます。また本 PJ ではこちらのシステムの情報を先方に出す必要もあったためそれもこのフェーズでまとめておきます。
4.10 インフラ構築
実際の構築です。今回はそれほど土台の工事は必要ではありませんでした。概ね以下のような作業でした。
4.11 連携先との疎通テスト
先にやってしまおうと思っていた疎通テストです。上の方にも書きましたが当初の計画よりかなり後ろのフェーズでの確認になってしまいました。QA テストのタイミングで以下のようなバグが見つかるなど...。
- 連携先の仕様書に変更があり通信時に応答が返らない (変更に気づかず...)
- 単純に自分の書いた API クライアントにミスがあった
4.12 シーケンス図 (2回目)
今度のシーケンス図の粒度は自分以外のエンジニアとセキュリティエンジニアが理解できるように書きます。レビュー待ちが開発のボトルネックにならないようにするため、本PJに参画していなくてもコードレビュー可能なよう、この設計図自体もレビューにかけつつ、PJ外メンバーには内容を理解してもらいます。
「おおまかなシーケンス図」がシーケンス図(1回目)のことで「期待動作」がその名の通り期待動作洗い出しの成果物です。これらをインプットとして数々のパターンを網羅する形でシーケンス図を書きます。
4.13 ER図
期待動作洗い出しで DB の状態が定まったらデータチームからの要件も盛り込んで DB 設計を行います。ポイントとなるテーブルだけ抜き出すと次の様な設計としました。
- ユーザ: 既存テーブル。このテーブルには変更なし。
- 年齢確認リクエスト: 新規作成。どのような年齢確認手段(銀行、通信キャリア)においても必ず作られる。年齢確認の起点となるテーブル。ユーザは何回も年齢確認を行う可能性があるため、ユーザ:年齢確認リクエスト = 1:N となる。
- 年齢確認リクエスト_手段A_固有情報: 新規作成。それぞれの年齢確認手段を行う際の固有のデータを格納するテーブル。年齢確認リクエストは各手段で0回(途中でアプリを落とすなど)または1回行われるため年齢確認リクエストと1:0..1。
- 年齢確認リクエスト_手段A_実施: 新規作成。実際に年齢確認処理を行った際に記録されるテーブルで、
年齢確認リクエスト_手段A_固有情報
と JOIN することでリプレイアタックが行われたかのチェックにも使う。年齢確認リクエストと1:0..1。 - 年齢確認リクエスト_手段A_取得した生年月日: 新規作成。連携先から取得した生年月日を格納するテーブル。年齢確認リクエストと1:0..1。
- 年齢確認_生年月日差異: 新規作成。連携先から取得した生年月日とカンム側に格納されているユーザの生年月日が異なっていた場合に作られる。年齢確認リクエストと1:0..1。
- 年齢確認_完了:新規作成。連携先から取得した生年月日とカンム側に格納されているユーザの生年月日が一致していた場合に記録される。年齢確認リクエストと1:0..1。
ポイントは以下の通り。
- 積み上げ式とする: 自分がこのような設計を好んでいるというのもありますが delete と update はしない方針とします。insert で状態を表現していきます。これはデータは存在していることに価値があるためです。ユーザ分析の要件としてもユーザがどの手段でどこまで年齢確認を進めたか?(ex: ある通信キャリアで途中まで進めたが離脱、後に銀行で年齢確認を試みて成功、など...)を知る必要があるためユーザの行動をトレースできるような設計としておきます。
- 年齢確認手段が増えた際に対応できるようにする: 拡張性です。実は本人確認書類のアップロードによる年齢確認も可能なのですが、 PJ 発足当時はやるかやらないかが揺れていて、後続のフェーズになってようやく「やるかも?」くらいの状態になっていました。ここで本人確認書類のアップロードを含め、新しい年齢確認手段が増えたら手段Bのような形で年齢確認リクエストテーブルを起点にリレーションを生やす戦略としました。
投げる SQL とインデックスもここで示しておきます。テーブル設計は済んだが SQL が無茶になりすぎたら厳しい気持ちになるのでここで面妖な SQL にならないかの確認もしておきます。
4.14 アプリ/サーバ間の API 設計
バンドルカードのアプリとサーバの間の API 設計を行います。期待動作洗い出しとシーケンス図 (2回目) をインプットに API スキーマを考えます。上の図で「アプリ/サーバ間の API 設計」が ER 図とシーケンス図と同列になっているのは間違いですね...。
4.15 セキュリティレビュー (設計編)
1回目のセキュリティレビューを行います。シーケンス図、ER図、API設計をセキュリティエンジニアに見てもらいます。連携先の仕様書にセキュリティガイドラインも書かれているケースもあるので、それをクリアできているかどうかもチェックしてもらいます。
設計のタイミングでセキュリティエンジニアに入ってもらうのはとてもよいです。セキュリティエンジニアに最初からこのタイミングでレビューをしてほしいので工数もらっていいですか?と声をかけておいてよかった。
4.16 実装
セキュリティレビューのフィードバックを設計に盛り込むと、期待動作、各種設計、連携先のAPIクライアントが出揃います。ここでようやく本体の実装が可能になります。例えば年齢確認を実行する API エンドポイントについて、あらかじめまとめた期待動作を網羅する形でテストを書いていきます。期待動作をまとめてあるため比較的容易に TDD を行うことができます。このテストをパスするように本体の実装を書くことで、PdM やデザイナーやフロント担当などチーム内で決めた期待動作を実装の時点で結構な割合で保証することができます。
t.Run("誕生日一致", func(t *testing.T) { ... res, err := executeAgeVerificationRequestDocomo(ctx, app, queryParams) assert.NoError(t, err) assert.Equal(t, constant.AgeVerificationSuccessURL, res) status, err := service.GetAgeVerificationRequestStatusByRequestIDAndUserID(ctx, tx, avr.ID, avr.UserID) assert.NoError(t, err) assert.Equal(t, constant.AgeVerificationRequestStatusCompleted, status) avred, err := model.GetAgeVerificationExecDocomoByPkContext(ctx, tx, avr.ID) assert.NoError(t, err) assert.NotNil(t, avred) t.Logf("age_verification_exec_docomo: %+v\n", avred) diff, err := model.GetAgeVerificationBirthdayDiffDetectionByPkContext(ctx, tx, avr.ID) assert.Error(t, err) assert.Nil(t, diff) t.Logf("age_verification_birthday_diff_detection: %+v\n", diff) completion, err := model.GetAgeVerificationCompletionByPkContext(ctx, tx, avr.ID) assert.NoError(t, err) assert.NotNil(t, completion) t.Logf("age_verification_completion: %+v\n", completion) userInfo, err := model.GetAgeVerificationIdentificationDocomoByPkContext(ctx, tx, avr.ID) assert.NoError(t, err) assert.NotNil(t, userInfo) t.Logf("age_verification_identification_docomo: %+v\n", string(userInfo.UserIdentificationData)) bd, _, err := service.GetAgeVerificationBirthdayDocomoByRequestID(ctx, tx, avr.ID) assert.NoError(t, err) assert.Equal(t, time.Date(2001, 4, 1, 0, 0, 0, 0, time.UTC), bd) } t.Run("誕生日不一致", func(t *testing.T) { ... } t.Run("認証セッションがタイムアウトした", func(t *testing.T) { ... } t.Run("ユーザが同意しなかった", func(t *testing.T) { ... } t.Run("情報が存在しなかった", func(t *testing.T) { ... } t.Run("法人契約端末が使われた", func(t *testing.T) { ... } t.Run("レスポンスが返ってこなかった", func(t *testing.T) { ... } t.Run("メンテナンス中", func(t *testing.T) { ... }
4.17 セキュリティレビュー (実装編)
セキュリティのプロに実装の方も目を通してもらいます。セキュリティホール、ログに出したらいけないものを出そうとしていないか、セキュリティレビュー (設計編) で指摘された事柄がクリアされているかを確認してもらいます。
4.18 QA テスト
度々登場する期待動作一覧の表をもとにひたすらテスト -> 修正を繰り返します。このフェーズの前あたりに自分は離脱...。横目でバグの発生を見ながら申し訳ない気持ちになるなど...。
4.19 リリース
もはや私はいないのですが PdM がリリース体制を構築してくれました。対ユーザ向けにはバグなしで機能を提供できたはずです。 最終的なリリース日時について「その日・時間帯は避けろ」と横から口出しをしたくらいです。
5. まとめ
開発フローに以下のようなことを導入して一定の効果が得られました。
- ウォーターフォールを見直して先人のヒントを盛り込むことである程度円滑に PJ を進めることができた。
- 「2回やる」を局所的に導入した。局所的な導入でも今回については効果があり、要件定義の考慮漏れが回収された。
- 早い段階からテスト計画を練り、期待動作の一覧を網羅したものを作った。その期待動作一覧を正解の仕様として設計、実装、リリース後の仕様/バグ確認として活かすことができた。
- 上記により TDD が容易になった。
- セキュリティレビューを早い段階から2回行い、セキュリティの観点からの修正を早期に行うことができた。
カンムのチーム環境としては以下の通りです。PJ運営をする際に製作サイドから見てしがらみが少なかったのは開発フローの柔軟性に大きく寄与されていると思われます。
- 職種間のパワーバランスがそこまで大きくなく要件を決めたりスケジュールを引く権限が現場にあった
- 開発スタイルは担当者に委ねられている
- 必要に応じて各職種を巻き込むことができる
開発フローの策定においては開発するものの特性、難易度、規模、チームの状況、会社の事情、自社内と他社を含んだステークホルダーのパワーバランス、個々のスキル...といったパラメータが複雑に絡み合っています。これらを考慮して最適なフローを構築することが必要です。世の中によく知られた既存の開発フロー、例えば本記事でも挙げたウォーターフォールですが、半世紀以上前に考案されたものをそのまま適用するのは無理があります*5。当時の状況を鑑みて再現性を考慮する必要があります。今回の我々の PJ もたまたまうまく適合したのかどうかは振り返りを行い、成功に再現性のある事柄は他チームに展開するなどして自チームにあったスキームを育てていく必要があります。
おわり
*1:ポチっとチャージの年齢確認って?: https://support.vandle.jp/hc/ja/articles/31207907137817-%E3%83%9D%E3%83%81%E3%81%A3%E3%81%A8%E3%83%81%E3%83%A3%E3%83%BC%E3%82%B8%E3%81%AE%E5%B9%B4%E9%BD%A2%E7%A2%BA%E8%AA%8D%E3%81%A3%E3%81%A6
*2:おそらくこちら: 半自動式防空管制組織 https://ja.wikipedia.org/wiki/%E5%8D%8A%E8%87%AA%E5%8B%95%E5%BC%8F%E9%98%B2%E7%A9%BA%E7%AE%A1%E5%88%B6%E7%B5%84%E7%B9%94
*3:負荷試験や不可、異常系はやってはいけない、端末はカンムで用意、などテスト時に必要な情報が洗い出される
*4:振り返ってみるとこれをこのタイミングでしっかり作ったことがかなり大きかったと思っています
*5:古いからダメというわけではない