振る舞い駆動開発

speed

はじめに

レバレジーズ株式会社でフロントエンドエンジニアとして勤めている森山です。

今回は振る舞い駆動開発(Behavior Driven Development、通称BDD)をご紹介します。 ここでの振る舞い駆動開発とはDan North氏が提唱しているBDDの要点を掴み、自チーム流に昇華させたものです。

振る舞い駆動開発を導入して最も良かったことは、アジャイルにおいても開発スピードを保ちつつ一定の品質を担保できたことです。 一定の品質というのは機能適合性が担保されているということです。

また振る舞い駆動開発をすすめていく中で副次的に開発者のプロダクトへの理解度や視座が高まり、より良いプロダクトを作ろうという意思が一人ひとりに芽生えたと感じています。

概要

振る舞い駆動開発の目的は以下です。

  • 開発スピードを維持しながら一定の品質担保

振る舞い駆動開発の手順は以下になります。

  • ユビキタス言語の定義
  • ユーザーストーリーの定義
  • ユースケースの定義
  • テストケースの定義
  • プロダクト実装
  • テスト実装

振る舞い駆動開発ってなに?

そもそも私が振る舞い駆動開発をどう定義しているのか説明します。

観点としては以下になります。

  • 目的はなに?
  • 振る舞い駆動開発ってなに?
  • テスト駆動開発とは何か違うの?
  • 振る舞いってなに?

目的はなに?

開発スピードを維持しながら一定の品質担保です。

振る舞い駆動開発ってなに?

端的に言うと「システムの振る舞いに焦点を当てたテスト駆動開発」です。
一般的に振る舞い駆動開発はテスト駆動開発の1つの流派として捉えられています。

テスト駆動開発とは何が違うの?

テストの対象が異なります。

イメージとしては、テスト駆動開発よりも振る舞い駆動開発の方がテスト粒度が荒く、網羅性が低いです。
振る舞い駆動開発はその名の通りシステムの「振る舞い」のみをテストの対象としています。

一方、テスト対象以外はテスト駆動開発と振る舞い駆動開発もテストファーストな開発手法としては同じです。

テストファーストとある通りテストコードを先に書いてから、
そのテストコードをパスするような実装とリファクタリングを繰り返します。

テストファーストは以下のメリットがあります。
致命的な欠陥を開発終盤に持ち越さない。
開発者がシステム要件を深く理解できる。
デグレードを恐れずリファクタリングができる。

ただテストファーストな開発はメリットだけではないと思っています。

以下のような難点も存在します。

  • プロダクトコードのみを書く場合よりも開発が遅い。
  • テストすべき内容や粒度をどのように判断するのか難しい。
  • 仕様が変更された時の影響が大きい。

上記はウォーターフォール開発であればさほど目立ちません。
そもそも短期間で成果を出すことは求められていないですし、
テストで担保したい基準も最初に決定するはずです。
また手戻りがあるとしても要因は考慮漏れくらいで、外的要因による仕様の変更も少ないはずです。
つまりテスト駆動開発の恩恵を存分に享受できます。

しかし上記の難点は、アジャイルな開発だと顕著になります。
特に課題に上がるのは以下です。

  • スプリント期間内でアウトプットを出す難易度が高い。
  • 機能毎にテストの粒度を判断するコストが高い。
  • 仕様変更による手戻りの影響範囲が大きい。

プロダクトコードと合わせてテストコードも書く必要があるので、短期的に見ると開発スピードは落ちます。
そのためスプリント期間内に動くものをアウトプットする難易度が上がります。

またテストはどの粒度で定義するのかという迷いも生じます。
最初に決めることができればよいですが、それも難しいはずです。
なぜなら、システムの最終ゴールが分からないからです。
そもそも開発当初にシステムの最終ゴールが見えないために、アジャイルな組織体制で市場の変動等に適応していく手段を選択していたりするのかなと思います。
テスト粒度の線引が完璧にできないので、機能毎に都度どこまで網羅的にテストをするのか判断していくことになります。
例えば、「データを描画するだけならこの程度で良いか。」、「決済機能は重大なインシデントに繋がるからカバレッジをできる限り高めよう」などです。
また過去に実装した時は適切な厳密さでテストができていたが、機能拡張で再度判断が必要になることもあります。
スプリントの中で都度テストの厳密さを適切に判断していくというのはコストが高いです。

さらに手戻りが発生した時の影響範囲も大きいです。
改修範囲がプロダクトコード + テストコードになるので当然ではありますが、手戻りの改修がしんどくなるのはアジャイルの良さが失われてしまいます。

とはいえテストファーストで一定の品質を保ちつつ開発者のシステム要件の理解度を上げていきたいので、アジャイルで開発しつつもテスト駆動開発の旨味を享受するためにテスト対象として「振る舞い」に着目します。

振る舞いってなに?

振る舞いとは、ユーザに価値を提供できるシステムの動きです。

極端な例ですがECサイトを考えます。
ユーザーが実現したいことはオンラインで欲しい物を購入できることです。
その過程として、「商品をカートに入れる」はユーザが実現したいことを叶えるためのシステムの動きです。
しかし「ボタンを連打できない制御」が施されていたとしてもそれはユーザーが実現したいことに密接には紐付きません。

「商品をカートに入れる」ことができれば、ボタンの連打制御は無くてもいいです。
あれば嬉しいなくらいです。
端的に言えば機能要件よりも非機能要件を重視したいということになります。

またこれは私の持論ですが、困りごとが解決できる手段があれば多少使い勝手が悪くても嬉しいです。
確かに入念なテストをパスしたソフトウェアは安心感があります。
しかし多少レイアウトが崩れていたり、フォームにバリデーションがない状態になっていたとしても課題を解決できる手段があることの方が圧倒的に嬉しいです。

ソフトウェアが提供する価値の種類にもよるので一概にも言えませんが、アジャイルな開発で求められるソフトウェアは上記がほぼ当てはまるのかなと思っています。
つまりテストをする際にもまずは動くことを担保するテストを優先させたいということです。

ただ単純にテストという言葉に引っ張られると大抵「xxxであるべき」という文言を使って理想状態を定義します。
そしてそれを満たしているかテストコードを書いていきます。
「xxxであるべき」となると各テスト項目はビジネスインパクトに関わらず、どれも等しく満たしている必要があるように見えます。
そしてテストを書いていく中でも「xxができていない。」、「yyは担保しなくていいのか?」と不安にも駆られます。

開発者としても「システムとしてあるべき状態を担保するテストを書け」と言われると際限がないように思えます。
しかし「ユーザが実現したいこと(振る舞い)を担保するテストを書け」と言われると自然と機能要件に着目しそれ以上を闇雲に目指すことは無いはずです。

もう一度、この章の冒頭の言葉を引用すると「振る舞いとは、ユーザに価値を提供できるシステムの動き」です。
この振る舞いにフォーカスすることで、アジャイルで開発していく中でも特に必要な機能をテスト対象として選定できます。

また「振る舞い」はコード上ではシステマチックに定義されます。

振る舞いひとつの構成は以下です。

  • Given: システムがこの状態の時に
  • When: この動作をすると
  • Then: システムはこう振る舞う

詳しくは後述しますが、振る舞いは1つのユーザーの1つのアクションに対してシステムがどう動くのかを定義します。
そしてこの振る舞いが複数集まることでユーザーがどの順番で、何を成し遂げたいのかも見えてきます。

振る舞い駆動開発の手順

振る舞い駆動開発の手順は以下になります。

  • ユビキタス言語の定義
  • ユーザーストーリーの定義
  • ユースケースの定義
  • テストケースの定義
  • プロダクト実装
  • テスト実装

上記の手順が生まれた背景・思考プロセスはざっくり以下です。

  • 一定の品質を保ちながら開発をすすめるためにテストファーストな開発をしたいな。
  • テストファーストな開発をするためには、何をテストすべきか決まっている必要があるな。
  • 何をテストすべきか決めるにはシステムがユーザーに提供したい価値や手段を明確にする必要があるな。
  • ユーザーに提供したい価値を明確にするためにはユーザーストーリー、ユースケースの定義が必要だな。
  • ユーザストーリーやユースケースを開発者やPDM等、誰が読んでも一律同じ解釈ができるためにはユビキタス言語の定義が必要だ。

上記の背景・思考プロセスを逆転させて振る舞い駆動開発の手順となっています。

次章から各手順でどんなことをしているのか説明します。

ユビキタス言語の定義

ユビキタス言語とは

開発者やPDMを含むチーム全体の共通言語のことです。

例えば、会社を表現する時にそのまま「会社」という表現を使うのか、「企業」という表現を使うのかをチームの中で決めておくということです。 またプロジェクト独自の概念や造語も含まれます。

ユビキタス言語を定義する目的

目的は以下の3つです。

  • コードやドキュメントを誰が読んでも一律同じ解釈ができる状態にすること
  • システム上での表記揺れを無くすこと
  • 新規参入者のオンボーディング

ユビキタス言語の定義方法

定義方法は簡単で、テーブル形式で記載しています。
以下がその一例です。

ドキュメント 類義語 プログラム単数 プログラム複数 意味
企業 会社、組織 corporation corporations 利益を得ることを目的にして事業を行う組織体のこと。
引き直し 再登録 paymentReload - サブスクリプションサービスを解約していた人が再契約した時に決済予定が記録されること。

各カラムについて説明します。

ドキュメント

開発者やPdMが会話上やドキュメント上で活用していく表現です。

類義語

端的に言うとNGワードのような扱いです。
定義されたユビキタス言語が「表記ゆれを起こすとしたらこんな表現だろう」という言葉が類義語というカラムに記録されます。
なので類義語を見つけたら開発者やPdM間で指摘し合います。

プログラム(単数/複数)

コード上で変数名やテーブルのカラム名として活用されます。
プログラム上でもユビキタス言語が無いと企業idをフロントエンドではcorporationIdと定義しているのに、
バックエンドではcompanyIdという命名で定義しているということが起きてしまいかねません。

念の為複数形も定義しておくと良いです。
基本的には英語の複数形のルールに従えばOKですが、プロジェクト独自の概念や造語があるとブレそうになる場面があります。
概念として複数形が存在しない時はハイフンを入れています。

意味

その単語の意味です。

ユビキタス言語の恩恵

ユビキタス言語の恩恵としては、コードやドキュメント上で顕著に発揮されています。

特にコード上で、企業idをフロントエンドではcorporationIdと定義しているのに、バックエンドではcompanyIdという命名で定義しているというような場面を稀に見かけます。

これの悪いところは、変数名を変更するという改修が顧客に対して価値を生み出さないところです。
ビジネスインパクトが弱いからという理由で修正が後回しにされていくことが多いです。
しかし個人的にこれは大きな技術的負債です。
見るたびに脳内でcorporationId = companyIdという変換でメモリが食われて本当に考えたいことに100%のリソースを避けないことが永遠に続きます。
たった1つであれば問題ないと考えているとどんどん増えるので見かけたら即直してユビキタス言語を更新する方が良いと思っています。

ユビキタス言語は新規開発者のオンボーディングでも役に立ちます。
ユビキタス言語の一覧表を眺めるだけでそのシステムが何をしたいシステムなのか雰囲気をさっと掴むことができます。

ユーザーストーリーの定義

ユーザーストーリーとは

ユーザーストーリーとは、ユーザーができるようになりたいことをまとめたものです。 システムが何を提供すべきかではなく、ユーザーが何をしたいかに焦点を当てます。

ユーザーストーリーの目的

ユーザーストーリーの目的は開発するシステムに引っ張られずにユーザーに提供したい価値を見える化することです。 ユーザーストーリーがあることで開発者は「振る舞い」を実装・テストする際にそもそもその振る舞いは何を実現したいのか?を見極め自分自身でシステムとして適切な機能なのか有効なテストなのか判断できます。

ユーザーストーリーの構成

ユーザーストーリーは以下の要素から構成されています。

  • アクター
  • エピック
  • ユーザーストーリー

それぞれ何を意味するのか説明します。

アクター
ユーザーストーリーに登場する人物の列挙と各人の説明です。

エピック
ユーザーストーリーの骨子です。
エピックはユーザーストーリーよりも1段抽象度が高い概念です。
複数のユーザーストーリーのまとまりがエピックです。

ユーザーストーリー
ユーザーのニーズと背景が2つセットで表現されます。
ユーザーストーリーは箇条書きで記載しています。

ユーザーストーリーの具体例 例としてフリマサイトのユーザーストーリーを書いてみます。

アクター

  • 出品者
    • 商品の出品者
  • 購入者
    • 商品の購入者

エピック

  • 出品者は、商品を出品する。
  • 購入者は、商品を検索する。
  • 購入者は、商品の詳細を確認する。
  • 購入者は、商品をお気に入り登録する。
  • 購入者は、商品を購入する。

ユーザーストーリー

  • 出品者は、商品を出品する。
    • 出品者は、出品する商品の画像をアップロードする。
      • なぜなら、商品がどんな商品なのか購入者に画像で知らせたいから。
    • 出品者は、出品する商品の価格を設定する。
      • なぜなら、商品をその価格で販売したいから。
    • 出品者は、出品する商品の詳細情報を設定する。
      • なぜなら、商品の画像で伝わらない商品の情報を購入者に伝えたいから。
    • 出品者は、商品の出品を完了する。
      • なぜなら、設定した内容で商品を出品したいから。
    • 出品者は、自身の出品リストを確認する。
      • なぜなら、先ほど出品した商品が出品できているか確認したいから。
      • なぜなら、出品した商品の設定内容に誤りが無いか再確認したいから。
  • 購入者は、商品を検索する。
    • 購入者は、
    • …etc

上記のように箇条書きで羅列する形式でユーザーストーリーを書いています。

ユーザーストーリーの定義方法

定義方法の要点は3点あります。

  • フォーマット
  • 行動はシステムを介さないユーザー視点
  • いきなりユーザーストーリーを書かない

1.フォーマット
フォーマットとしては、以下の体裁を守っています。

  • [アクター]は、xxxする。
    • なぜなら、yyyしたいから。

ユーザーのニーズと背景が必ず一対一もしくは一対多で紐付く形式にするためです。

2.行動はシステムを介さないユーザー視点
商品の出品完了に関しては「商品の出品ボタンをクリックする。」ではなくわざと「商品の出品を完了する。」のようにやや抽象度の高い表現になっています。
些細な違いですが、「出品ボタンをクリックする。」になっているとこの時点で出品ボタンがある前提のUIが設計されます。
「完了する。」にしてUI決定の余地を残しておくことで、もしかしたら上にスワイプすることでリッチなアニメーションと共に出品が完了するようなUIになるかも知れません。

また場合によってはシステムが介入しないユーザーストーリーも存在しえます。
例えばシステム上のデータをプリンターでプリントアウトする。等です。
労務系の業務システムであれば帳票を印刷して行政に提出するようなアナログな動きが業務の中に含まれることもあり得ます。

3.いきなりユーザーストーリーを書かない
ユーザーストーリーをいきなり箇条書きで列挙するとユーザーにはそのニーズ以外を満たす余地を与えないタスク指向のシステムになってしまいやすいです。
事前にユーザーストーリーマッピングやオブジェクト思考UIを絡めて定義していくのが良いです。

ユースケースの定義

ユースケースとは

ユースケースとは、端的に言うとユーザーストーリーを具体化したものです。
ユーザーストーリーを実現するためのユーザーとシステムのインタラクティブな動きを表現するものです。
ユーザーストーリーとの違いは、システムが介入するかどうかです。

ユースケースの目的

ユースケースの目的は、ユーザストーリーを実現するための画面上での遷移や操作を表現することです。

単にシステムがどう振る舞うかのドキュメントではなく、「ユーザーストーリーを実現するための」というところが重要です。
システムの振る舞い1つ1つがユーザーストーリーを実現するために適切な動きなのか、適切な導線設計なのか、開発者からも深ぼって確認することができます。

ユースケースの構成

ユースケースは以下の要素から構成されています。

  • アクター
  • エピック
  • ユースケース

それぞれ何を意味するのか説明します。

アクター
ユースケースに登場する人物の列挙と各人の説明です。

エピック
ユースケースの骨子です。
エピックはユースケースよりも1段、抽象度が高い概念です。
複数のユースケースのまとまりがエピックです。

ユースケース
ユーザーのアクションとシステム挙動が2つセットで表現されます。

ユースケースは箇条書きで記載しています。

ユースケースの具体例

例としてフリマサイトのユースケースを書いてみます。

アクター

  • 出品者
    • 商品の出品者
  • 購入者
    • 商品の購入者
    • システム

エピック

  • 出品者は、商品を出品する。
  • 購入者は、商品を検索する。
  • 購入者は、商品の詳細を確認する。
  • 購入者は、商品をお気に入り登録する。
  • 購入者は、商品を購入する。

ユースケース

  • 出品者は、商品を出品する。
  • 出品者は、フリマアプリにアクセスする。
    • システムは、ログイン画面を表示する。
  • 出品者は、ログイン情報を入力する。
    • システムは、トップページを表示する。
  • 出品者は、「出品ボタン」をクリックする。
    • システムは、出品画面に遷移する。
  • 出品者は、「商品の画像アップロードボタン」をクリックする。
    • システムは、OSのファイルシステムを起動する。
  • 出品者は、出品したい画像を選択する。
    • システムは、アップロードされた画像を表示する。
  • 出品者は、商品の価格に3000と入力する。
    • システムは、商品の価格を3000と表示する。
    • システムは、商品の手数料を300と表示する。
    • …etc
  • 購入者は、商品を検索する。
    • 購入者は、
    • …etc

上記のように箇条書きで羅列する形式でユースケースを書いています。

ユースケースの定義方法

定義方法の要点は3点あります。

  • フォーマット
  • ユーザーストーリーと紐ついていること
  • ケースの網羅は不要

1.フォーマット
フォーマットとしては、以下の体裁を守っています。

  • [アクター]は、xxxをする。
    • システムは、yyyする。

2.ユーザーストーリーと紐ついていること 基本的にユースケースは何かしらのユーザーストーリーと一対一で紐ついています。
また、エピックも同様です。
ユーザーストーリーで定義されたエピックは、ユースケース上にも存在します。

3.ケースの網羅は不要 基本的にユーザーストーリーを実現する手段は細かく見ると複数存在します。
例えば、「出品者は、画像をアップロードする」というユーザーストーリーでも以下の3種類のアクションが考えられます。

  • デバイスのカメラを起動して撮影してアップロード
  • OSのファイルシステムを起動してアップロード
  • ドラッグ&ドロップでアップロード

ユースケースはユーザーストーリーを実現するための詳細な挙動なので3パターンのユースケースが想定できますが、
基本的には最も発生頻度が高そうなケースで1パターン書かれていれば良いです。
ユースケースの主たる目的としてユーザーストーリーという1つのフローを実現するための大まかな動きを確認することだからです。

テストの定義

テストケースとは

ここでのテストケースとは、振る舞いをシステマチックにテストケース化したものです。

振る舞いはユースケースの一つ一つから以下のようなフォーマットで作成されます。

  • Given: システムがこの状態の時に
  • When: この動作をすると
  • Then: システムはこう振る舞う

上記の様にGherkin記法と呼ばれる書き方で記載します。

  • Given = 前提条件
  • When = ユーザーのシステムに対する動作
  • Then = ユーザーの動作に対するシステムの挙動

Given、When、ThenのそれぞれをStepと呼びます。
またこれらのStepの塊をScenarioと呼びます。

そして1つのユースケースが1つのScenarioとしてテストケース化されます。

テストケースの具体例

テストケースの具体例としては以下になります。

Feature: 商品の出品

    Background: 前提条件
        Given ログインしていること

    Scenario: 出品の開始
       Given トップページにアクセスしていること
        When 「出品」ボタンをクリック
        Then 商品の出品画面に遷移していること

    Scenario: 商品価格の入力
       Given 商品の出品画面にアクセスしていること
        When 商品の価格フォームに3000と入力
        Then 商品の価格が3000と表示されていること
           * 商品の手数料が300と表示されていること

    Scenario: 商品詳細の入力...

Gherkinではまず上記の様にFeatureとしてユースケースのエピックに該当する大項目を記載します。
その後、各Scenarioに共通する処理があればBackgroundに表現します。
(BackgroundはJestで言うところのBeforeEachのような役割です。)

そこから各Scenarioを列挙していきます。
振る舞いのテストケースでは1アクションnレスポンスの形式になっています。
なので各ScenarioにおけるWhenは1つになるのが一般的です。
1つのアクションに対して複数の動作が発生することはありえるのでThenは複数になることもあります。

テストの実装

Gherkin記法で定義したテストケースに紐付くテストを実装していきます。
テスティングライブラリとしてCypressを活用しているのでcypress-cucumber-preprocessor(以下、CCP)というライブラリを用いています。

テストの実装自体はCypressを用いた実装になりますが、CCPの関数を間に挟むことでテストケースと紐付くテストが存在していることを機械的に担保できるようになります。

例として、先ほどのユースケースに紐付くテストを書くと以下になります。

import { Given ,When, Then } from '@badeball/cypress-cucumber-preprocessor';

Given("ログインしていること",()=>{
  cy.login();
});

// 出品の開始
Given("トップページにアクセスしていること",()=>{
  cy.visit("http://toppage")
});
When("「出品」ボタンをクリック",()=>{
  cy.get('[data-cy="ExhabitButton"]').click();
});
Then("商品の出品画面に遷移していること",()=>{
  cy.url().should("equal", "http://exhabit");
});

CCPは、各Step(Given、When、Then)の関数を提供してくれています。
テストファイルの中ではGherkin記法で書いた各Stepの文面を第一引数に取ってCCPのStep関数を呼び出します。

各Step関数の中では、Cypressのテスト関数がそのまま使えるので対応するユーザーのアクションを定義していきます。
イメージとしてはCypressのE2EテストをCCPのStepでラップしてカテゴライズしているような感覚です。

テストファイル内での各Step関数の第一引数はGherkinファイルで定義した各Stepの内容と完全一致している必要があります。

CCPのアルゴリズムとしては、まずテストケースの中身を上から順番に読み込みます。
その中でStep(Given、When、Then)があればそのStepに紐付く文面をテストファイルの中から探しに行きます。
テストファイルの中でStep関数の第一引数が完全一致したStepの中身を実行します。
完全一致する文面が存在しなければエラーを返します。

以下は「商品の出品画面に遷移していること」というテストが存在しなかった時のerrorです。

そもそもテストが存在しないときには、紐付くテストを定義するようにサジェストされます。
Gherkin記法で記載したテストケースとそれに対応するテストが実装されていない場合は上記の様にテストがfailするので担保したい振る舞いがテストできていることを機械的に検知できます。

サジェストにあるように”pending”という文字列をStep関数で返却するとテストをスキップすることができます。

Then(("商品の出品画面に遷移していること")=>{
  return "pending";
});

テスト結果ではPendingの件数が表示されます。
なのでプロダクトコード実装前に動作を担保したい振る舞いはテストの中身をPendingで全て仮定義します。
プロダクトコードの実装が進むと同時にテストの中身を実装しPendingのテストを潰していきます。
実装結果にPendingの件数が表示されるので担保したい振る舞いのテストが未実装のまま放置されてしまうことも無いはずです。

振る舞い駆動開発の恩恵

最も実感しやすかった恩恵は実装スピードを落とさずに一定の品質を担保できているところです。

元々は、開発者とPdMの目線があっていないことが原因で手戻りが発生していました。

しかし以下がシステマチックに管理されることでその手戻りは激減しました。

  • システムはどう振る舞うべきか?
  • その振る舞いはユーザーにどんな価値を提供するのか?

またテストの実装漏れも少なくなり、実装段階から品質を担保することができるようになりました。

そして開発者が機能の背景を理解していることで「ユーザーがこのニーズを満たしたいのであればxxの機能があったほうが良いのでは?」等の新たな提案も生まれやすくなりました。

振る舞い駆動開発の副次的な恩恵として開発者がやらされ仕事で実装するのではなく、視座を高くもって開発に取り組むことが実現できたことも良い点でした。

終わりに

いかがでしたでしょうか。 私の所属する事業部は発足してまだまだ成長段階でプロダクトの状況も0-1フェーズにあります。なのでこういったどういう構成で開発を進めるべきかなどを検討・決定していける機会が多いです。この記事をご覧になり、この事業部を一緒に盛り上げてみたいなと思う方が1人でも増えれば嬉しく思います!(詳しくはこちら

レバレジーズで歩む、ユーザーファーストなエンジニアとしてのキャリア

はじめに

こんにちは、レバレジーズ株式会社の髙橋です。2020年に新卒エンジニアとして入社して以降、エンジニア向けQ&Aサイト「teratail」のメンバーとして働いています。最初の2年弱を企画メンバーとして過ごした後に、開発メンバーとしてのキャリアを今日まで進めてきました。

本記事では、新卒から2年弱を開発せずに過ごした結果、

  • どのような経験をし、
  • どのような能力を身につけ、
  • どのようなエンジニアへと成長して行ったのか

を、時系列に沿って以下の7章綴りでお送りいたします。

本記事(私のキャリアパス)を通して、なかなか知る機会のないレバレジーズでのエンジニアキャリアをお伝えし、少しでもレバレジーズの魅力をお伝えできればと思います。

また、本記事では私がキャリアを歩む上で抱えた苦悩も紹介しています。 エンジニアでありながら、開発をせずに過ごす上で、本記事で紹介するような苦悩はつきものと思っております。全てが良いことだけで歩めるキャリアではなかったため、同じキャリアを歩まれている方に向けて、同じ悩みを抱える私を知ってもらうことでエールを届けられたらと思っております。

※ お忙しい方は、後半2章をご覧ください。現在の姿はここに詰まっています。

対象読者

対象読者は以下になります。

  • レバレジーズでのエンジニアキャリアに興味がある方
  • エンジニアとしての仕事に興味がある方
  • エンジニア経験が浅く、今後のエンジニアとしてのキャリアパスが定まっていない方、あるいは、悩んでいる方

入社前から変わらない、私のキャリア目標

私のキャリアの目標は「あったらいいな」を実現できるエンジニアとなり実現していくことです。「あったらいいな」とは、自分だけでなく周囲のさまざまな人が抱えている悩みを解消するものであったり、あるいは、夢や空想上の欲しいと思うものやサービスのことです。私がこのキャリアの目標を立てたときは、ソードアートオンラインという作品のナーヴギアというゲーム機に憧れがあり、こうした憧れを実現できるような人になりたいと思っていました。そのためには、自分を含め、人が持つ「あったらいいな」を分析し、システムとして表現できる能力をつける必要がありました。

こうして、エンジニアとしての就活をはじめ、レバレジーズのエンジニアとなり、キャリアをスタートさせていきます。

エンジニアの課題解決を促進していく。これからのteratailを考える

入社後の研修を経て2020年7月にエンジニアとしてteratailチームに配属されました。最初は、teratailのサービスとシステムの理解を深めるために、過去のドキュメントの確認や簡単なバグの修正を通してオンボーディングを進めました。

当時のteratailは開発と企画の2チームがあり、開発はサービスの運用や機能追加・修正などを担当し、企画はteratailのユーザーを分析し、どのようにサービスを拡大するかを企画することをメインで行っていました。

エンジニアとして配属された私は当初、エンジニアとして開発スキルを伸ばすために開発をすべきであると考えていましたが、ユーザーを分析しサービスを改善・拡大するという企画チームの業務内容を知ったことをきっかけに、自分の目指すエンジニアになるためには企画での経験値を積んだ方が良いのでは? と思い始め、teratailでの企画に興味を持つようになりました。

さて、なぜエンジニアである私が、開発ではなく企画に興味を持ったのでしょうか。

理由は大きく二つあります。

一つは、自身のキャリア目標を踏まえた時に、ユーザーの目線に立って企画を進める経験は重要で、必要な能力であると考えたからです。「あったらいいな」を考えるためには、

  • 誰をターゲットとして
  • どのようなことに悩んでいて
  • 何を必要としていて
  • なぜそれを必要としているのか

といった視点で考察できる必要があると考えており、このような視点で考察する能力を養ううえで、企画することは学びを得られる場であると考えました。

もう一つは、teratailでの企画の仕事はやりたくてもなかなかできない仕事だと考えたからです。teratailでの企画は、機能の改善のための企画だけでなく、サービスの今後を考え、どのように拡大していくのかを決めることまで担っていました。こうしたタスク(特に後者)に入社まもないうちから着手し経験を積めることも、自身のキャリアにとってプラスになると考えました。

そして、企画のタスクに手を挙げ、teratailを利用するユーザーの課題解決に向けた企画を進めることになります。

抱える苦悩、同期の成長とベクトルが違う自分

企画の仕事に携わるようになりしばらくした頃でした。

新卒のエンジニアとして入社した同期が皆、それぞれの配属先でエンジニアとしての成果を出すようになりました。同期との交流が盛んだったこともあり、互いが今どのような開発をしているのかを話す機会も多く、同期の活躍が自然と耳に入ってきます。

最初はこうした話を嬉しく思っていたのですが、次第にある不安を抱えるようになります。

それは、同期と比較した時の自身の「開発力の低さ」と「成果を実感しにくい」ことです。

配属されて間もない頃から企画に取り組んできていたため、開発はほとんどできていませんでした。同期が業務を通して開発力を伸ばす一方で、私の開発力は停滞したままでした。エンジニアであるのに開発力が伸びていない状況は、エンジニアとしての自信をなくす要因となりました。

そして、企画を通して実感できる成果のなさが、さらに自信を無くさせることになりました。開発では、作ったものをリリースすることですぐにエンドユーザーに届き、利用してもらえるため、目に見えた成果を実感しやすいです。一方で企画の仕事は、ただ企画をするだけでは目に見える成果とし現れず、そこから要件定義や開発を通して初めて形となります。また、企画としての完成度が高くなければ開発に動き出すことができず、何度も企画をやり直し、終わりの見えない仕事に成果を実感することはありませんでした。

こうして、同期との開発力の差の広がりや実感できる成果の少なさから、自分の仕事に自信をなくしていきました。

大規模なプロジェクトが動き出す

企画をしている頃、開発チームでは、CakePHPをTypeScriptを使用したマイクロサービスへとリプレイスするプロジェクトが始まります。

※ 本記事では技術的な内容は記載しません。もし興味がありましたら、これに関する記事を公開しておりますので、以下からご覧ください。

tech.leverages.jp tech.leverages.jp

1年以上をかけてリプレイスを行うことになるのですが、私はリプレイスプロジェクトへはほとんど参画しません。これまで通り企画に従事しつつ、開発メンバーが行っていた保守運用の仕事を引き受けることになります。ここで、ようやく開発に少しずつ着手することになりました。

そして、リプレイスプロジェクトも終盤に差し掛かった頃に、サイトの動作テストを担当することになります。

比較的規模の大きいサイトでもあったため、一時的なテストチームを組むことになるのですが、エンジニアは私のみでした。限られた時間でテストを完了するには、非エンジニアの方でもスムーズにテストを進められるテスト項目書を作成する必要がありました。

このテスト項目書の作成の際に、企画で培ったユーザーやサービスを見る力が活きることとなります。それは、

  • サービスを利用するカスタマー
  • テスト項目書を利用するテストチームのメンバー

の2者のユーザーを考えてテスト項目書を作成することです。

サービスを利用するカスタマーが、普段、teratailをどのように利用しているのかを考えることで、サービスとしてあるべき姿をテスト項目として抽出し、 テスト項目書を利用するデバッグチームのメンバーが、どのような属性を持っているのか(どれくらいITリテラシーがあるかなど)を考えることで、テスト手順を書く際に理解しやすく表現することを自然とできるようになっていました。

このような形で企画の時に培った能力を発揮できていたことで、過去に感じられなかった成長を感じることができました。

こうしてデバッグを終え、リプレイスのプロジェクトが一段落しました。  

そしてエンジニアへ

大規模なリプレイスプロジェクトが一段落したころに、本格的に開発をすることになります。バックエンド開発に始まり、次第にフロントエンドに寄っていき、現在に至るまで、フロントエンド開発に携わっています。

開発をする上でも企画の経験が活きる瞬間があり、また、過去に感じていた不安が払拭し始めます

開発メンバーとして開発を始めたての頃は、設計や言語仕様への理解が浅く一つ一つのタスクをこなすだけで精一杯なことが多く、とてもエンジニアとはいえないレベルでした。それでも、実装したものをすぐにユーザーに利用してもらえ、Twitterやサービスのお問い合わせを通して多くのフィードバックを受けられるようになったため、成果を実感しやすくなりました。開発を通したこの体験が、過去に抱えていた不安を次第に解消してくれました。

そして、開発に慣れてからは、協働するメンバーが読みやすいコードとなるように意識したり、作業時間の短縮による時間的な余裕が生まれ、機能追加と併せてリファクタリングできる箇所を併せて修正したり、簡単に直せるバグや機能追加であれば対応することができるようになっています。

ここまでの開発経験を通して、ある程度自走したエンジニアとしてチームに貢献ができるようになりエンジニアとして自信が持てるようになっていきました。

自分で企画し、要件を定義し、開発できるエンジニア

バックエンド・フロントエンド両方の開発に慣れ、開発に自信がつき始めたころ、小規模な機能追加プロジェクトを立ち上げることになりました。このプロジェクトでは、企画、要件定義、プロジェクトマネジメント、開発全てに携わる形で、これまでに得た能力を掛け合わせて遂行しました。

企画や開発ではこれまでに体験し得た能力をそのまま活かし、要件定義やプロジェクトマネジメントでは、企画や開発を通して得たユーザーファーストな思考から、

  • その機能を通してサービスのユーザーにどのような体験を生み出し、実現するか
  • プロジェクトを遂行する上で、いかにしてメンバーの働きやすさを実現するか

を自然と意識して取り組むことができるようになりました。

また、「企画、要件定義、プロジェクトマネジメント、開発全てに携わりながらプロジェクトを遂行する」ことは、私のキャリア目標である「「あったらいいな」を実現できるエンジニアとなり実現する」ことでもありました。

3年目にして、必要な基礎能力を一通り揃えることができ、こうして一つの機能として実現できたことで、「あったらいいな」を実現できるエンジニアとしてのキャリアを始められた気がします。

レバレジーズで歩む、ユーザーファーストなエンジニアとしてのキャリア(まとめ)

これまでのまとめです。ここまでお読みいただいた方、本当にありがとうございました。拙く長い文章で読みにくかったことと思いますが、あと少しだけお付き合いください。

今日に至るまで、多くのことに挑戦させていただきました。サービスの企画を通してユーザー志向を身につけ、設計や開発の際に応用することで、サービスのユーザーや協働するメンバーに価値を提供することができるようになりました。(道中で苦悩もありましたが、、、

とはいえ、まだまだ至らぬ部分も多く成長できる余地が多いため、今後も「あったらいいな」を実現するエンジニアとして、より多くの価値を提供していけるように努めて参ります。

新卒からの3年間をこのように歩み、「あったらいいな」を実現するエンジニアをスタートさせることができたのは、「エンジニア = システム開発をする人」という枠にとらわれずに、さまざまな職域に挑戦できるレバレジーズの文化が大きく関わっています。レバレジーズには、こうした挑戦をするメンバーを支えてくれる上司やメンバーが多くいます。私の他にも開発以外の職域を経験しているエンジニアは多く、それぞれが違ったスキルを持ち、関わり合うことでより大きな価値を生み出しています。

本記事を読んで、レバレジーズのエンジニアとしてのキャリアに興味を持ってくださった方は、ぜひ、採用フォームよりご応募ください。

最後までお読みいただき、ありがとうございました。

https://recruit.jobcan.jp/leveragesrecruit.jobcan.jp

【Architect New World on AWS 2022】ITエンジニア特化型Q&Aサイトteratailを 言語、DB、クラウドなど フルリプレイスした話

レバレジーズ株式会社 テクノロジー戦略室室長 兼 テラテイルチームリーダーの竹下です。

Architect New World on AWS 2022のイベントで発表した際のスライドです。 teratailのリプレイスのインフラ面に注目した発表になっています。

現在レバレジーズでは一緒に働いてくれる仲間を募集しています。ご興味のある方は、以下のリンクからご応募ください。
https://recruit.jobcan.jp/leverages/

テックフェス2022秋レポート React Suspense ~ v18から見えること ~

はじめに

こんにちは。レバレジーズ株式会社の松本です。普段はHRテック事業部で主にフロントエンドの開発を担当しております。 今回の記事では、入社2ヶ月の私がテックフェス全体のレポートと、弊社フロントエンジニアの小林がReact18について発表した「Parce que j’aime la React.」を受けて、追加で調べたり試したりしたことをご紹介したいと思います!

テックフェスとは?

レバレジーズグループに所属するエンジニアを対象に社内で半年に一度行われている技術の祭典です。エンジニアが新しい技術に興味を持ち、勉強をするきっかけを作ることを目的とし、グループ全体の技術力向上を目指します。

11月22日行われたテックフェス2022秋では、「まずは知ろう。次に学ぼう。そして明日使おう」というテーマで開催されました。

テックフェス全体について

当日の流れは、有名な「テスト駆動開発」の日本語訳をされた和田卓人氏をお招きし、「質とスピード - ソフトウェア開発の典型的な誤解を解く(2022年秋版)」の基調講演から始まり、Sessionが2時間弱、LTが30分程、最後にパネルディスカッションが1時間程というタイムスケジュールでした。

オープニング

基調講演(和田卓人氏)

Sessions(30分 × 4名の弊社エンジニア)

LT(5分 × 5名の弊社エンジニア)

パネルディスカッション(1時間 4名の弊社ベテランエンジニア)

パネルディスカッションでは、「ITエンジニア35歳定年説について語る」というテーマで、意見を交わしていました。

・面白かった発表
特に面白かった発表は、個人的には和田卓人氏の発表と最後のパネルディスカッションでした。 和田卓人氏の「質とスピード」についての発表では、スピードのために犠牲にしようとしている品質とは何を指すのか、品質とスピードはトレードオフではないことを経験談やデータに基づいてお話ししていただきました。 スピードおよび質とトレードオフなのは教育、成長、多様性への投資であるというお話もあったので、内部品質(スピードは落とさない)への投資はエンジニアだけでなく事業会社全体にとって重要であると感じました。

また、最後のパネルディスカッションも個人的には非常に面白かったです。 一般的に、「ITエンジニア35歳定年説」というのはネガティブな意味で広まっていると思います。 調べてみると一般的に35歳定年説と言われているのは、学習能力の低下や体力の低下が理由とされているようです。

ただ、今回はそれとは反対にポジティブな印象を受けました。 登壇された方々もキャリアを積むにつれてコードを書くことが減ってきているとのことでしたが、前述したネガティブな理由ではなく、できることが増えたことで業務の幅が広がったことが大きな理由の一つとなっているようでした。 エンジニアに関わらず、年齢に関するトピックではネガティブに語られることが多いイメージだったので、今回はポジティブな話を聞くことができて非常に良かったなと感じました。

・関心があった発表
SessionsやLTの発表も非常に面白かったです。また、個人的には弊社フロントエンジニアの小林によるReactについての発表(30分のSesssion)「Parce que j’aime la React.」は、普段触れている技術領域ということもあり非常に関心がありました。 今回は、この発表から何を知って、何を学んで、何を使ったのかについても共有できればなと思います!

何を知って、何を学んで、何を使ったのか?

・知ったこと
小林の発表ではReact18で提供されるものとして、並行処理レンダリング、Suspense(正しくはアップデート)の紹介がありました。 例えば、以下のような実装だと、FetchComponent内でPromiseがthrowされている場合(サスペンドした場合)は、fallbackで指定されているComponentが返されます。このPromiseが解決された時はFetchComponentが返されます。

function App() {
 return (
     <Suspense fallback={<Spinner />}>
       <FetchComponent />
     </Suspense>
 );
}

また、Reactの日本語公式ドキュメントには、以下のような記述があります。

サスペンスにより、「UI ロード中状態」というものが、React のプログラミングモデルで宣言的に記述可能な主要コンセプトに昇格します。

今まではuseQuery等を利用してloadingの状態を実装者が管理し、それを用いてUIを分岐させるような実装をしていた方も多いと思います。私も同じような実装をしておりました。

この分岐のロジックをReact側が担ってくれるようになるため、我々はloadingの状態やそれを利用した分岐に関心を持つ必要がなくなり、ロード中とロード完了後のUIを宣言するだけで良くなったということだと解釈しています。


・学んだこと
React18で提供される機能の一つとしてTransitionというものがあります。今回はこの機能について学んでみました。

Transitionは更新処理に優先順位をつけたり、中断するための機能です。この機能では、更新処理を「緊急性の高い更新」と「トランジションによる更新(緊急性の低い更新)」で区別しています。緊急性の高い更新は即座にUIを更新します。一方、トランジションによる更新では緊急性の高い更新が発生したら更新処理を中断し、最終的な結果のみを返します。これはSuspenseと合わせて利用することもできるようです。

また、React18について学んでみて面白かったのは、この更新処理に優先度をつけたり更新処理を中断するような機能は ”React Fiber” というアーキテクチャによって実現可能になったということです。

Fiberについて調べていた中で、こちらの記事がとても面白かったです。 ”React Fiber”はReact16から採用されたもので、React16以前とは仮想DOMを更新してUIに反映するアルゴリズムが変更されているとのことです。今までは仮想DOMを上から再帰的に更新していたものが、各ReactElement一つ一つに対して更新処理が用意されるため、非同期的に仮想DOMを更新することが可能になったとのことみたいです。

また、少し古いものですが、こちらにも以下のような記載があります。

The goal of React Fiber is to increase its suitability for areas like animation, layout, and gestures.

“suitability”という単語が使われているように、FiberはUIの動きを絶対的に早くするものではなく適性を高めること、つまりUXの向上を目的にしていると考えられそうです。 この目的を達成するための基盤にFiberが必要で、今回のReact18で導入された機能はまさにこの目的を達成するための具体的な方法の一つと言えそうだなと感じました。


・使ったこと
今回学んだ内容は業務ですぐに使える内容ではないこともあり、ここではサンプルを触ってみた感想のみにしようと思います。公式のReact Docsに載っているuseTransitionのサンプルコードを手元で動かしてみました。 所感としてはSuspenseは比較的理解しやすく感じた一方で、useTransitionは思っていたよりも腰を据えて学んでいく必要があるものだなと感じました。

SuspenseもuseTransitionも背景には並行処理レンダリングがあって、私たちが見えている画面の状態とは別でもう一つ状態をもっていることが少し理解できました。 今後遠くない未来で実務での利用もありそうなので、引き続きインプットを続けていこうと思います。

最後に

今回の記事では、テックフェスで知った内容からReact18について自分なりに学んだことを書かせていただきました。ReactはUX改善のためのアップデートを続けているというのが非常に面白かったです。また、テックフェスは非常に魅力的な発表ばかりで、話を聞いていてとても楽しかったです。 私は今年の9月に中途で入社したばかりですが、このように組織内での技術の共有があるのは、ナレッジ共有の観点からも非常に有意義だと感じています。 皆さんもレバレジーズで一緒に働いてみませんか? レバレジーズに少しでも興味を持っていただけた方は、こちらからエントリーをお願いします!

recruit.jobcan.jp

データサイエンティストの皆さん不安よな。MLOps動きます。

はじめに

こんにちは、テクノロジー戦略室の新城です。私は2022年6月に中途入社し、入社してまだ半年ですがMLOpsの推進という仕事を担当しています。
現在レバレジーズではレバテックダイレクトの求人マッチ度判定のような、レコメンドシステムを多くのサービスで活用しています。
一方で機械学習モデルを本番運用したはいいが、データサイエンティストがモデルの改善フローを回しにくく、再トレーニングしたモデルのデプロイが頻繁にできないといった問題が顕在化してきました。
そこで、今回は社内営業支援ツールにおいてMLOpsを導入し実際にデータサイエンティストがモデルの改善プロセスを回せるようになったその事例を紹介できればと思います。

背景

レバレジーズのMLシステムはMLOps レベル0と言われる状態に近く、データサイエンティストがモデルを作成し、プロダクトへモデルを一度組み込むとその後の改善は行いにくく、プロダクトでのモデル品質も充分に確認できない状態でした。
あるプロダクトでは、図中のModel Servingの部分でプロダクションエンジニアがプロダクトの要求レベルに沿うように前処理の高速化等を行っており、トレーニング時と推論時に前処理が異なっている状態(Training-Serving Skewが起こっている状態)でモデルの更新に双方のエンジニアに工数がかかってしまう状況でした。
このような状況では、データサイエンティストは他のエンジニアの手を借りないとデプロイが出来ないという制約から、軽微なモデルの改善を手軽に行うことができません。加えて、レコメンドにおけるMLモデルはその品質を保つために、定期的に再学習を行い、トレンドを学習する必要があります。しかし、再学習時のデプロイ毎に双方のエンジニアに工数がかかる状態は好ましくありません。
そこで今回はデータサイエンティストが主導してモデルの改善・デプロイを行うことができる環境を整えることで、プロダクトエンジニアの工数を削減し、モデルの改善に積極的に取り組めるような仕組みづくりを目指しました。

出典: https://cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning?hl=ja#mlops_level_0_manual_process

MLプラットフォームの導入

上記の状態を改善するため、今回AWS SageMakerやGCP Vertex AIに代表されるようなMLプラットフォームの導入を検討しSageMakerを採用することになりました。データの分析から学習、モデルデプロイを一手に担ってくれるプラットフォームを導入することでデータサイエンティスト主導の機械学習モデル開発・デプロイができるようになることが狙いです。
またMLパイプラインを用途ごとに設けることでMLOps レベル1の状態に少しでも近づけることを目標としています。

構成図

今回はAWS SageMakerとStep Functions DataScience SDKを用いてMLパイプラインを構築しました。グレーアウトしている部分は今後の実装を予定している部分です。
図中の棒人間がいる箇所をデータサイエンティストに実装してもらい、後はパイプラインを実行していけばプロダクトに使用する図中左下のエンドポイントまでデプロイが完了します。
全体構成だけではわかりにくいと思うので各パイプラインの役割をそれぞれ見ていければと思います。

1. データ取り込みパイプライン

まずはデータ取り込み処理部分について順を追って説明をしていきます。

1.1. Event Bridgeの起動
学習・推論に使用するデータをBigQueryから定期的に読み込むためにEvent BrigeでStep Functionsの起動をしています。

1.2. データ取得と前処理
レバレジーズではデータウェアハウスとしてBigQueryを採用しており、データサイエンティストはここから任意のデータを取得・前処理・データフレーム化して使用します。
少しややこしいのですが、ここでの前処理と後述する学習パイプラインの前処理は異なっており、統計的な特徴量(平均値や分散値など)等の推論毎に作成する必要のない特徴量を予め作成し、推論時にこれらのDataFrameを予め読み込んでおくことで推論処理の高速化を図っています。

1.3. データの品質チェック(将来実装)
取得するデータの特徴量ドリフト検知といった品質チェックや定期実行毎の特徴量の推移等を出力できるとデータサイエンティストに気づきを与えることができるのではないかと検討中です。

2. 学習パイプライン

S3に用意されたデータを元に学習から評価までを行っています。Lambda Stepではモデルとその評価値をSageMaker Model Registryへ登録しています。
ここでの処理は一般的に紹介されている学習パイプラインとほぼ同等と思って頂けたらと思います。

3. デプロイパイプライン

ModelRegistryへ登録されたモデルをデプロイするパイプラインです。

3.1. Model情報取得(Lambda)
Step Functionsへの入力となるモデルパッケージ情報とバージョン情報を元にModel Registryから必要な情報を取り出しています

3.2. Endpoint新規作成・更新判定
このLambdaは単にEndpointの新規作成か更新かを判定するものです。入力されたEndpointNameを元に判定しています。

3.3. 推論Endpointの立ち上げ
Endpoint立ち上げ時に前述のデータ取り込みパイプラインで作成したDataFrameを読み込んでおり、推論時に都度作成する必要のない特徴量はこちらを活用しています。
またデータキャプチャ設定で保存された推論結果をS3からBigQueryへ日次で転送しておりデータサイエンティストが推論結果をBigQuery上で解析できるようにしています。

3.4. エンドポイントに対しての自動テスト(未実装)
データサイエンティストがプロダクトに関わるエンドポイントの実装とデプロイを行うということで、心理的安全性の担保ためにここの実装は必要だと考えています。

改善できたこと

データサイエンティスト主導のモデル改善フローの実行

データサイエンティストが用意したパイプライン実行用のPython Notebookを使用して、モデルの改善を任意のタイミングでより容易に行うことができました。実際にSageMakerを導入してから短期間で複数回モデル変更のデプロイをデータサイエンティスト主導で行っておりその効果はあったのかなと思います。
一方で実行が手動なのは変わりなく、どのファイルを変更したらどのパイプラインを実行すれば良いといったフローはかなり煩雑なので、現在は変更に対応するパイプラインが自動実行されるといった自動化に着手中です。

Training-Serving Skewの改善

こちらのルールの通り、コードを再利用する実装を事前にデータサイエンティストと進めたことで、基本的にはデータサイエンティストの変更のみでモデルのデプロイを行う仕組みを整えることができました。一方でエンドポイントのレスポンス速度は常に保証する必要があるため、前述した自動テストでのチェックや後述の前処理高速化に取り組んでいきたいと考えています。

試したが今回は盛り込めなかったもの

前処理の高速化(ApachBeam等の導入)

今回はリアルタイム推論だったのですが、比較的前処理の量も少なく、特に工夫せずともプロダクト要件を満たす処理時間に収めることができたため学習コストの観点から導入はしませんでした。
但し、今後時間のかかる前処理やストリーミングで捌かないといけないデータが必要となった際にはApachBeamといったバッチとストリーム処理が可能なデータ処理パイプラインを導入しなければいけないと考えています。導入の際には推論時のみだけでなく、学習側もきちんと共通化できるように考えて実装していきたいです。

Feature Storeの導入

S3へ特徴量データを保存している部分をFeature Storeに置き換えたかったのですが、定期実行時にFeature Storeへ書き込むと今回のデータ量では書き込み量が多く、想定以上の料金がかかってしまうことがわかりました。データの差分のみを登録する仕組みを作る案も出ましたが、定期実行ごとにDataFrameを作りバージョン管理できた方が便利なため今回は採用を見送っています。上記の前処理高速化と関連してストリーミングデータを扱う際にはまた必要だと考えています。

今後の施策

MLOps レベル1への移行やより良いモデル開発体験を目指して以下のことを行っていきたいです。

  • パイプラインや運用プロセスの自動化
    • データサイエンティストがGitHub上で改修ブランチを切ったら前処理・学習・評価(ABテスト)まで自動で行われる仕組みを目指したい
  • モデル評価機構の確立
    • 特徴量や特徴量寄与度に違いがないか監視(ドリフト検知)
    • モデルレジストリへ登録しているモデルへのリアルタイム評価の反映
      • もしこのモデルがデプロイされたらこのくらいのパフォーマンスを出すだろうといった予測を与えられたら
  • 前処理高速化やFeature Storeの導入
    • 今回は必要性がなかったため断念したが、今後に向けて実装を検討していきたい

最後に

今回の記事ではSageMakerを導入しデータサイエンティストがモデルの改善プロセスを回せるようになったその事例を紹介しました。MLOpsの導入と言い切るにはまだまだ改善すべきことが多いですが、まずは初めの一歩を踏み出すことができたのかなと思います。

また、これらの実装には入社してまだ半年の私を支えてくれた、データ戦略室のデータサイエンティストの理解と協力無くしては実現できませんでした。 このように、レバレジーズには年次や経験を問わず、主体的に取り組める環境が整っています。ご興味のある方は、以下のリンクから是非ご応募ください!

recruit.jobcan.jp

SREから始める、関係者全員の幸福

はじめに

 レバレジーズ株式会社 テクノロジー戦略室室長の竹下です。レバレジーズでは現在SRE(Site Reliability Engineering)チームを立ち上げ、全社を巻き込んでSRE化を推進しています。
 SREを提唱したGoogleを筆頭に、日本でもメルカリやサイバーエージェントなど数多くのテック企業がSREチームを立ち上げ、インフラ基盤を維持管理するだけではなく、サイトの信頼性を担保することでサービス価値を最大化しようとしています。レバレジーズでも、1年ほど前からSREチームを立ち上げ、インフラからもお客様に最高のサービスを届ける事を目指しています。
 この記事ではSREチーム立ち上げの背景と、レバレジーズがどのように顧客に寄り添ったSREチームを目指しているか、そしてSREで使用している技術のご紹介をしていきたいと思います。

背景

 レバレジーズ株式会社では、チームをサービス単位で分けており、それぞれが大きな裁量を持ってサービス運営をしています。 これまでは、チーム毎にインフラ担当者を置きサービスに合わせた構築、運用を行ってきていました。そのため、チームによって使用技術やインフラ構成が異なっており、チームの統廃合や異動、退職などによりインフラ担当が変わる際には、高い学習コストがかかっていました。また、担当者に寄ってインフラのレベルもまちまちなので、CI/CDやIaCを組み込んで自動化出来ているチームもあれば、手作業でインフラ変更をしているためインフラ作業で手一杯になっているチームもあったりしました。このままでは、今後事業分野も拡大してより多くのチームが出来た時に、チーム間の流動性と、全体の効率が低下してしまい組織規模の拡大のブレーキとなることが予期され、また、レバレジーズではマイクロサービス化を進めておりその観点からもインフラ周りの効率化が必要となってきていました。
 そこで、インフラ関連の知識を集約し標準化を図るとともに全体最適を行うため、プロジェクト横断チームとして、SREチームを立ち上げることになりました。

SREチームのミッション

 SREチームのミッションとして
SRE化を通して、Developer Experienceの改善、事業の拡大への対応、お客様に信頼されるサービスの提供を実現する
を掲げています。
 ただのインフラ屋としてタスクをこなすだけでは、レバレジーズのミッションである「関係者全員の幸福を追求する」ことの達成は難しく、また、インフラは顧客から少し離れた所で開発を行うため、誰のためのサービスなのかの意識が薄くなりがちです。そのため弊社のSREチームでは、インフラの上で動くサービスの更にその先に顧客がいることを忘れず、常にチームとしても個人としても成長を続けられることを目指し上記のミッションを設定しています。

目指すSRE組織

 上記のミッションを達成するにあたってどのようなSREチームを構築すればよいか調べましたが、いくつかの型が紹介されていたり、他社事例を調べても各社それぞれSREチームの形は違っていました。そのため、レバレジーズの文化にもっとも適したSREの形を模索し、Embedded SREとEvangelist SREのハイブリッドを目指すことにしました。

Embedded SREとは

 レバレジーズのエンジニアの使命は、世の中に価値あるサービスを届けるところにあると考えています。最大限の価値を届けるためには、Embedded SREは、ドメイン知識を持ち事業を理解した人材でないといけません。そのため、Embedded SREは開発チーム内のアプリエンジニアから選出してもらい、サービスのインフラ運営、監視、トイル撲滅、SLOの策定など、SREの実務を行いDevOpsの実現をしてもらいたいと考えています。

Evangelist SREとは

 しかし、Embedded SREだけではチーム内に知識が閉じてこれまでと同様に局所最適に陥ってしまう可能性もあります。また、アプリエンジニアに任せることになるため技術レベルもチーム毎に違って来てしまいます。そこを補うためにEvengelist SREを置き、SREプラットフォームの構築、SREのトレーニング、ノウハウやベストプラクティスを各チームから吸い上げ、全社に浸透させるなどの動きをしていきます。今回立ち上げたSREチームは、このEvangelist SREの集団になります。

使用技術

 SREチームで標準となる技術を策定し全社に広めることで、ノウハウやベストプラクティスの共有を効率的に行おうとしています。現在使っている or これから使おうと考えている主要な技術を紹介したいと思います。

Infrastructure as a Code (IaC)

  • CDK
  • Terraform
  • Ansible

 AWSでの新規のインフラ構築には主にCDKを利用しています。既存のリソースのインポートだったり、DataDogやGCPの場合はTerraformを利用しています。新しく立ち上げるサービスやリプレース時には、CDK,Terraformだけで構築できるようにしていますが、既存のEC2などでセットアップが必要な場合にはAnsibleを利用しています。

監視

  • Datadog
  • Datadog RUM(Real User Monitoring)
  • Datadog APM(Application Parformance Monitoring)

 メトリックスやログは現在Datadogへ集約を進めています。また、DatadogもダッシュボードをIaC化し、全てのサービスで標準的な監視をすぐに始められるようにしています。またDatadog RUMやAPMを導入により、より詳細なメトリックスの取得し、効率的なパフォーマンスチューニングを目指しています。

サーバレス

  • AWS Fargate
  • Cloud Run
  • Docker
  • kubernetes

 新しく立ち上げるサービスは全てサーバーレス化を進めています。SREチームの前身になるインフラチームの際には人数が少なくkubernetesクラスターの維持管理が難しと考えて、フルマネージドサービスである、AWS Fargateを採用しています。GCPではCloud Runを主に採用しています。ただ、SREチームの人数も増え管理を、より自由な構成を行うためにkubernetesの導入検討も続けてはいます。

マイクロサービス

  • gRPC
  • GraphQL
  • AWS App Mesh

 サーバレス化とともにマイクロサービス化も進めています。サーバー間の通信はgRPCを主に採用しており、一部のサービスではGraphQLも利用しています。また、サービス数も増えてきたため現在はAWSのフルマネージドのService MeshであるAWS App Meshの導入も進めています。

最後に

 いかがでしたでしょうか?SREチームもシステムのその先にいるユーザーを視野に入れ、より良いユーザー体験を届けるために日々奮闘しています。また、ただ闇雲に頑張るのではなく、適切な技術を選定しかつ組織に浸透させるところまでを考えながらSRE化を進めています。

 一緒にSREのチームひいてはSREの文化を創っていきたいという方や、今はまだインフラ知識が無いけどフルスタックエンジニアになりたいという意欲のある仲間を募集中です。ご興味のある方は、以下のリンクからご応募ください。

 一緒にSREのチームひいてはSREの文化を創っていきたいという方や、今はまだインフラ知識が無いけどフルスタックエンジニアになって行きたいという意欲のある仲間を募集中です。ご興味のある方は、以下のリンクからご応募ください。 https://recruit.jobcan.jp/leverages/

おまけ

 カバー画像にもしているのですが、AIに「Site Reliability Engineering」で描いてもらったらなんとなくそれっぽいのになりました

【Next.js】Server Side RenderingでABテスト(Google Optimize)を実装した話

はじめに

初めまして、レバレジーズ株式会社の小林といいます。
私は2022年4月に開発未経験でエンジニアとして中途入社し、teratailというサービスのフロントエンド開発とマーケティング周りの業務に携わっています。

teratailでは、昨年末にリプレイスを行ったこともあり分析基盤がきちんと整備されておらず、各種分析ツール(Google Analytics4やBigQuery、Google Optimizeなど)を導入する必要がありました。

中でも今回は、導入に特に苦労した「Next.jsのServer Side RenderingでABテストを実装した方法」について紹介したいと思います。

使用技術

今回の実装で使用する主な技術は以下の通りです。

  • Next.js
  • TypeScript
  • Recoil, React Context (状態管理)
  • Google Optimize (ABテスト)

背景

■なぜこの記事を書いたのか

  • 参考文献が少なかった
    通常のReactアプリ(SPA)のOptimize導入については参考文献が豊富ですが、Next.jsのServer Side Rendering時などにサーバー側からABテストを制御する方法に関しては文献が多くありませんでした。公式リファレンスを読んでも「???」な部分が多くあり、予想以上に詰まったため、同じような苦労をされている方は多いのではないかと思い記事にしました。

  • 今回の苦労を残しておきたかった
    このタスクを担当したときは入社3〜4ヶ月目の頃でしたが、開発経験ほぼゼロで入社した私にとっては難易度が高かったです。入社早々に今回のような貴重な経験ができたので、この苦労をここぞとばかりに共有したく、記事にしました。

■なぜ分析基盤を整備するのか

プロダクト自体は月間PV数が1,000万を超えており十分なデータが存在するにも関わらず、分析基盤が整っていないために、データドリブンに施策の立案や効果の検証が行えていないという課題がありました。 今後teratailが更にグロースしていく上で、データドリブンに意思決定を行える体制を作ることが必須だと考え、そのための分析基盤構築の一環としてABテスト環境を整えることとなりました。

■対象の読者

特に以下のような方々の参考となれば嬉しいです。

  • Next.js(SSR)でABテストを実装したいエンジニア
  • フロントエンドエンジニア
  • ABテスト担当者、マーケター等

前提知識

■Next.jsについて

プリレンダリング(ページにアクセスがあったときにサーバー側でJavaScriptを実行してHTMLを生成する)機能の一つで、SEO対策や高速なコンテンツ表示が可能となります。

  • 通常のReactアプリ(SPA)との違い
    通常のReactアプリ(Single Page Application)はブラウザ側でHTMLを生成するのに対し、SSRではサーバー側でHTMLを生成します。後述しますが、サーバー側で生成されるという点が、本記事の大きなポイントとなります。

■Google Optimizeについて

  • Google Optimizeとは
    Googleが提供する無償のABテストツールです。専用のタグをサイトに埋め込むだけで簡単にエクスペリエンス(=ABテスト)を実施することできます。エクスペリエンスやデータの集計は_gaexpというcookieで管理されます。

  • 管理画面でエクスペリエンスの実施が可能
    Google Optimizeの特徴として、ブラウザの管理画面で各種設定を行うことで簡単にABテストを実施することができる、という点が挙げられます。ノーコードでテストパターンの要素変更ができるため、非エンジニアでも気軽に操作することが可能です。

今回の問題点

通常、Google OptimizeのABテストはクライアント側で処理される仕様のため、サーバー側で機能を切り替えることができません。クライアント側でページの読み込み後に処理されるため、ABテストの実行(要素の変更)時にはどうしてもちらつき(ページフリック)が発生してしまいます。

今回、このちらつきを避けることや、管理画面側では設定できないようなABテストをプログラム側で実装することを考慮し、通常の方法ではなくサーバー側でOptimizeを使用する環境を整える必要がありました。

それではここから、本題の「Server Side RenderingとGoogle OptimizeでABテストを実装する方法」について説明していきます。

Server Side Rendering×OptimizeでABテストを実装する方法

■結論

Server Side RenderingでGoogleOptimizeのABテストを実装するためには、Optimizeのサーバーサイドテストの機能を使い、プログラム側とOptimize管理画面側でそれぞれ以下の項目を実装・設定する必要があります。

【プログラム側での実装】
1) テストパターンの割り当て
2) Optimize(GA)側にテストパターンを送信
3) テストパターンのグローバル管理、取得
4) コンテンツの出しわけ

【Optimize管理画面側での設定】
1)テストパターンの作成
2)ページターゲティング
3)その他

それぞれ説明していきます。

■プログラム側での実装

1) テストパターンの割り当て
まず、ABテストのseed値がCookieにあればCookieから取得、なければ生成する関数を定義します。
(※seed値にはuserIdを用いてもOKですが、今後「ログアウト⇨ログイン状態」をまたいだABテストを行うことも考慮し、userIdを利用せずにABテスト用にseed値を生成してパターンの振り分けを行うように実装しています。)

【abTestSeed.ts】

import { NextPageContext } from 'next';
import nookies, { parseCookies } from 'nookies';
 
export interface AbTestSeed {
 seed: string;
}
export const COOKIE_KEY_AB_TEST_SEED = 'abTestSeed';
 
const genRandomString = () => {
 return Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
};
 
export const getOrCreateAbTestSeed = async (context: Pick<NextPageContext, 'req' | 'res'>): Promise<AbTestSeed> => {
 const cookies = parseCookies(context);
 
 if (COOKIE_KEY_AB_TEST_SEED in cookies && cookies[COOKIE_KEY_AB_TEST_SEED]) {
   return {
     seed: cookies[COOKIE_KEY_AB_TEST_SEED],
   };
 }
 
 const seed = genRandomString();
 nookies.set(context, COOKIE_KEY_AB_TEST_SEED, seed, {
   maxAge: 5 * 365,
   path: '/',
   httpOnly: false,
   sameSite: 'lax',
 });
 return {
   seed,
 };
};

次に、今回のエクスペリエンスをSAMPLE_TESTとし、以下の項目を設定します。

  • テストを開始するページ: トップページ
  • テストパターンの数: 2
  • テスト対象の割合: 全てのユーザー

【abTestSetting.ts】

export interface AbTestSetting {
 experimentId: string; // OptimizeのエクスペリエンスID
 triggerPaths?: RegExp[]; // テスト開始をトリガーするPathを指定
 numOfVariants?: number; // テストパターンの数
 targetRatio?: number; // 対象とするユーザーの割合。0~1で設定
}
 
// 全てのクエリを含む、トップページが対象
const TopPageRegex = /^\/(\?.*)?$/;
 
export const SAMPLE_TEST = {
 experimentId: 'XXXXXXXXXXXXXXXX',
 numOfVariants: 2,
 targetRatio: 1,
 triggerPaths: TopPageRegex;
};
 
export const ABTests: AbTestSetting[] = [SAMPLE_TEST];

今後複数のエクスペリエンスを実行することを考慮し、ABTestsという変数に配列で格納しておきます。

次に、abTestSeedを受け取ってテストパターンを返す関数を定義します。
OptimizeABテスト用のReact Contextも作成しておきます(詳細は後述します)。

【abTestState.ts】

import { createContext } from 'react'; 
import { ABTests, AbTestSetting } from './ABTestSettings';
import * as crypto from 'crypto';
import * as hashSha256 from 'sha256-uint8array';
 
export type ABTestVariant = {
 experimentId: string;
 variant: number;
};
 
// テストパターンを決定する
export const determineVariantsForPath = (path: string, seed: string): ABTestVariant[] => {
 const abTests = ABTests.filter((setting) => {
   if (!setting.triggerPaths) {
     return true;
   }
   return setting.triggerPaths.some((pattern) => pattern.test(path));
 });
 return _determinVariants(abTests, seed);
};
 
const _determinVariants = (abTests: AbTestSetting[], seed: string): ABTestVariant[] => {
 const variants = abTests
   .map((setting) => {
     return {
       experimentId: setting.experimentId,
       variant: determineVariant(setting, seed),
     };
   })
   .filter((variant) => variant.variant !== NO_VARIANTS);
 return variants;
};
 
const UINT_MAX = 4294967295;
export const NO_VARIANTS = -1;
 
export const determineVariant = (setting: AbTestSetting, seed: string) => {
 //Seed値をハッシュ化させてパターンを割り振る
 const digest = hashSha256.createHash().update(`${setting
.experimentId}-${seed}`).digest().buffer;
 const patternBase = new DataView(digest.slice(0, 4)).getUint32(0);
 const numOfPatterns = setting.numOfVariants || 2;
 const targetRatio = setting.targetRatio || 1;
 const pattern = patternBase % numOfPatterns;
 
 if (targetRatio >= 1) {
   return pattern;
 } else {
   // 比率が1ではない場合は、4~8byte目を使用する
   const ratioBase = new DataView(digest.slice(4, 8)).getUint32(0) / UINT_MAX;
   if (ratioBase < targetRatio) {
     return pattern;
   } else {
     return NO_VARIANTS;
   }
 }
};
 
//グローバルで状態管理するためにReact Contextを使用
export const ABTestContext = createContext({
 seed: 'empty',
});
 
//テストパターンを取得。実際にABテストを実施するコンポーネントで使用する
export const getVariant = (setting: AbTestSetting): number => {
 const seed = useContext(ABTestContext).seed;
 const variant = determineVariant(setting, seed);
 if (variant !== NO_VARIANTS) {
   return variant;
 } else {
   return undefined;
 }
};

2) Optimizeにテストパターンを送信
abTestSeedを取得する関数を_app.tsx内で実行し、メタ要素を管理するMetadataコンポーネントに渡します。

【_app.tsx】

import { NextPageContext } from 'next';
import { useRouter } from 'next/router';
import Metadata from './Metadata';
import { determineVariantsForPath } from './ABTestState';
 
const App = ({ Component, pageProps, abTestSeed }) => {
 const router = useRouter();
 return (
   <>
     <Metadata
       abTestVariants={determineVariantsForPath(router.asPath, abTestSeed)}
     />
     <Component {...pageProps} />
   </>
 );
};
 
App.getInitialProps = async (appContext: AppContext) => {
 const abTestSeed = await getOrCreateAbTestSeed(appContext.ctx);
 
 return { abTestSeed: abTestSeed.seed };
};
 
export default App;

【Metadata.tsx】

import React from 'react';
import Script from 'next/script';
import { ABTestVariant } from './ABTestState';
 
const googleAnalyticsTrackingId = 'YYYYYYYYYYYYYYY'
const googleOptimizeTrackingId = 'ZZZZZZZZZZZZZZZ'
 
interface MetadataProps {
 abTestVariants: ABTestVariant[];
}
 
const Metadata: React.FC<MetadataProps> = (props) => {
 return (
   <>
     <Script src={`https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsTrackingId}`}
     />
     <Script
       id="gtag-snippet"
       strategy="afterInteractive"
       dangerouslySetInnerHTML={{
         __html: `
           window.dataLayer = window.dataLayer || [];
           function gtag(){dataLayer.push(arguments);}
           gtag('js', new Date());
           gtag('config', '${googleAnalyticsTrackingId}', {
            page_path: window.location.pathname,
            experiments: [${props.abTestVariants
       .map((variant) => {
         return `{ id:'${variant.experimentId}', variant:'${variant.variant}' 
      }`;
       })
       .join(',')}],
     });
       `,
       }}
     />
   </>
 );
};
 
export default Metadata;

これで、トップページにアクセスがあったタイミングでOptimizeにデータが送信されるようになります。

3) テストパターンのグローバル管理、取得
(2)で記述した_app.tsxのコンポーネントをReact Contextでラップすることで、テストパターンをグローバルで状態管理します。

【_app.tsx】

import { ABTestContext} from './ABTestState';
 
//...省略...
 
return (
   <ABTestContext.Provider value={{ seed: abTestSeed }}>
     <GoogleAnalyticsMetadata
       abTestVariants={determineVariant(abTestSeed)}
     />
     <Component {...pageProps} />
   </ABTestContext.Provider>
 );
};
 
//...省略...

私たちのプロダクトでは状態管理にRecoilを使用していますが、Recoilの場合、RecoilRootごと(各ページごと)に状態が分割しているためテストパターンを全体で共有できず、React Contextを使用するようにしました。

4) コンテンツの出し分け
最後は実際の表示部分です。
テストパターンを取得し、表示を出しわけます。

【SampleComponent.tsx】

import React from 'react';
import { getVariant } from './ABTestState';
import { SAMPLE_TEST } from './ABTestSettings';
 
const pattern = ['オリジナル','パターンA']
 
const SampleComponent: React.FC = (props) => {
 const variant = getVariant(SAMPLE_TEST);
 
 return (
   <>
     <p>{pattern[variant]}</>
   <>
 );
};
 
export default SampleComponent;

上記のコンポーネントでは、テストパターンが0だと「オリジナル」、1だと「パターン1」が表示されるようになります。

※実施するABテストの内容によっては、コンポーネント自体を別で作って表示を出し分けるような実装でもいいかと思います。テスト内容に応じて調整ください。

以上がプログラム側で必要な実装となります。

■Optimize側での実装

Optimize側で必要な設定は主に以下の2つです。

1) テストパターンの作成
ABテストに必要な数だけパターンを作成します。 テストの実装は全てプログラム側で行っているため、編集ボタンでの操作は一切不要です。パターンを作成するだけでOKです。

2)ページターゲティングの設定
存在しないサイトのURLを入力してください。正式なURLを入力してしまうとエクスペリエンスがOptimize側で自動的に開始されてしまい、プログラム側で実装した処理よりも優先して開始されてしまいます。ここでは公式リファレンスを参考にして「SERVER_SIDE」と入力しています。画像のようなWarningが出ますが、無視でOKです。

その他
それ以外に必要な設定は特にありません。管理画面にはトラフィックの割り当てやアクティベーションイベントなどを設定する項目がありますが、今回のサーバーサイドでのABテストの場合、それらは全てプログラム側で設定することになるため管理画面側での設定は不要です。

あとは、GAとの連携やOptimizeのインストールが済んでいればエクスペリエンスの開始が可能なので、「開始」ボタンをクリックしてエクスペリエンスを開始させてください。プログラム側からデータが正常に送られているとデータが集計されます。

設定は以上となります。

■ハマった点

  • gtag.jsでのデータ送信
    Googleの測定サービスにデータを送信するにはgtag.jsというフレームワークを使いますが、公式のサンプルコードではgtag.jsの’set’ディレクティブが記述されているものの、今回の実装だとデータが送信されませんでした。結局’config’ディレクティブに変更して上手くいきましたが、かなり詰まりました。

  • 管理画面側で必要な設定項目
    通常のReactアプリ(SPA)でのOptimize実装の文献を読むと「アクティベーションイベントを設定してプログラム側でuseEffectで呼び出す〜」といった説明がありますが、サーバーサイドでのテストの場合はこれらの設定は一切不要です。 上述の通り、管理画面側で必要な設定は主に2つのみです。管理画面はあくまでABテストを行うためだけの土台作りのイメージ、ということを認識する必要がありました。

  • 状態管理
    状態管理にはRecoilを使用していますが、私たちのプロダクトでは各ページごとにRecoil Rootで状態を分割しているため、テストパターンの値を全体で共有できませんでした。React Contextを使うことで解決しました。

  • ABTestSeed生成のタイミング
    該当ページにアクセスした時にseed値を生成するようにすると、複数ページにまたがるABテストを実行する場合に矛盾が生じてしまいます。
    例)/hogeと/fugaで同一のテストを行いたい場合、/hogeにアクセス(オリジナル) ⇨ /fugaに遷移(テスト発火) ⇨ /hogeにアクセス(パターンA) のように、同じユーザーに対して/hogeの表示内容が変わってしまう可能性があります。
    なのでABTestSeedの生成を_app.tsxで実行することで、全てのページにアクセスがあったタイミングでseed値の生成をするようにしています。

■懸念点

  • ブラウザ(Optimize管理画面)側でテストの稼働を制御できない
    ABテストを中断するにはプログラムを修正する必要があります。管理画面でエクスペリエンスを停止すると結果の集計はされなくなりますが、テスト自体はプログラム側で実装しているため、プログラムを修正しない限り継続して稼働し続けてしまいます。 対策として、DBにテストの稼働を管理するためのデータを持たせて制御するような仕組みを検討していく必要があると考えています。

最後に

いかがでしたでしょうか。

Server Side RenderingでのABテストの実装は参考文献が少なく、詰まった部分がとても多かったですが、今回無事に実装できてよかったです。これでABテストを行う基盤が整ったので、今後は実際にテストを沢山まわしてプロダクトの分析・グロースに繋げていければと思います。

レバレジーズでは、私のようにエンジニア未経験で入社した社員でも幅広くチャレンジできる環境が整っています。ご興味のある方は、以下のリンクから是非ご応募ください!

recruit.jobcan.jp