kazokmr's Blog

試したこと、読んだこと、見たこと、聴いたことを書きたくなったら書くブログ

HA構成のアプリのHTTPセッション管理にRedis を使う

今更な話ではありますが、AWSのECS Fargate と ALBを使った簡単なデモアプリの環境構築をTerraformで進めていて、セッション管理について調べていたら「ALB側でパーシステンス (スティッキーセッション)を実装するより、ErastiCashなどをセッションストアとして使う」というのを知りました。

個人的にHA構成の経験がほとんどなくRedisも使ったことが無かったことと、またローカル環境でHA構成の動作確認が行えると便利だと思ったので作ってみました。

構成

ソースコードGitHubで公開しています。

github.com

ローカル環境で動作確認をしたかったので、Docker-compose を使って以下のような構成を用意します。

  • ロードバランサ: HAProxy
  • アプリケーションコンテナ: Spring Boot
  • セッションストア: Redis

Spring Boot アプリ

まずはSpring Boot (Spring MVC)を使って、セッションの状態を確認できる簡単なアプリを作ります。

こちらはセッション管理するBean

package net.kazokmr.study.hasession;

import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
@SessionScope
public class Reception implements Serializable {

    private static final long serialVersionUID = -3101986789734320497L;

    private String host;
    private LocalDateTime receptionAt;
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getReceptionAt() {
        return receptionAt == null ? null : DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(receptionAt);
    }

    public void setReceptionAt(LocalDateTime receptionAt) {
        this.receptionAt = receptionAt;
    }
}

コントローラではこのBeanに「状態」「ホスト名」「実行日時」を登録するメソッドとセッションを破棄するメソッドを用意してリクエストを受け付けます。

package net.kazokmr.study.hasession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDateTime;

@RestController
public class ReceptionController {

    private final Reception reception;

    @Autowired
    public ReceptionController(Reception reception) {
        this.reception = reception;
    }

    @GetMapping("/hello")
    public String hello() throws UnknownHostException {
        if (reception.getReceptionAt() == null) {
            reception.setHost(InetAddress.getLocalHost().getHostName());
            reception.setReceptionAt(LocalDateTime.now());
            reception.setMessage("セッション開始");
        } else {
            reception.setMessage("セッション中");
        }
        return String.format("%s,  ", reception.getMessage()) +
                String.format("Host: %s,  ", reception.getHost()) +
                String.format("受付日時: %s", reception.getReceptionAt());
    }

    @GetMapping("/bye")
    public String goodbye(HttpSession session) {
        session.invalidate();
        return "セッション切断";
    }
}

これをIntelliJから起動しChromeからアクセスすると以下のように出力します。

初回アクセス

リロード時

セッション切断

Spring Boot アプリをDockerコンテナで起動する

このアプリケーションをDockerコンテナから起動してアクセスしてみます。尚、ホスト名をわかりやすくするために起動時のコマンドに -h オプションを付けます。

docker run --name <コンテナ名> -p 8080:8080 -h docker-container-1 <イメージ>

docker コンテナで起動したアプリにアクセス

HAProxy をDockerで起動する

HAProxyのDockerイメージは公式に記載されている通り、公式イメージに haproxy.cfg を上書きしたイメージをビルドして起動します。

Docker

FROM haproxy:2.3
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

haproxy.cfg はアプリケーションコンテナを3つ用意し、checkで死活監視とラウンドロビンで振り分けているだけとなります。

global
    daemon
    maxconn 256

defaults
    mode http
    timeout connect 5000ms
    timeout client  50000ms
    timeout server  50000ms

frontend http-in
    bind *:80
    default_backend servers

backend servers
    balance roundrobin

    server app01 app01:8080 check
    server app02 app02:8080 check
    server app03 app03:8080 check

このhaproxyコンテナとSpring Bootアプリケーションコンテナを docker-compose で起動します。

app01 - 03 は予めDockerイメージを作成しています。またhostnameオプションを使って、どのコンテナにアクセスしているわかりやすくしました。

version: '3'

services:
  haproxy:
    build: ./haproxy
    ports:
      - 80:80

  app01:
    image: sessiontest:latest
    hostname: docker-container-1

  app02:
    image: sessiontest:latest
    hostname: docker-container-2

  app03:
    image: sessiontest:latest
    hostname: docker-container-3

結果

haproxy ではパーシステンスを行っていないためセッションが保持されません。なのでブラウザをリロードする度に、ラウンドロビン方式によって別のアプリコンテナに対して新規接続を行ってしまいます。

haproxy経由でアクセス

HAProxyでパーシステンスを実現する場合

以下のブログの内容を参考にhaproxy.cfg のbackend セクションで、JSESSIONID に振り分け先のserver追加してcookie にセットすることで2回目以降のアクセスも同じアプリケーションサーバーに接続するようにします。

Load Balancing, Affinity, Persistence & Sticky Sessions

HAProxyでHTTPロードバランシング - CLOVER🍀

backend servers
    balance roundrobin

    cookie JSESSIONID prefix nocache

    server app01 app01:8080 check cookie app01
    server app02 app02:8080 check cookie app02
    server app03 app03:8080 check cookie app03

haproxyのDockerイメージを再ビルドして起動したところ、セッションが継続されました。

haproxyでCookieにサーバー情報を埋め込んだ場合

Redis でセッション管理を行う

本題はこちらなので、HAProxyは、再度Cookie設定を戻してセッションが継続されない状態に戻します。

アプリをRedisに対応させる

Reidsをキャッシュストアとして利用するように Spring Boot アプリを変更します。

はじめに開発用にRedisのDockerコンテナを起動します。 Docker

docker run --name redis -p 6379:6379 -d redis

Spring Boot アプリケーション側の設定変更はこちらのチュートリアルを参考にしました。 https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-redis.html

RedisをSessionキャッシュにするために Spring Session Data Redis を依存関係に追加します。

更にアプリケーションからRedisに接続すためのクライアントとしてJedis も追加します。 Jedisは前述のチュートリアルでは含まれていませんが、これを入れないとSpring Boot起動時にRedisに接続できずにエラーとなってしまいます。

<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
</dependency>

最後に application.properties で sessionの保存先をredis に変えます。

spring.session.store-type=redis

これだけでHttpSessionの保存先をRedisに変えるための springSessionRepositoryFilter beanを生成してくれるようです。 またRedisの接続先やSessionタイムアウトの管理なども application.properties に追加してカスタマイズできますが、ローカルのRedisに接続するだけであればこれだけでOKです。

詳細は前述のチュートリアルをご覧ください。

Redis に保存されているセッション情報を見る

Docker Composeで環境を作る前に、IntelliJからアプリケーションを起動してRedisにどのように格納されているか見るため、アプリにアクセスしていない状態から確認します。データの検索はRedisのクライアントツールであるRedis-cli を使用します。このツールは Redisにバンドルされているので、Redisコンテナの中に入り利用してみます。

docker exec -it redis /bin/bash
root@redis:/data# redis-cli
127.0.0.1:6379>

接続したら keys * と実行すると保存されているKeyの一覧が表示されます。まだアプリケーションにアクセスしていないので (empty array) と出力されます。

127.0.0.1:6379> keys *
(empty array)

今度はブラウザからアクセスしてから検索します。

127.0.0.1:6379> keys *
1) "spring:session:sessions:expires:7f57263c-0743-471f-b4cc-5e3d41b60e3d"
2) "spring:session:sessions:7f57263c-0743-471f-b4cc-5e3d41b60e3d"
3) "spring:session:expirations:1611233700000"

"spring:session:" から始まるKeyが3つ出力されました。この中で2)にセッションデータが入っているようです。 type <key> でデータの形式を確認するとhash と出力されたので hgetall <key> で検索します。

127.0.0.1:6379> hgetall spring:session:sessions:7f57263c-0743-471f-b4cc-5e3d41b60e3d
1) "sessionAttr:scopedTarget.reception"
2) "\xac\xed\x00\x05sr\x00%net.kazokmr.study.hasession.Reception\xd4\xf3\x87s#\xeev\x8f\x02\x00\x03L\x00\x04hostt\x00\x12Ljava/lang/String;L\x00\amessageq\x00~\x00\x01L\x00\x0breceptionAtt\x00\x19Ljava/time/LocalDateTime;xpt\x00\x11MacBook-Pro.localt\x00\x15\xe3\x82\xbb\xe3\x83\x83\xe3\x82\xb7\xe3\x83\xa7\xe3\x83\xb3\xe9\x96\x8b\xe5\xa7\x8bsr\x00\rjava.time.Ser\x95]\x84\xba\x1b\"H\xb2\x0c\x00\x00xpw\x0e\x05\x00\x00\a\xe5\x01\x15\x15\x18\x02:|FXx"
3) "creationTime"
4) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01w$\xe6\xd1\xf6"
5) "maxInactiveInterval"
6) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b"
7) "lastAccessedTime"
8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01w$\xe6\xd1\xf6"

所々にbeanのデータが読めるのでそれっぽいですがわかりづらいですね。そこで下記の記事に書かれているようにセッションデータのシリアライズ方法をJava標準のものからJson形式にしてみます。

Spring Boot+Spring SessionでスケーラブルなステートフルWebアプリが簡単につくれるよ〜 #nginx - Qiita

余談でSpring Securiy も使うなら以下の記事のようにSpring Securityの拡張モジュールの適用もした方が良いと思います。

Spring Security 4.2 主な変更点 #Java - Qiita

package net.kazokmr.study.hasession;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

@Configuration
public class HttpSessionConfig {
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

(注意) JSON形式に変更した結果、LocalDateTimeで持っていて受付日時のシリアライズで例外が発生してしまいました。本来なら対応するべきですが今回はString型で取得した現在時刻を保持するように変えました。

アプリを起動する前にRedisに残っているデータを全て削除して、再起動しセッションデータを検索します。

127.0.0.1:6379> hgetall spring:session:sessions:a3b43f71-3c5b-48e6-a700-807d7bcb48c5
"sessionAttr:scopedTarget.reception"
"{\"@class\":\"net.kazokmr.study.hasession.Reception\",\"host\":\"MacBook-Pro.local\",\"receptionAt\":\"2021/01/21 21:50:27\",\"message\":\"\xe3\x82\xbb\xe3\x83\x83\xe3\x82\xb7\xe3\x83\xa7\xe3\x83\xb3\xe9\x96\x8b\xe5\xa7\x8b\"}"
"creationTime"
"1611233427618"
"maxInactiveInterval"
"1800"
"lastAccessedTime"
"1611233427618"

セッション情報がJSON形式で出力され、生成日時や最終アクセス日時なども見やすくなりました。

とこでRedisに保管されているSESSIONキーですが、これはJSESSIONIDではなく、SESSIONという専用のCookieを使っているようです。ですが、Redisで検索するとUUIDの形式で出力されますが、Chromeの開発者モードで見るとUUIDでは出力されていません。なのでChormeのCokkieからSESSION情報を検索するのは難しそうです。

RedisというかSpring Sessionを利用すると、SessionIDのCookie名が JSESSIONIDではなく SESSION になるようです。名称などのCookieのカスタマイズは以下のように CookieSeriarizer Beanを定義してあげれば良さそう

docs.spring.io

Redis を追加したDocker Composeでアプリを起動する

docker-compose.yml にはRedisを追加し、Redis > app01-03 > HAProxy の順番に起動するように依存性を追加しました。

version: '3'

services:
  haproxy:
    build: ./haproxy
    depends_on:
      - app01
      - app02
      - app03
    ports:
      - 80:80

  redis:
    container_name: redis
    image: redis

  app01:
    image: sessiontest:latest
    hostname: docker-container-1
    depends_on:
      - redis

  app02:
    image: sessiontest:latest
    hostname: docker-container-2
    depends_on:
      - redis

  app03:
    image: sessiontest:latest
    hostname: docker-container-3
    depends_on:
      - redis

また Spring BootアプリのコンテナもDocker Compose のRedisと接続できるように以下の設定を変更しました。

  1. Docker Compose 専用の application-docker.properties を追加し、compose上のcontainer_name: redis に接続するようにホスト名を追加しました。
spring.redis.host=redis
  1. アプリケーションコンテナをビルドする前に、Maven で clean package を実行するようにしました。これは maven のTestフェーズでRedisとの接続が必要なため、ローカル環境のRedisコンテナにアクセスしてMavenビルドを実行したかったためです。

  2. アプリケーションコンテナ用のDockerfile の内容を変更してDockerイメージを再ビルドしました。 上記のNo.1と2に対応する形で、マルチステージビルドを止め、ENDPOINTでjarを起動時にapplication-docker.properties を参照するようにprofileのactivate 設定オプションを追加しました。

FROM openjdk:11-jre
COPY ./target/*.jar .
EXPOSE 8080
ENTRYPOINT ["java","-jar","ha-session-0.0.1-SNAPSHOT.jar","--spring.profiles.active=docker"]

Redisを使ったセッションストアでアクセスした時

感想

新しい技術では無く沢山の情報もあったことやRedisのセッション管理もSpring Session Data Redisが良い感じで行ってくれるので難しくは無かった。

ただ Redisでセッション管理するにはアプリケーション側の方で設定を行うことになるので利用しているフレームワークなどによっては導入コストが掛かる場合もあると思うので、その場合はロードバランサーのパーシステンスの採用を検討した方が良いかもしれない。

これでローカルの開発環境で冗長化構成の動作確認を簡単に行うテンプレートを用意することができた。これに、HTTPS接続とRDBMSの接続も追加しておくと個人的にはより便利になりそう。

JetBrains Academy で Java Developer コースを進めてみた

今週末で無料で受けられる期間が終了するので、使ってみた感想を書きます。

www.jetbrains.com

動機

8月に Twitterか何かで 見かけて「最近コードを書いていないなー」って思ったので、鉛切った身体を鍛え直すつもりで、最大で2ヶ月ちょっとは無料で受けられるので使ってみました。

進め方

ログインするとまずはTrack と呼ばれる、言語ごとのコース選択があります。私は Java Developer を選択しました。 他にはPython Developer があり、Kotlin Developer や Web Developer などがβ版として選択ができます。

次にプロジェクトを選びます。プロジェクトはこの学習のなかで完成させるアプリケーションとなっていて、プロジェクトの中で5−6のステージに別れています。各ステージではトピックと呼ばれる特定のスキルに対する説明と演習からなる学習が並んでおり、トピックを順番にこなすとステージの最後にプロジェクトの課題が提示され、これまでのトピックで学んだことを活用しながら、課題をクリアします。

トピックの演習とステージの課題は、IntelliJ IDEAと連動しておりIntelliJ でコードを書いて評価します。 ちなみに間違えても特にペナルティはなく、何度もでも挑戦できますし、クリアしないと次に進めない仕組みになっています。 また各演習については参加している他のメンバーがコメントしたりヒントを提示していたり、クリアすると他の人のコードを見たり、自分のコードを公開したりできます。 海外のプログラマーのコードと自分のコードを比較するのも勉強になることもあったり、あまり変わらないなーと思ったり、なんでそんな書き方しているんだろ?って思ったりと面白かったです。

プロジェクトとトピック

プロジェクトはJava Developerだと32種類あり、Easy, Medium, Hard, Challenging の4つの難易度になっています。 トピックは、Javaに関する様々なスキルを学ぶことができ、Java以外にもデータベースやフロントエンドに関するトピックスや、ソフトウェア工学アルゴリズム、代数・幾何と言ったジャンルもあり、全部で403のトピックがあり、知らないことも多かったりしてとても勉強になりました。

Java Developer – JetBrains Academy

JetBrains Academy - Learn programming by building your own apps

進捗

2ヶ月続けて、11プロジェクト(Easy:1, Medium: 3, Hard:5 , Challenging:2 )完了し、トピックは 221/403 (55%) でした。 Hard位だと割とスムーズにクリアできるのですが、Challengingは時間がかかりました。平日は朝と夜に少しずつ進めて、休日に家にいるときに進めていましたが、子供の面倒や外出などもあって、まとまって進める時間はあまり取れませんでした。 f:id:kazokmr:20201022220609p:plain (一応、クリアしたプロジェクトはGitHubに上げているのですが、課題の回答などにもなり規約とかも有りそうなのでPrivateにしてあります。)

感想

Javaについて言えば、色々な範囲を細かくカバーして演習が出来たのでとても勉強になった。特にLambda や StreamAPI などは前職のJavaのバージョン的な都合もあって、あまり触れる機会がなくて、IDEの予測機能などを駆使したパワープレイ的な感じで書くことが多く、いまいち理解できてなかったのですが、関数型インターフェースの学習から始まり、StreamAPIの様々な使い方をトピックとして学べたので力がついたと思います。

またアルゴリズムの勉強や代数・幾何の連立方程式行列式などの計算プログラムを作るプロジェクトもあり、学生時代を思い出しながら復習できたこと、あるいはブロックチェーン の基本的な仕組みが理解できるプロジェクトなどに取り組むことで新しいことも覚えられたのがとても良かったです。

またコースは全て英語になっているので、細かいニュアンスや専門用語にわからない部分が多かったりしました。問題文を見ても何を求められているのかがわからなかったりする部分が多く、DeepLは欠かせなかったです。 海外の人が「この問題の説明はよくわからないな」「英語というのは時折、正確に伝えないことがある。」みたいなことをコメントしていることもあり、自分の英語スキルの問題だけではなさそうでしたが。

またプロジェクトの課題についても、難易度に関係なく説明が大雑把だったりして、どうしたらいいのかを探りながら進めたり、初期の頃は「このプロジェクトのステージやトピックでやったことを使ってクリアしないと」という気持ちが強すぎて、シンプルにコードを書くことができなかったりしました。

今後

無料期間が過ぎたら受講料が発生するのですが、現時点で中途半端な進捗なのとまだ学習してみたいトピックもあるので、多分継続することになると思います。最初のEasyコースが簡単だったので2ヶ月あれば終わるんじゃないかなと思っていたのですが甘過ぎました。

と、JetBrains Academyがベストかは分かりませんが、IntelliJを使いながらプログラミングスキルを上げつつ、英語の勉強にもなるので自分にはあっているなとは感じました。

BDD in Action 2nd Edition 第6章を読んだ

BDD in Action 2nd Edition の第6章は、正しい要件を定義することの最終章として、前章で探索した主要な例を使って、フィーチャーのシナリオをGherkinフォーマットで書く方法を説明しています。

www.manning.com

前半は、Gherkinの主要なキーワードの用途や使い方の説明で、これはCucumberのチュートリアルなどでも学べる内容でした。 この中で知らなかったキーワードにはBackground があり、複数のScenarioの冗長性を排除することができます。

これは書籍に載っていたことではありませんが、キーワードにRuleExampleが追加されていました。 RuleFeatureの中で、Scenarioをグループ化できます。ExampleScnarioの別名なので、用途はScenarioと変わりません。

RuleExampleは、より実例マッピングに近い形式で表現できます。

章の後半では、優れたGherkinの書き方を紹介しています。

Gherkin を書く際に気を付けることとして以下のようなことが挙げられていました。どれもプログラミングの原則でよく言われていることと似ています。

  • シナリオでは宣言を書き、命令的な指示は書かない。(Howではなく、Whatを書く)
    操作することや入力する内容などを1つ1つ詳細に書くのではなく、シナリオで達成したい目的に対して何をしなければいけないかに着目して書くとよい。

  • 1つの関心事に着目してシナリオを書く。 特に初めはシンプルな関心事から初めて、徐々に複雑なシナリオを組み立てる。

  • シナリオの登場人物に個性を付ける(ペルソナを作る) ユーザーや管理者のような味気ない役割だけではなく、目標・能力・背景などを付けると良い。また初めからペルソナを詳細に書くことに時間を費やさずにシナリオを書きながら付け足すと良い。

  • シナリオには必要なことだけを書く。容易に想定できることは省略する。
    とあるフィーチャーのシナリオを書く時に、そのフィーチャー(画面)にたどり着くまでの汎用的で容易に想像がつく手順まで記述しない。特にGivenについてはシナリオを検証するために必要な前提条件だけを書く。

  • 既存のテストスクリプトやテストコードの手順をそのままシナリオとして書かない。
    これらは指示・命令的なステップが多いから。「比較する」や「確認する」が含まれているシナリオは大抵、テストスクリプトのような書き方になりがち。

  • 各シナリオは独立して実行できるようにする シナリオごとに必要な前提条件を宣言して独立して実行できるようにする。一連のシナリオの纏まりを順番に実行する必要がある構造にはしない。(あるシナリオが失敗したら、後続のシナリオが実行できないことが無いようにする。)

個人的には経験上、指示的な書き方になりがちなので、この辺を気をつけていきたい。

ここまでの章で、シナリオをまとめたFeatureファイルを成果物とした、要件定義の進め方を学んできました。これまで自分が行って来たやり方とは全然違っているので慣れるまでに時間がかかりそうだが、作るべきことにより着目して明確に分かりやすい書き方で整理できそうに思えました。

BDD in Action 2nd Edition 第5章

5章を読み終えましたので、学んだことをまとめてみます。

www.manning.com

5章は、BDDの活動サイクルの中の Illustrateフェーズでの作業として、3〜4章の Speculateフェーズで特定し、詳細化したフィーチャー、あるいはユーザーストーリーを明確にしていきます。

フィーチャーの内容を明確にするための手段は「例を使ったビジネス側との会話」です。

例を使って話し合う

  • 話し合いは、ユーザやステークホルダーも参加して、フィーチャーの受け入れ基準を理解し、定義し、合意していく。

  • 話し合いでは、良く適切な質問ができなかったり、脱線したりして、思うように進まずに長引くことがある。

  • フィーチャーに対して、ビジネス側の言葉を使った例で説明することで、話し合いが円滑に進む。

  • 例を使って話う会う時は反例も出すことで矛盾や誤解を知り、不確実なことを洗い出して明確にすることができる。

  • 例を使って理解したことを確認したり拡張したりするために追加の例を使う。

表を使って例を整理する

  • 表はインプットと期待する結果が簡単に説明できるような要件に適している、データ中心的なアプローチ

  • デシジョンテーブルを作るイメージ。だけどこの段階では網羅することよりも要件の理解を優先する

  • アプリケーションの振る舞いなどは表現できないので、実例マッピングやフィーチャーマッピングと組み合わせて使うのが良い。

実例マッピング (Example Mapping)

  • フィーチャー(ストーリー)に紐づくビジネスルールや制約、そしてルールに対する例と反例のリストを特定していく。

  • 目的は、フィーチャーやルール(受け入れ基準)の共通理解を得ること

  • 挙げられたルール、または例の数が多いと、フィーチャーが大きいか、複雑なことを示していて分割した方が良いかもしれない。

  • 話し合いは一つのフィーチャー(ストーリー)に対して、25-30分のタイムボックスで行い、そのフィーチャーに集中する。

  • 話し合いの最後に「作業を始めるために十分な理解を得られたか?」「更に話し合う必要があるか?」を投票して決める。

  • 話し合いで回答できない例や質問は、別に記録しておいて、その話し合いではそれ以上議論しないようにする。

  • 例を追加していく時は「もしこうだった場合は?」「そのルールはいつもそうなのか?」「ルールが適用できない場合はあるか?」などの視点で質問すると良い。

フィーチャーマッピング

  • 大きく、不明確なフィーチャーに適している。

  • 実例マッピングと違う点は「例をステップ単位に分けて説明する」「ステップの最後に期待する結果となる結論(Consequence)を書く」などがある。

  • 実例マッピングが「ビジネスルールを基に例を挙げていく」ように進めるとしたら、フィーチャーマッピングはどちらかといえば「ストーリーから想定される例を上げていって、ビジネスルールや制約を特定する」ようなイメージに読み取れた。

  • フィーチャーマッピング自体は、大きなフィーチャー(あるいはエピック)をルールや制約単位に分割することも目的の一つのなので、その後実例マッピングを使って理解を深めていくと良さそう。

スリーアミーゴス

例を使った話し合いでは、テスター、プロダクトオーナー (ビジネスアナリスト)、開発の3つの役割を持ったメンバーがそれぞれの視点でルールや例を質問していくと良い。

  • テスターは、細部に注意を払い、検証することに着目して、エッジケースなど見落としがちなシナリオを指摘する

  • プロダクトオーナー は、ビジネス視点の妥当性や相対的な価値に着目して、シナリオを指摘する

  • 開発者は、技術的な検討事項を指摘する。

まとめ(感想)

  • 顧客と要件定義を行う時に、わからないことがあれば例を使って話し合うことがたまにあったが、それを意図的に利用して要件の理解を深めることが目的。

  • 付箋あるいはホワイトボードツールを使って、ルールと例を整理する。

  • ここで作ったルールや例のうち主要なものは、Gherkinのシナリオ(Given...When...Then)として使うことになる。だけどこの時点ではそれを意識して書く必要はない。実際このフェーズで上がった例のほとんどはシナリオとしては使わないらしい。

  • この5章は、初版には載っていなかった(あるいは前後の章の中で少し触れていた程度だった)が、2nd Editionになって、「フィーチャーやストーリーを定義する」と「Gherkinのためのシナリオファイルを書く」の間にこの章が作られたことを考えると、これまでフィーチャーからシナリオファイルに変換する作業が難しかったので、例を用いた様々なテクニックが使われるようになったのかなと推測した。

次の6章は、要件定義について解説した第2部の最後に当たり、成果物としていよいよシナリオを書いてFeatureファイルを完成させていくようなので楽しみ。

BDD in Action, Second Edition 第4章を読んだ

4章を読み終えたのでまとめ。*1

www.manning.com

さて4章は、3章で出てきたフィーチャーの詳細化の方法を説明しています。

3章で疑問に思ったフィーチャーの定義

3章でフィーチャーと特定された中には「これはフィーチャーなのか?」と思う物がありました。 kazokmr.hatenablog.com

この疑問について章の最初に触れており、フィーチャーに限らずこれらの概念として用いられる用語は、扱う方法論によって少しずつ定義が異なることがよくあり、混乱の基になっていると書かれていました。

そこでBDD(あるいは、この本)の中では、用語を次のように定義しています。

  • この本では、主にケイパビリティフィーチャーユーザーストーリー例(Example)の4つを用いて説明する。
  • ケイパビリティは、ユーザーやステークホルダーが「何かを行うための能力」を表し、ソフトウェアの実装に依存しない。
  • フィーチャー, エピック, ユーザーストーリーは、ケイパビリティをサポートするためのもの。 扱う規模が違うが概念はほぼ同じで「エピック => フィーチャー > ユーザーストーリー *2」となる。

フィーチャー

前章で利用したインパクマッピングにおいて、最後の成果物(Deliverable)をフィーチャーとします。 ちなみに、前段の影響(Impact)を、ケイパビリティとして扱います。

インパクマッピングから、フィーチャーを定義すると次のようになります。

Feature: "成果物(Deliverable) "
In order to "影響 (Impact)"
As a "関係者(Actors)"
I want to "成果物をビジネス用語で表す。"

起点となるGoalがここには現れませんが、「そのフィーチャー(Deliverable)は、ケイパビリティ(Impact)を関係者(Actors)に与えるために提供する。」と表現できるので、個人的にフィーチャーが考え易くなると感じました。

また、3章および元々のインパクマッピングでは「影響をHow、成果物をWhat」と説明しているが、フィーチャーを定義するときは「影響をWhat、成果物をHow」と表している。 これについては、目標を機転に成果物を特定していく時と、特定した成果物を表現する時の違いと考えればそれほど違和感を持たなかった。*3

フィーチャーとユーザーストーリーの違い

BDDでは通常、ユーザーストーリーは、フィーチャーを小さく分割して定義していくそうです。*4

ユーザーストーリーがフィーチャーと大きく異なる異なるのは「それ単独でリリースする価値があるかどうか?」と言う点であると説明されていました。*5

ユーザーストーリーの主な概念は(目的?)は次のようになります。

  • フィーチャ-における1つの事象、1つの側面を切り取ったもの
  • ステークホルダーとの会話をより良く進め易く、また、開発し易い大きさにしたもの
  • 1回のイテレーションでリリースできるサイズにしたもの

フィーチャーについてより深く考えていく過程で、思いついた具体的な状況や例を一つ一つをユーザーストーリーとすると作りやすいと感じました。

Real Options と Deliberate Discovery

訳すと「現実的な選択」と「意図的な発見」となるかなと思います。

ステークホルダーの目標を達成するためのケイパビリティに役立つと考えるソリューション、つまりフィーチャーを検討する中で不確実なことが出てきます。それらは大抵、自分たちの無知や誤解から生じます。このような不確実性に向き合って要件に対する共通理解を得るために、この2つの原則について説明しています。

Real Options (現実的な選択)

不確実性が原因で、最適だと考えるフィーチャーが思いつかない、あるいは確信が持てない場合に用いる代替案や保険的なフィーチャーを考えておく事を指します。

この目的は「決断を可能な限り先延ばしにする事」にあり、最適なフィーチャーを考える時間を確保することにあります。 そのためにReal Optionにはいくつかのルールがあります

  • Real Optionにも価値を持つ必要がある。
  • Real Optionに期限を設ける。
  • 何事もすぐに決断をしない。
  • でも、最適なフィーチャーである確信が持ったら、期限を待たずに実装する。
Deliberate Discovery (意図的な発見)

最適なフィーチャーを特定するために、無知なことを学んだり、誤解を無くして共通認識を持つ事を積極的に進める考え方で、不確実なことはプロジェクトのリスクになるので、早く解消する事を目的とします。

本に書いてあった説明だと、例えばバックログリファインメントで、どのフィーチャーから取り上げていくかを考えたときに、「最適な方法が明確なもの」よりも「不確実性というリスクを持つもの」から優先して取り掛かることで、プロジェクト全体の明確さを底上げした方がいいとありました。実際にはバランスが重要ではありますが。

この2つの原則を併用して、不確実性を取り除いた最適なフィーチャーを積極的に特定していくと良さそうです。

感想

これまでは最初から、断片的なユーザーストーリーを作ろうとして、ストーリーが上手く定義できなかったり整理できないことが多かったので、「ケイパビリティ -> フィーチャー > ユーザーストーリー」と順番に思考していくと作りやすそうだと感じだので、チャレンジしていきたい。

(おまけ) describe と illustrate

どちらも 「説明する」 と言う意味だが、この章では主にDescribeを使っていて、次の章からはIllustrateフェーズと呼ぶように後者が話の中心になる事を明示していたので、この意味の本質的な違いを調べてみました。

describe
  • 「詳細化する」と言う意味もある。
  • 大きなフィーチャーを小さく分割していき、ユーザーストーリーに分割していく流れは、まさに詳細化だと感じました。
illustrate
  • 「解説する」「例となる」と言う意味合いが強い。
  • 明確にするようなハッキリとさせる様子があると感じました。
  • 次章で紹介する例(example)などは正に、この意味合いになると思います。

*1:ちなみにこれを書いている時点で、次の5章までしか公開されていない。

*2:実際には、フィーチャーとユーザーストーリーには大きな性質の違いがあるのですが、それは後述します。

*3:後者において成果物を実装するコードと捉えればHowと言えるし、そのフィーチャーの目的と捉えれば影響すなわちケイパビリティをWhatと表現することはそれほどおかしくは無いと思う。

*4:他の方法には、大きなテーマからユーザーストーリーを考え出す方法があり、どちらから言えば自分はこの考え方だった。

*5:ちなみにビジネス価値はシステムが対象としている業界によって変わるので、ある場合はユーザーストーリーと定義された物が、違う状況ではフィーチャーとして価値が出せることもあります。

BDD in Action Second Edtion の3章を読んだ感想

BDD in Action Second Editionの3章を読み終わりましたので、この章の感想を記録しておきます。

www.manning.com

第3章は、Specurate(推測)フェーズとして、プロジェクトの最上位のコンセプトとなるビジョンをスタートに、ビジネスゴール, ケイパビリティ ,フィーチャー と順番に特定する方法を説明しています。

BDDのためというよりは、問題の解決方法を仮定するまでの考え方と色々なテクニックを紹介しています。

また章を通して「コミュニケーションを重ねること」と「解決方法を仮定して証明すること」の重要性を説いているようにも感じました。

ビジョン

ビジョンステートメントを作る。本の中では「キャズム」の著者が提唱し「アジャイル サムライ」のエレベーターピッチの書き方でも採用されているテンプレートを使っている。

ビジネスゴール

ビジョンに基づいて、達成したい事と達成する事で得られる利益を定義する。SMARTを活用した明確な目標を立てる。要求されたことの本質的なゴールを特定するために、その要求が「ビジョンにとってなぜ有益なのか?」を明らかにするためには、なぜなぜ分析を使っている。これについては「ザ・ゴール」を読むとよりわかりやすそう。

インパクマッピング

「ゴール -> 関係者(アクター) -> インパクト(影響/効果) -> 成果物」と関係性を定義することで、必要なケイパビリティあるいはフィーチャーを特定していく。

  • ゴール: ペインポイントとして、現在抱えている問題を解決する方法を定義する。 (もちろんSMARTやなぜなぜ分析も使って洗練させると良いと思う。)
  • 関係者: ゴールに関係するステークホルダーやユーザーを定義する。直接的/間接的を問わず、ゴールに対して恩恵を受ける人を考える。
  • インパクト: 関係者がそのように振る舞うことで、ゴールが達成できると仮定する行動を書く。
  • 成果物: その行動を起こすための「何か」を仮定する。ソフトウェアやツールで実現することにこだわる必要な無い。

インパクマッピングは、仮定の検証を繰り返す事でアップデートしていく。なので検証で判断するための測定基準も考える。

パイレートキャンバス

インパクマッピング、パイレートメトリクス、そして制約理論*1を組み合わせて、幅探索アプローチで(TODO: フィーチャーやケイパビリティ)を特定するやり方。

パイレーツメトリクスはAARRR*2と呼ばれる、Aqcuition(獲得)、Activation(活性化)、Retention(保持)、Referral(紹介)、Return(利益)の5つの視点で測定基準で分析を行うこと。本の中ではこれらの測定基準を使って、ToCにおけるボトルネック、つまり制約を特定していく。

パイレートキャンバスはこれを応用して、この5つの視点ごとに業務の制約とその制約を測る指標を考える。その後にこの制約と指標を目標にして、インパクマッピングと同じように関係者やインパクト、成果物を特定する。この成果物をエピックになる。

インパクマッピングとパイレートキャンパスの使い分け

明確で優先するべき目標が定まっているかどうかがポイントと感じた。目標に対する関係者・インパクト・成果物を特定するプロセスはどちらも同じなので、インパクマッピングはどのみち使っていくことになる。

パイレートキャンパスは目標を考えることから始めるので、多くの目標が生まれ、それぞれに対するインパクトや成果物を順番に特定する幅優先アプローチとなる。この方法は労力や時間は掛かるが、幅広いアイデアが生まれる可能性が高く、どの目標から優先して取り掛かるべきかが把握しやすいことが特徴。

パイレートキャンバスを使って、ペインポイントとなる潜在的な制約(ボトルネック)を特定し、それを向上させるためのケイパビリティ(エピック)について、ソフトウェアによる解決方法に拘らずに話し合って見つけていくやりとりは、劇中作とは言え興味深く読むことができました。

感想

相手の要望を初めから、ソフトウェアで解決する方法から考えたり、相手からも「ソフトウェアをこうして欲しい。」といった感じで要求されることが多いかった。これはつまり、Featureの要望や検討をしていると言うことになるのだが、正しいフィーチャーを特定するためには、まず「何ができるようになると嬉しいのか」といったケイパビリティを特定するプロセスが重要でした。

この章ではプロジェクトや企業のビジョンから、フィーチャーを特定するまでの説明だったが、ゴールとケイパビリティの特定に重点が置かれているように感じた。フィーチャーについては大枠を掴んだだけといった感じだったので、次章以降でこの荒削りなフィーチャーを洗練する方法を説明していくと思います。

英語の表現を日本語で表すのが難しい言葉が多くなってきた。そのまま英語で表現した方が意味がより理解できる気がする。この章は特にそれを感じた。

*1:ゴールドラット氏が著書「ザ・ゴール」で提唱した、Theory of Constrains。ちなみにこれを知るために「ザ・ゴール」と「ザ・ゴール2」のコミック版を買って読みました。

*2:これが海賊の雄叫びのようなことがパイレーツメトリクスの語源みたい

BDD in Action Second Edition の第2章の感想

第2章を読み終えました。この章から構成が初版と変わってきているなと感じました。*1

www.manning.com

この章ではプロジェクトのライフサイクル全体におけるBDDの使い方を理解する目的で、架空のプロジェクトを舞台にして話が進んでいきました。

また後半は実際にCucumberを使って、featureとグルーコード(Definition step)を起点に、テストしながらプロダクションコードを書き進めます。

実践部分は、掲載されているコードが間違っていたり(古い?)、所々説明が端折られていたので、実際に全部のテストを通すためには自分で考えながら進める必要がありました。 *2

英語を訳しながら進めたことや前述の様なこともあってか、BDDスタイルの書き方を やらされた感が強くあったり、スッキリしないところもあったので、これはまた自分なりにやり直してみたいと思います。

その他、この章で感じたこと

  • Cucumberの使い方ではなく、BDDの考え方を学ぶストーリーだったのが良かった。(とはいえ、DefinitionStepクラスのテストを通すためにはCucumberの初歩の使い方は知らないと詰まりそうですが。)

  • DefinitionStepを起点にコードを実装し始めるときは、Then -> When -> Given の順番に実装していくと良い。GivenとWhenは、前提と入力の位置付けなので、そこはリテラルな値やモック的なメソッドを仮実装したものを利用して、まずはThenを実証できる様にして置いた方が、組み立てやすいからと理解しました。

  • コードを実装していく所は基本的にTDDで進めるのですが、実装クラスに対応した「実装クラス名 + Test」の様なクラスを用意するのではなくストーリーレベルでテストクラスを作ったり、テストメソッドの中でWhenやThenの考え方を取り入れるなどBDDスタイルなテストコードの書き方も取り入れて、使い分けるのも良いと思う。

  • 真の要件を見つける時に利用していた、インパクマッピングも初めて知りましたが、Why->Who/Whose->How->What の流れで分解するやり方は問題の解決策を考えやすい方法だと感じましたし、本文を見ていて自分がWhatだと思っていたことはHowの段階であり、さらに分解して具体的にする必要があるんだと気付けました。

  • シナリオの記法は目的に応じて使い分けると良い。例えば、「ビジネスの目的のためにユーザーにさせたいシナリオなら、"Given ... When ... Then"」「ユーザーの目的のために達成したいシナリオなら、"As a … I want … so … that"」 と言った感じに分けて使うと良い。

  • 開発チームやテスターといったプロジェクト側のチームが「本当の要件を発見すること」を強調していて、顧客側から「こう言う問題をこう解決したい」の様に、要件だけでなく解決方法も指示されたことも多かったので、まずはそもそも問題の本質を自分達も一緒になって改めて考えることを気をつけないと再認識しました。

実体験で、本章の様な顧客との会話のやりとりは要件定義ミーティングでも頻繁にあったのですが、この様なミーティングは、毎週または隔週の数時間だけと限られていたので、正しい要件を見つけるには時間が全然足らなかった。さらには、実装する機能のボリュームはやたらと多かったので、こんな進め方だとそりゃ上手くいかないよな。と再認識した。

この辺の要件定義の進め方や正しい要件の見つけ方は、BDDの学習を通して、身に付けていきたいです。

*1:初版を読んだわけではなくて、目次のセクションや本文をざっと見たときの印象ですが。

*2:著者の方がGithubに最終形となるコードを上げているので、それも参考にしました。