From 6b34a8bab6c6ea1c248d75fa678f4fa63471c23f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EB=AF=B8?= Date: Tue, 11 Aug 2026 16:37:58 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B0=80=EB=B2=BC=EC=9A=B4=20=EC=9E=90?= =?UTF-8?q?=EC=B2=B4=20=EA=B5=AC=ED=98=84=20Feature=20Flag=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 트렁크 기반 개발에서 미완성/위험도 있는 기능을 브랜치에 오래 묵히지 않고 main에 먼저 merge한 뒤, 배포 이후 원하는 시점에 켤 수 있게 하기 위한 최소 구현. SaaS(GrowthBook 등) 대신 application.yml의 feature-flags.flags.{key}: true/false 값을 FeatureFlagProperties로 바인딩하고, FeatureFlags.isEnabled(key)로 조회한다. 정의되지 않은 키는 기본값 false(비활성)로 취급한다. --- README.md | 27 ++++++++++++++ .../featureflag/FeatureFlagProperties.java | 21 +++++++++++ .../config/featureflag/FeatureFlags.java | 20 +++++++++++ .../config/featureflag/FeatureFlagsTest.java | 35 +++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 src/main/java/org/runnect/server/config/featureflag/FeatureFlagProperties.java create mode 100644 src/main/java/org/runnect/server/config/featureflag/FeatureFlags.java create mode 100644 src/test/java/org/runnect/server/config/featureflag/FeatureFlagsTest.java 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(); + } +}