Spring Framework

프로젝트에 Feature Flag 적용하기

Junuuu 2024. 6. 1. 15:40

Feature Flag란 무엇인가? (Feature Toggle)

Feature Flag란 특정 기능을 동적으로 활성화 혹은 비활성화하기 위해 사용되는 조건부 코드 실행 메커니즘입니다.

런타임 환경에서 특정 조건에 따라 코드 특정 부분을 스위치 하여 실제 사용자에게 제공되는 서비스 기능을 다르게 제어할 수 있습니다.

특히 이러한 제어를 위해서 매번 코드를 수정해서 배포할 필요가 없다는 특징이 있습니다.

 

https://buildd.co/marketing/feature-toggles

 

Feature Toggle이라고도 불리며 기능을 ON / OFF 할 수 있는 게 특징입니다.

 

매우 다양한 활용도

특정 기능을 ON / OFF 할 수 있는 만큼 매우 다양하게 활용해 볼 수 있습니다.

 

예를 들면 아래와 같은 활용을 수행해 볼 수 있습니다.

  • 서킷브레이커 ON/OFF
  • A/B 테스트
  • 카나리 배포
  • TrunkedBase Deployment에서의 독립적인 배포

 

프로젝트에 도입한 이유

외부환경을 위한 테스트환경을 구축하거나 점검기능을 도입할 수 있습니다.

 

예를 들어 간단하게 오전 9시부터 오후 3시까지만 동작할 수 있는 기능이 있다고 가정해 보겠습니다.

QA에서 해당 기능을 테스트하기 위해서는 오전 9시부터 오후 3시까지는 정상적으로 동작하는 기능을 테스트하고 이외의 시간에는 동작이 제대로 하지 않는다는 메시지를 보고 싶을 수 있습니다.

 

이때 오후 5시에 정상적으로 동작하는 기능을 테스트하고 싶다면 어떻게 해야 할까요?

테스트가능한 시간을 제거하거나, 오후 6시로 늘리기 위해 배포를 수행해야 합니다.

 

하지만 Feature Flag을 적용한다면 해당기능을 ON / OFF로 테스트해 볼 수 있으며 유사하게 점검기능도 손쉽게 도입하고 테스트해볼 수 있습니다.

 

 

단점

토글을 사용하면 장점도 많지만 단점도 존재합니다.

토글이 많아지면 자연스럽게 관리하기 어려워집니다.

이에 대안으로 토글에 유효기간을 두고 유효기간이 지난 경우 애플리케이션의 구동 혹은 테스트에 실패하도록 구성할 수 있습니다.

혹은 토글의 개수에 제한을 두어 신규로 토글을 구현하려면 과거의 토글 하나를 제거하는 전략을 취할 수도 있습니다.

 

 

Database를 활용한 Feature Toggle 구현 예제

interface FeatureFlagManager {
    fun setFeature(keyName: String, isEnabled : Boolean)
    fun isEnabled(keyName: String): Boolean
}

토글을 관리하고 활용하기 위해서는 2가지 메서드가 필요합니다.

Feature를 등록할 수 있는 setFeature 메서드, 해당 Feature가 활성화되어 있는지 확인할 수 있는 메서드입니다.

 

@Component
class H2FeatureFlagManager(
        private val featureFlagRepository: FeatureFlagRepository,
): FeatureFlagManager {
    override fun setFeature(keyName: String, isEnabled: Boolean) {
        val featureFlag = featureFlagRepository.findByKeyName(keyName)
        if(featureFlag == null){
            featureFlagRepository.save(
                    FeatureFlag(
                            keyName = keyName,
                            enabled = isEnabled
                    ))
            return
        }
        featureFlag.enabled = isEnabled
        featureFlagRepository.save(featureFlag)
    }

    override fun isEnabled(keyName: String): Boolean {
        return featureFlagRepository.findByKeyName(keyName)?.enabled
                ?: return false
    }
}

FeatureFlagManager interface를 구현하는 H2 FeatureFlagManager 클래스를 구현하였습니다.

해당 클래스는 H2 Database 기반으로 Feature Toggle을 관리하며 MySQL, Redis 등 Feature를 저장할 수 있는 어떤 것으로 구현해도 상관없습니다.

 

@Entity
@Table(name = "feature_flag")
data class FeatureFlag(
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        val id: Long = 0,
        @Column
        val keyName: String,
        @Column
        var enabled: Boolean,
){
        companion object{
                const val TEST_TOGGLE = "TEST_TOGGLE"
        }
}


interface FeatureFlagRepository: JpaRepository<FeatureFlag, Long> {
    fun findByKeyName(keyName: String): FeatureFlag?
}

실제로 Toggle을 DB에 적재하기 위해서는 JPA를 활용하였습니다.

실제 Toggle을 enum이나 상수 등으로 관리하게 되면 어떤 토글이 존재하는지도 한 번에 알아볼 수 있을 것 같습니다.

 

fun interface ExternalApiCaller {
    fun invoke(): Long

    @Component
    class Default: ExternalApiCaller {
        override fun invoke(): Long {
            // 외부 API 호출
            return 1
        }
    }

    @Component
    @Primary
    @Profile("local")
    class Dummy(
            private val featureFlagManager: FeatureFlagManager,
            private val default: Default,
    ): ExternalApiCaller {
        override fun invoke(): Long {
            if(featureFlagManager.isEnabled(TEST_TOGGLE)){
                return 0
            }
            return default.invoke()
        }
    }
}

이제 외부 API를 호출하는 특정 클래스를 두고 특정환경(예: Local)에서는 토글의 ON / OFF에 따라 실제 외부 API를 호출하는 동작을 수행할 수도 있으며 0이라는 Dummy 값을 제공할 수 있습니다.

 

@RestController
class FeatureFlagAdminController(
        private val h2FeatureFlagManager: H2FeatureFlagManager,
        private val externalApiCaller: ExternalApiCaller,
) {

    @PostMapping("/feature-flag/keys/{keyName}/enabled/{isEnabled}")
    fun setKey(
            @PathVariable keyName: String,
            @PathVariable isEnabled: Boolean,
    ){
        h2FeatureFlagManager.setFeature(keyName = keyName, isEnabled = isEnabled)
    }

    @GetMapping("/feature-flag/keys/{keyName}")
    fun getKeyEnabled(
            @PathVariable keyName: String,
    ) = h2FeatureFlagManager.isEnabled(keyName = keyName)

    @GetMapping("/test")
    fun test(){
        println(externalApiCaller.invoke())
    }
}

Feature를 운영 및 관리하기 위해서는 외부에서 Feature를 등록하고 Toggle을 ON / OFF 할 수 있어야 합니다.

이제 AdminController를 통하여 제어할 수 있게 됩니다.

테스트를 위해 /test 경로를 호출해 본다면 TEST_TOGGLE의 ON / OFF 여부에 따라 1 또는 0이 반환되게 됩니다.

 

테스트 코드로 너무많은 Feature 개수 제한하기

class FeatureFlagTest{

    @Test
    fun `Feature Toggle의 개수는 10개 이하여야 한다`(){
        val toggleCount = FeatureFlag.Companion::class.java.declaredFields.size
        require(toggleCount < 10)
    }
}

Companion Class의 정의된 변수의 개수 혹은 Enum의 Size등을 통하여 Feature 개수를 적절히 세팅하여 제한할 수 있습니다.

 

 

 

참고자료

https://martinfowler.com/articles/feature-toggles.html

https://11st-tech.github.io/2023/11/07/openfeature/

https://buildd.co/marketing/feature-toggles