SmartHR Tech Blog

SmartHR 開発者ブログ

こんなフィーチャーフラグはイヤだ —— アンチパターンから考える消しやすいフィーチャーフラグの使い方

こんにちは!SmartHR で勤怠管理機能の開発をしている @yoiwamoto です。

フィーチャーフラグ、便利ですよね。一番好きなラグです。

フィーチャーフラグというのは、DevOps などの文脈で用いられるプラクティスの一つで、機能の有効・無効を制御する仕組みです。
一般的にはデプロイメントとリリースを切り離すことや、トランクベース開発を可能にすることに目的が置かれています。

勤怠管理機能の開発チームでは、フィーチャーフラグを利用した開発を行っており、一定規模以上の機能はフィーチャーフラグでユーザーが利用できないように制御しながら、機能内の個別の開発アイテムを main ブランチにこまめにマージおよびデプロイしています。
私たちのチームは、3 つのサブチームで構成される LeSS 体制で複数の機能を並行開発していることもあり、スムーズな開発のために、この仕組みはなくてはならないものになっています。
(調べたところ、導入からの約 1 年で 47 個の機能の開発にフィーチャーフラグを利用していました!)

この記事では、これまでの運用で学んだ、快適にフィーチャーフラグを運用するためのちょっとしたコツなどを、アンチパターンを例に紹介します。

目次

SmartHR 勤怠管理機能におけるフィーチャーフラグ

まずは前提として、SmartHR 勤怠管理におけるフィーチャーフラグの運用について簡単に触れます。

私たちのチームでは、このフィーチャーフラグをミニマムに実装・運用しています。
具体的には、利用テナントごとに機能の有効・無効が制御できるだけの仕組みとして利用しています。

フラグの識別子はコードベースに静的に定義されていて、有効化状況は DB に永続化されています。
大枠の実装は以下のようなイメージです。

# モデル実装
class FeatureFlagSetting < ApplicationRecord
  belongs_to :tenant

  FEATURE_FLAGS = [
    "gh_1234_slack_punch",       # Slack 打刻 #1234
    "gh_5678_36agreement_alert", # 36協定のアラート #5678
    ...,
  ].freeze

  def self.enabled?(feature_flag_name)
    self.exists?(feature_flag_name:, enabled: true)
  end
end
# バックエンド (Ruby) での利用イメージ
enabled = FeatureFlagSetting.enabled?("gh_1234_slack_punch")
// フロントエンド (js) での利用イメージ(API 経由で利用する)
const enabled = useFeatureFlag('gh_1234_slack_punch');

フィーチャーフラグの枠組みの中で、段階的ロールアウト・カナリアリリース・ユーザー属性やセグメントに基づく A/B テストなどの高度なユースケースがカバーされることもありますが、現時点ではこれらは採用していません。

フィーチャーフラグによる分岐の性質

フラグという名前の通り、フィーチャーフラグの実体はただの Boolean であり、これを使った分岐実装も根本的にただの if 文です。
フィーチャーフラグによる分岐に特有の性質は、あくまで分岐が一時的なものであり、いずれ必ず削除されるという点です。 逆に言うと、フィーチャーフラグの使い方のコツはほとんど、「いかに消しやすい分岐にするか」とも言い換えられます。

こんなフィーチャーフラグはイヤだ

今回は主にこの「いかに消しやすい分岐にするか」の観点から、いくつかのアンチパターンを挙げてみます。

どう消していいか分からない / 迷う

フィーチャーフラグを削除する際に、コードを読んでも意図が分からず、どう修正すればよいか迷うパターンがあります。

例えば、分岐箇所を grep して順番に削除している最中にこんなコードを見つけたら、どうでしょう?

result = articles.select { _1.draft? || !feature_enabled }

一瞬「なんて?」と思考が停止します。
(ちなみにこのコードから分岐を削除すると、result = articles.select(&:draft?) になるでしょう)

この例はある程度シンプルですし、頭の回転が速い方なら特に苦にも感じないかもしれません。
しかし、実際のコードベースでは、より複雑な分岐が複数箇所に散らばっていることもよくあり、これを削除していくのは結構大変です。各箇所で「この分岐は有効時の挙動なのか、無効時の挙動なのか」を判断し、適切にコードを修正する必要があるためです。
最近だと単純作業は AI エージェントがこなすことが多いと思いますが、とはいえ複雑なロジックになればなるほど安全性は下がりますし、レビューのコストは無視できません。

先ほどのコードであれば、以下のように、if-else で有効時・無効時それぞれの分岐を明示することで、意図が明確になると思います。

result = if feature_enabled
           articles.select(&:draft?)
         else
           articles.all
         end

この書き方であれば、フィーチャーフラグを削除する際は、feature_enabledtrue の場合の分岐(articles.select(&:draft?))を残せばよいことが一目で分かります。

大抵はこのようにシンプルな分岐に落とし込めるはずですが、場合によっては、パフォーマンスや前後のコードとの一貫性などを優先して、分かりにくい分岐を書かざるを得ないこともあるかと思います。
その場合、TODO コメントなどで消し方についてメモを残しておくと迷わずに削除が可能です。

複数のレイヤを跨いで分岐している

システム上の複数のレイヤ・コンポーネントを跨ぐようなフィーチャーフラグ分岐も、扱いづらいパターンの一つです。

例えば、フロントエンド、バックエンド、非同期処理ワーカー、マイクロサービスなどに分岐が散在している状態は、単純に実装量やテストケースが増えるだけでなく、「フィーチャーフラグが有効な時、どのようなデータがやり取りされて、結局どういう結果になるのか」という全体の挙動の見通しが悪くなる可能性が高いです。
また、デプロイタイミングによるズレや、既に実行中の非同期処理のジョブが、DB のフィーチャーフラグを更新した後にどう振る舞うかなどの挙動の追跡も困難になります。

個人的には、フィーチャーフラグはなるべく UI の表示・非表示制御で完結できるのが理想的だと思っています。
削除も簡単ですし、有効時・無効時それぞれの挙動が誰から見ても明らかです。

バックエンドでの制御が必要な場合でも、逆にフロントエンドの分岐が不要になるということもあります。 例えば、フィーチャーフラグが有効な時のみ見せたいデータについて、フィーチャーフラグが無効な時にバックエンドのレスポンスに含まれないのであれば、フロントエンド側での分岐は必要ありません。
ロジックがバックエンドに寄っているアプリケーションであれば、このパターンの方が多いかもしれません。

分岐が細かい / 多い

特定の UI の中で、5、6 箇所にわたって変更が入ることは普通にあります。
レイヤを跨いだ分岐ほどではなくても、分岐が分散して実装されると、その機能の全体像はやはり見えづらくなります。

分岐箇所が多い場合は、実装の大元で分岐してしまうことを検討するのが良いと思います。
例えば、ホーム画面の UI がところどころ、5、6 箇所ほど変わるような変更の場合、<HomePage> 内の大量のコンポーネントそれぞれに分岐を入れるのではなく、<HomePage><NewHomePage> のように大きく分岐します。

ちなみにこのようなケースでは、以下のような順序で開発を行うと、フィーチャーフラグの分岐追加時・削除時の両方で安全にレビューできます。

  1. 複製と分岐実装のみを適用し、commit 現在の実装を関連コンポーネントごと複製し、片方を LegacyXxx のようにリネーム。
    その後、これらのコンポーネントをフィーチャーフラグで出し分けます。

    GitHub のコミット詳細画面。「chore: HomePage の実装を複製し、フィーチャーフラグで分岐」というコミットについて、ファイル差分が表示されている。legacy-home-page というファイル群が新規追加されている。App コンポーネントでは、HomePage と LegacyHomePage をフィーチャーフラグによって出し分ける分岐が実装されている。
    1 の commit のレビュー時のイメージ

  2. もとの実装に、変更を適用する 個別の分岐をせずに変更を適用します。
    細かい分岐ではないので、ファイル削除や構造の変更など、新しい仕様に最適な実装への設計変更なども簡単に適用できます。
    また、現在本番で動いているコードをほぼ触らずに済むので、デグレが生じるリスクも限定的です。

    GitHub のコミット詳細画面のスクリーンショット。「feat: ホーム画面の新しい UI を実装」というタイトルで、通知表示の削除などの変更が、分岐なしで適用されている。
    2 の commit のレビュー時のイメージ

  3. 旧実装とフィーチャーフラグ分岐を削除する リリース時は、旧実装とフィーチャーフラグの分岐を削除するだけで OK です。
    旧実装の方を LegacyHomePage とリネームしているので、新しい実装を NewHomePageHomPage のように変更する差分も生じず、レビュー時の差分が非常にクリアになります。

    GitHub のコミット詳細画面のスクリーンショット。「feat: ホーム画面の UI 刷新をリリース」というコミットで、legacy-home-page ディレクトリのファイルが削除され、フィーチャーフラグによるコンポーネントの出し分けの分岐が削除されています
    3 のcommit のレビュー時のイメージ

やや具体的すぎるかもしれませんが、大きな UI 変更を実施する際には参考にしてみてください。

秘匿情報がユーザーに露出している

フィーチャーフラグによる分岐をブラウザ上でも実装する場合、結局はクライアントサイドのバンドルにコードが乗るわけなので、開発中の機能の存在を厳密な意味でユーザーから隠し通すことは難しいです。
もちろん、誰にでも開発中の機能一覧が分かるような状態は避けると思いますが、JavaScript として配信されている以上は、知識のある人がソースを読み解けば、どんな機能が開発中であるかあたりを付けられます。

これの対策として、RSC (React Server Components) などの BFF (Backend For Frontend) 側で分岐を行う、フラグの識別子を難読化した上で関連モジュールは dynamic import するなどの方法も考えられますが、最終的には、「最悪バレても問題ない」スタンスを意識するのが最も安全かと思います。
(開発期間にもよりますが、直近開発している機能が顧客に知られてただちに困る、というプロダクトはそうそうない気もします)

副作用のある分岐をしている

フィーチャーフラグによって、データベースへの永続化など副作用を伴う処理に分岐を実装するのは、避けた方が良いケースが多いです。

例えば、あるデータの持ち方を旧スキーマから新スキーマに切り替える際、フィーチャーフラグの有効・無効に応じて保存するスキーマを変えてしまうと問題が生じます。
リリースで一度フラグを有効にした後、不具合切り戻しで無効に戻したり、あるいは特定のテナントに限定公開してから、一度無効に戻したりといった操作を行うと、異なるスキーマのデータが混在し、アプリケーションが壊れる可能性があります。

データ構造の変更が伴うような場合は、内部では両方のスキーマに互換性を持たせた状態で、表面上の挙動のみをフィーチャーフラグで切り替えるといった工夫が必要です。
が、多くの場合このような対応は複雑度が高まりますし、フィーチャーフラグの恩恵がその複雑度に見合うとは限りません。

このようなケースでは、フィーチャーフラグを使用せずに、フィーチャーブランチで開発を行うことも検討するのが良いかもしれません。

最高のフィーチャーフラグ

これまでのアンチパターンを踏まえると、快適にフィーチャーフラグを運用するためのポイントは以下のようになります。

  • 削除の仕方が一目で分かる分岐にする
  • 分岐箇所をなるべく一箇所に集約する
  • 細かい分岐を避け、大元で分岐する
  • ブラウザ上で分岐するなら、秘匿情報は含めない
  • 副作用を伴う処理への分岐は慎重に検討する

これらのポイントは、一つ一つは当たり前に見えても、意外に普段の設計やレビューのタイミングで度々議論になります。
最近は Agent Skills の標準化も進んでいるので、この機会にチームでフィーチャーフラグの運用について認識を揃えて、ポリシーを SKILL.md にまとめてみるのはどうでしょうか?

それでは、Happy Feature Flagging! (?)

We are hiring!

SmartHR の勤怠管理機能では、バックエンドエンジニアを募集しています!

open.talentio.com

他の領域の採用も行なっているので、もし興味を持っていただけた場合は、こちらもご覧いただけると嬉しいです。

hello-world.smarthr.co.jp