Techtouch Developers Blog

テックタッチ株式会社の開発チームによるテックブログです

ユースケース層 大規模リニューアル:安全な移行のための3つのアプローチ

バックエンドエンジニア taisa です。最近腸活によって少しだけ体重が減りました。テックタッチは最近クリーンアーキテクチャにおけるユースケース層の大規模なリニューアルを行いました。本記事では、リニューアルの際に工夫した内容を簡単に紹介します。

はじめに

テックタッチのバックエンドはクリーンアーキテクチャとドメイン駆動設計(DDD)を組み合わせた構成で開発を進めています。詳細な理由や背景は割愛しますが、開発生産性の向上を目的に、クリーンアーキテクチャにおけるユースケース層のリニューアルを実施しました。その際に以下のアプローチを採用することで、安全かつ効率的な移行が実施できました。

  1. ユニットテスト・インテグレーションテストを用いた差分検証
  2. 同時実行パターンによる差分検証
  3. 動的な切り替え機能を利用した新系への段階的な移行

前提条件

リニューアル完了の前提条件の1つとして「旧ユースケースと新ユースケースでは、処理結果のデータ構造が完全に一致すること」があげられます。つまり API I/F や永続化データは何も変わらない状態で、ユースケースだけを旧系から新系に切り替えることができれば良いということです。

※ ここでのユースケースは、ユーザー作成・パスワード更新・ユーザー削除のようなアプリケーション固有のビジネスルールを記述する層を指しています。

1. ユニットテスト・インテグレーションテストを用いた差分検証

まずはユニットテスト、インテグレーションテストのレイヤーで新、旧系で差分が出ないことを確認する必要があります。

ユニットテストの拡充

差分比較をするためには、まず元のコードに対するテストコードが必要です。テックタッチはある程度テストコードが揃っている状態でしたが、ユースケース層を大きく変更する場合、DB の Mock テストがテストの意味をなさない等のこともあり、ユニットテストを拡充することから始めました。

すべてのユニットテストの拡充ができたら、新系ユースケーステスト内に旧系ユースケースの呼び出しを追加し、結果が変わらないことを確認しました。

新系ユースケースのテストに旧系ユースケースを追加した例)

// ....
for _, mode := range []string{"old", "new"} {
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            u := &UseCase{/*...*/}
            var got *output.Output
            var err error
            switch mode {
            case "old":
                got, err = u.OldUsecase(tt.args.ctx, tt.args.param)
            case "new":
                got, err = u.NewUsecase(tt.args.ctx, tt.args.param)
            }
            if (err != nil) != tt.wantErr {
                t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            opts := cmp.Options{/*...*/}
            if diff := cmp.Diff(tt.want, got, opts); diff != "" {
                t.Errorf("mismatch (-want +got): %s", diff)
            }
        })
    }
}

インテグレーションテストの拡充

テックタッチバックエンドのインテグレーションテストは runn を利用した API 単体・結合テストのことを指します。詳しくは下記記事を参照ください。

runn と Testcontainers で「ちょうどいい」Go API テスト

インテグレーションテストでは、実際のユースケースに沿った流れで API コール、レスポンスのキャプチャを行い、新系と旧系で結果が変わらないことを CI で確認できる状態としました。

図のようにインテグレーションテストは、API コール時に experimental header を付与し handler 内で新旧ユースケースを切り替えることで実現しました。

テストの拡充により、この時点で9割以上問題ないことは確認できましたが、例えばレスポンスフィールドにおける空とnullの違いなど、細かい差分含め想定しきれていないケースが残っている可能性があります。

このような想定しきれていないケース発生時のリスクを最小限にした上で、問題なく新旧切り替えを実現するために以下の対応を行いました。

2. 同時実行パターンによる差分検証

新ユースケースへの移行において、データの整合性確保が重要な課題であるため「同時実行パターン」を採用しました。同時実行パターンについては、様々な呼び方があるかもしれませんが本記事では「モノリスからマイクロサービスへ」を参考にしています。

同時実行パターンとは

同時実行パターンについて、モノリスからマイクロサービスへでは以下のように書かれています。

同時実行では、古い実装と新しい実装のどちらかではなく、両方の実装を呼び出して結果を比較することで、それらが同等かを確認する。両方の実装を呼び出すものの、どちらか一方の実装が真の情報源としてみなされる。通常、新しい実装が信頼できると検証できるまでは、古い実装が真の情報源とみなされる。

また、本対応では同時実行パターンを効率的に実行するために AWS AppConfig を利用しました。

AWS AppConfig とは

AWS AppConfig は以下のような機能を有します。本対応では、単純な機能フラグとして活用しましたが、他の AWS サービスと関連付けて利用するなどより複雑な使い方も可能です。

AWS AppConfig 機能フラグと動的設定により、ソフトウェアビルダーはフルコードのデプロイなしで本番環境のアプリケーション動作を迅速かつ安全に調整できます。 AWS AppConfig はソフトウェアリリース頻度を高速化し、アプリケーションの耐障害性を向上させ、緊急の問題をより迅速に対処できます。 機能フラグを使用すると、新しい機能をすべてのユーザーに完全にデプロイする前に、徐々にユーザーにリリースし、それらの変更の影響を測定できます。運用フラグと動的設定を使用すると、ブロックリスト、許可リスト、スロットリング制限、ロギングの冗長性を更新したり、その他の運用上の調整を行うことで、実稼働環境の問題に迅速に対応できます。

AWS AppConfig とは

App Config の使い方については公式ドキュメントを参考にしてみてください。

本対応では、AppConfig で handler に同時実行フラグを差し込み、既存のユースケースと並行して新ユースケースを非同期で実行しました。そして、両方のユースケースから出力されるログの差分を一定期間にわたって監視・検証しました。

同時実行 AppConfig 設定例)

{
  "enable_concurrency_execution_flag": {
    "rate": 1.0
  }
}

AppConfig による同時実行フラグ差し込みイメージ)

差分検知ログは datadog 上にダッシュボードを追加し以下のように diff を確認できるようにしました。

// 差分例
      ...
      "key": map[string]any{
-         "value": string("or"),
+         "value": string("and"),
      },
      ...

これにより、新ユースケースの動作検証、データ差分のチェック、問題の早期発見が可能になりました。

3. 動的な切り替え機能を利用した段階的な移行

さらにユースケース移行を安全に実施するため、同時実行パターンによる差分検証後は、動的な切り替え機能を利用した段階的な移行を行いました。

通常の開発サイクルで進めると新旧切り替えの度にデプロイが必要となるため以下のような課題が考えられます。

  • デプロイ時のリスクが発生する
  • ロールバックに時間がかかる
  • 段階的切り替えに時間と手間がかかる

※ 通常デプロイ時は Blue/Green Deployment を採用しています。

AWS ECS で実現するBlue/Green Deployment:運用を見据えたCDK実装例

これらの課題についても AppConfig を活用した動的な切り替え機能を利用し対応しました。

新旧ユースケース切替 AppConfig 設定例)

{
  "use_experimental_usecase_flag": {
    "matchers": [
      {
        "actions": ["特定のユースケース処理に絞る"],
        "clients": ["特定のクライアントに絞る"],
        "rate": 1.0
      }
    ]
  }
}

AppConfig による新旧ユースケース切替イメージ)

デプロイなしの即時切り替えや、問題発生時の即時ロールバックが可能な状態で、特定のユースケース処理やクライアントに絞りながら切り替えを行う段階的な移行ができたことで、より安全かつ効率的に新系への移行が実施できました。

まとめ

今回のユースケースリニューアルでは、ユニットテスト・インテグレーションテストを用いた差分検証、同時実行パターンによる差分検証、動的な切り替え機能を利用した段階的な移行という3つのアプローチを組み合わせることで、安全かつ効率的な移行を実現しました。また、AWS AppConfig を活用した動的な機能切り替えにより、デプロイを伴わない柔軟な移行と、問題発生時の即時ロールバックが可能となり、安全かつ迅速な移行が実現できました。

テックタッチでは、新機能を開発しながら他にも様々な改善を行なっているので以下の記事も参考にしてみてください。