diff --git a/README.md b/README.md index 6752d1f..7f353b8 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,33 @@ 과거에는 `dev`/`main` 두 개의 장수 branch를 cherry-pick으로 동기화하는 방식이었으나, 두 branch가 내용상 계속 어긋나는 문제(cherry-pick hell)가 반복되어 폐기했다. staging 배포도 `main` push를 기준으로 트리거된다. +### 🚩 Feature Flag + +미완성/위험도 있는 기능을 브랜치에 오래 묵히지 않고 `main`에 먼저 merge한 뒤, 배포 이후 원하는 시점에 켤 수 있게 하는 최소 구현이다. (`org.runnect.server.config.featureflag`) + +```yaml +# application.yml (environment별로 값이 다를 수 있음) +feature-flags: + flags: + new-run-summary: false +``` + +```java +@RequiredArgsConstructor +public class SomeService { + private final FeatureFlags featureFlags; + + public void doSomething() { + if (featureFlags.isEnabled("new-run-summary")) { + // 새 로직 + } + // 기존 로직 + } +} +``` + +yml에 키가 없으면 기본값은 비활성(`false`)이다. +

diff --git a/src/main/java/org/runnect/server/config/featureflag/FeatureFlagProperties.java b/src/main/java/org/runnect/server/config/featureflag/FeatureFlagProperties.java new file mode 100644 index 0000000..04583a8 --- /dev/null +++ b/src/main/java/org/runnect/server/config/featureflag/FeatureFlagProperties.java @@ -0,0 +1,21 @@ +package org.runnect.server.config.featureflag; + +import java.util.HashMap; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * application.yml의 feature-flags.flags.{key}: true/false 값을 그대로 바인딩한다. + * yml에 없는 키는 FeatureFlags에서 false(비활성)로 취급한다. + */ +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "feature-flags") +public class FeatureFlagProperties { + + private Map flags = new HashMap<>(); +} diff --git a/src/main/java/org/runnect/server/config/featureflag/FeatureFlags.java b/src/main/java/org/runnect/server/config/featureflag/FeatureFlags.java new file mode 100644 index 0000000..f395186 --- /dev/null +++ b/src/main/java/org/runnect/server/config/featureflag/FeatureFlags.java @@ -0,0 +1,20 @@ +package org.runnect.server.config.featureflag; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * 미완성/위험도가 있는 기능을 main에 먼저 merge하고 배포 이후에 켤 수 있게 하는 + * 트렁크 기반 개발용 최소 기능 플래그. 값은 environment별 application.yml의 + * feature-flags.flags.{key}로 관리한다 (예: feature-flags.flags.new-run-summary: true). + */ +@Component +@RequiredArgsConstructor +public class FeatureFlags { + + private final FeatureFlagProperties properties; + + public boolean isEnabled(String key) { + return properties.getFlags().getOrDefault(key, false); + } +} diff --git a/src/test/java/org/runnect/server/config/featureflag/FeatureFlagsTest.java b/src/test/java/org/runnect/server/config/featureflag/FeatureFlagsTest.java new file mode 100644 index 0000000..8e99ca8 --- /dev/null +++ b/src/test/java/org/runnect/server/config/featureflag/FeatureFlagsTest.java @@ -0,0 +1,35 @@ +package org.runnect.server.config.featureflag; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +class FeatureFlagsTest { + + @Test + void yml에_true로_설정된_플래그는_활성이다() { + FeatureFlagProperties properties = new FeatureFlagProperties(); + properties.setFlags(Map.of("new-run-summary", true)); + FeatureFlags featureFlags = new FeatureFlags(properties); + + assertThat(featureFlags.isEnabled("new-run-summary")).isTrue(); + } + + @Test + void yml에_false로_설정된_플래그는_비활성이다() { + FeatureFlagProperties properties = new FeatureFlagProperties(); + properties.setFlags(Map.of("new-run-summary", false)); + FeatureFlags featureFlags = new FeatureFlags(properties); + + assertThat(featureFlags.isEnabled("new-run-summary")).isFalse(); + } + + @Test + void yml에_정의되지_않은_플래그는_기본값으로_비활성이다() { + FeatureFlagProperties properties = new FeatureFlagProperties(); + FeatureFlags featureFlags = new FeatureFlags(properties); + + assertThat(featureFlags.isEnabled("존재하지-않는-플래그")).isFalse(); + } +}