Spring Boot で Feature Flag を使う
Spring Profile 機能を用いて Feature Flag によるリリース管理を行います[1].
Feature Toggles (aka Feature Flags)
Spring Profile
Spring Profile 機能は, 設定した Profile に応じた Bean を使用することができる機能です[2]. Spring Boot ではインスタンスのDIコンテナへの登録が Spring 起動時の初回のみ行われる singleton
スコープがデフォルトのため, この機能を使った Release Toggle の実装のみを扱います.
エンドポイントの有効化
Spring Profile 機能を使って Controller の実装を切り替えることで, エンドポイントの Release Toggle を実現することができます. リリース予定日が決まっていたり, 内部実装前にリリースすることができるようになります.
まずはエンドポイントのインターフェースを作成します. インターフェースがなくても profile の切り替えは可能ですが, 複数の Controller クラスが同じエンドポイントであることを示すため, インターフェースを作成します.
public interface GreeterController {
ResponseEntity<String> greet();
}
次に, 内部実装を行った feat-greet
profile 用の Controller クラスと, 非公開のためのクラスとを作成します. ここで内部実装は簡単にHTTPステータスコード200と "Hello, World!" を返すだけのものとします. 非公開の場合にはHTTPステータスコード404を返すようにします.
GreeterControllerFeat.java
@Slf4j
@Profile("feat-greet")
@Controller
public class GreeterControllerFeat implements GreeterController {
@Override
@GetMapping("/greet")
public ResponseEntity<String> greet() {
log.info("Get greet feat-greet");
return ResponseEntity.ok("Hello, World!");
}
}
GreeterControllerProd.java
@Slf4j
@Profile("!feat-greet")
@Controller
public class GreeterControllerProd implements GreeterController {
@Override
@GetMapping("/greet")
public ResponseEntity<String> greet() {
log.info("Get greet production");
return ResponseEntity.notFound().build();
}
}
@Profile
アノテーションの値に !
を付けると, その profile 値が有効でない場合に Component が使用されます[3]. つまり application.properties
で feat-greet
が設定されていれば GreeterControllerFeat
が, そうでなければ GreeterControllerProd
が使用されます. これにより本番環境の application.properties
に変更を加えずに開発中の成果物をリリースすることができます.
開発環境の application.yml
.
spring:
profiles:
active: foo, bar, feat-greet # /greet が有効になる
本番環境の application.yml
.
spring:
profiles:
active: foo, bar
SpringBootTest での Profile の切り替え
確認のため, SpringBootTest を使ってテストを行います(実際にはより実際に即しており, 可搬性も高い Web API テストによって検証されるのが望ましいでしょう). SpringBootTest では @ActiveProfiles
によってテスト時の Profile を指定することができます. なお GreeterControllerProd
に対するテストでは feat-greet
profile 以外を指定する必要があるため, てきとうに prod
という profile を作成しています.
GreeterControllerFeatTest
@ActiveProfiles("feat-greet")
@AutoConfigureMockMvc
@SpringBootTest
public class GreeterControllerFeatTest {
@Test
public void testGreet(@Autowired MockMvc mockMvc) throws Exception {
MvcResult result = mockMvc.perform(get("/greet")).andExpect(status().isOk()).andReturn();
assertThat(result.getResponse().getContentAsString()).isEqualTo("Hello, World!");
}
}
GreeterControllerProdTest
@ActiveProfiles("prod")
@AutoConfigureMockMvc
@SpringBootTest
public class GreeterControllerProdTest {
@Test
public void testGreet(@Autowired MockMvc mockMvc) throws Exception {
mockMvc.perform(get("/greet")).andExpect(status().isNotFound());
}
}
機能の有効化
エンドポイントの有効化と同様に既存のエンドポイントに新たに機能を追加する場合や既存の機能を変更する場合にも利用できます. 今回は既存機能を変更する場合について考えてみます. 前述のエンドポイントの有効化の場合と同様に Service の実装を丸ごと切り替えることでも実現できますが, 今回は変更部分のみを Service クラスから切り出して, その部分のみを切り替えます. 切り替える部分は Bean を使って DI することで実現します.
例として TODO アプリケーションを考えます. GETリクエストに対して登録済みの TODO のリストを返すエンドポイントを作成します. TODOが期限日 deadline
と優先度 priority
のプロパティを持つとき, リストのソート順を変更することを考えます.
TODOのプロパティ
interface Todo {
"task": string,
"done": boolean,
"deadline": string,
"priority": number
}
まずは deadline
でソートする実装を行います.
TodoService
@AllArgsConstructor
@Service
public class TodoService {
private final TodoRepository todoRepository;
public List<Todo> getTodos() {
List<Todo> todos = todoRepository.selectTodos();
return todos.stream()
.sorted(Comparator.comparing(Todo::deadline))
.collect(Collectors.toList());
}
}
次に, ソート処理を別クラスに切り出して, DIできるようにします.
TodoService
@AllArgsConstructor
@Service
public class TodoService {
private final TodoRepository todoRepository;
private final SortTodo sortTodo;
public List<Todo> getTodos() {
List<Todo> todos = todoRepository.selectTodos();
return sortTodo.sort(todos);
}
}
SortTodoByDeadline
@Slf4j
public class SortTodoByDeadline implements SortTodo {
@Override
public List<Todo> sort(List<Todo> todos) {
log.info("Sorting todos by deadline");
return todos.stream()
.sorted(Comparator.comparing(Todo::deadline))
.collect(Collectors.toList());
}
}
優先度 priority
によるソート処理を行うクラスを作成します.
SortTodoByPriority
@Slf4j
public class SortTodoByPriority implements SortTodo {
@Override
public List<Todo> sort(List<Todo> todos) {
log.info("Sorting todos by priority");
return todos.stream()
.sorted(Comparator.comparing(Todo::priority))
.collect(Collectors.toList());
}
}
これらのクラスをそれぞれ Bean として登録し, Profile によって切り替えることで, Release Toggle を実装することができます. 優先度 priority
によるソート処理が feat-priority
profile で有効になるようにします. これにより Spring Component 以外の機能でも Feature Flag を実装することができます.
SortTodoBean
@Configuration
public class SortTodoBean {
private final SortTodo sortTodoByDeadline = new SortTodoByDeadline();
private final SortTodo sortTodoByPriority = new SortTodoByPriority();
@Profile("!feat-priority")
@Bean
public SortTodo sortTodoByDeadline() {
return sortTodoByDeadline;
}
@Profile("feat-priority")
@Bean
public SortTodo sortTodoByPriority() {
return sortTodoByPriority;
}
}
まとめ
Spring Profile 機能を使って Feature Flag によるリリース管理を行う方法を紹介しました. application.properties
に付与する profile によって, 静的な Feature Flag である Release Toggle を実現することができます.
-
Release Toggles | Feature Toggles (aka Feature Flags) | martinfowler.com ↩︎
-
正確な定義や詳細は公式ドキュメントや書籍を参考にしてください. 3. Profiles | Core Features | docs.spring.io, 『プロになるためのSpring入門――ゼロからの開発力養成講座』など. ↩︎
-
If a given profile is prefixed with the NOT operator (!), the annotated component will be registered if the profile is not active — for example, given @Profile({"p1", "!p2"}), registration will occur if profile 'p1' is active or if profile 'p2' is not active. Profile (Spring Framework 6.1.4 API) ↩︎
Discussion