Remote config 적용기

0koang·2024년 1월 4일
1

ETC

목록 보기
1/6
post-thumbnail

적용 이유

강업 공지때문에 주말 출근하기 싫어서





Remote config ?

원격 구성. 앱을 업데이트를 하지 않아도 하루 활성 사용자 수 제한 없이 앱의 동작과 모양을 변경할 수 있음

  • 변경사항에 빠르게 적용할 수 있음
  • A/B 테스트
  • 비율대로 새 기능 출시
  • 플랫폼 및 언어별 프로모션 배너 정의
  • json을 사용한 복잡한 앱 항목 구성




Remote config 생성





Remote config data & manager

  • RemoteConfigData
enum class RemoteConfigData(val key: String, val defaultValue: Any) {

	// 테스트
    HELLO_BOOLEAN(
        key = "hello_boolean",
        defaultValue = false),
    HELLO_STRING(
        key = "hello_string",
        defaultValue = "test string 123"),
    HELLO_LONG(
        key = "hello_long",
        defaultValue = 0L),
    HELLO_DOUBLE(
        key = "hello_double",
        defaultValue = 0.0),

    // 강제 업데이트
	FORCE_UPDATE(
      key = "force_update",
      defaultValue = JSONObject().apply{
          put("real", JSONObject().apply{
              put("isForceUpdate", false)
              put("forceUpdateVersion", "0.0.0")
              put("noticeMsg", "")
              put("marketUrl", "market://details?id=앱패키지")
          })
          put("dev", JSONObject().apply{
              put("isForceUpdate", false)
              put("forceUpdateVersion", "0.0.0")
              put("noticeMsg", "")
              put("marketUrl", "market://details?id=앱패키지")
          })
      }.toString());

    companion object {
        private fun getJsonStr(config: RemoteConfigData) = RemoteConfigManager.getValue(config)
        private fun getServer() = if (isReal()) "real" else "dev"

        // 강제 업데이트
        private fun getForceUpdate() = JSONObject(getJsonStr(FORCE_UPDATE)).optJSONObject(getServer())
        fun isForceUpdate() = getForceUpdate()?.optBoolean("isForceUpdate")
        fun getForceUpdateVersion() = getForceUpdate()?.optString("forceUpdateVersion")
        fun getNoticeMsg() = getForceUpdate()?.optString("noticeMsg")
        fun getMarketUrl() = getForceUpdate()?.optString("marketUrl")
    }

}
  • RemoteConfigManager
object RemoteConfigManager {

    private val TAG = this::class.simpleName
    private const val FETCH_INTERVAL = 60*60L

    private var mIsCompletedRemoteConfig    = false     // remote config 잘 받아왔는지

    private var mOnReadyListener: (() -> Unit)? = null      // remote config 준비 완료 리스너

    fun init() {
        // 셋팅
        // 앱에서 단기간에 가져오기를 너무 많이 수행하면 가져오기 호출이 제한되고 SDK는 FirebaseRemoteConfigFetchThrottledException을 반환합니다.
        // SDK 버전 17.0.0 이전에는 60분 동안 가져오기 요청 수가 5회로 제한되었지만 최신 버전에서는 좀 더 많이 허용됩니다.
        Firebase.remoteConfig.setConfigSettingsAsync(
        	remoteConfigSettings{
  				// 개발할 땐 바로 fetch하기 위해 1로 셋팅
                minimumFetchIntervalInSeconds = if(BuildConfig.DEBUG) 1 else FETCH_INTERVAL
			}
		)

        // 인앱 매개변수 기본값 설정
        Firebase.remoteConfig.setDefaultsAsync(mutableMapOf<String, Any>().apply {
            RemoteConfigData.values().forEach{ remoteConfigConst ->
                put(remoteConfigConst.key, remoteConfigConst.defaultValue)
            }
        }).addOnCompleteListener {
        }

		// 값 가져오기 및 활성화
        Firebase.remoteConfig.fetchAndActivate()
            .addOnCompleteListener { task->
            	if (task.isSuccessful) {
                    val updated = task.result
                    Timber.tag(TAG).d("Config params updated: $updated")
                    Timber.tag(TAG).d("Fetch and activate succeeded")
                } else {
                    Timber.tag(TAG).d("Fetch failed")
                }

				mIsCompletedRemoteConfig = true
                mOnReadyListener?.invoke()
            }
    }

	/**
     * remote config 잘 받아온 상태이면 바로 block 실행
     * 아직 remote config 받아오기 전이라면 onReadyListener 셋팅만 하고, 실제 remote config 받기 완료했을 때 onReadyListener 실행
     */
    fun onReady(block: () -> Unit) {
        if(!mIsCompletedRemoteConfig) {
            mOnReadyListener = block
            return
        }

        block.invoke()
    }

	fun getValue(config: RemoteConfigData): Any {
        // remote config 다 받아오기도 전이라면 기본값 넘기기
        if(!mIsCompletedRemoteConfig) {
            return config.defaultValue
        }

        return when(config.defaultValue) {
        	is Boolean -> Firebase.remoteConfig.getBoolean(config.key)
            is String -> Firebase.remoteConfig.getString(config.key)
            is Long -> Firebase.remoteConfig.getLong(config.key)
            is Double -> Firebase.remoteConfig.getDouble(config.key)
            else -> throw IllegalArgumentException("unused type")
        }
    }

}





Remote config 값 테스트

@Test
fun testRemoteConfig() = runBlocking {

	RemoteConfigManager.onReady {
        val booleanValue = RemoteConfigManager.getValue(RemoteConfigData.HELLO_BOOLEAN)
        val stringValue = RemoteConfigManager.getValue(RemoteConfigData.HELLO_STRING)
        val longValue = RemoteConfigManager.getValue(RemoteConfigData.HELLO_LONG)
        val doubleValue = RemoteConfigManager.getValue(RemoteConfigData.HELLO_DOUBLE)

        println("booleanValue : $booleanValue")
        println("stringValue : $stringValue")
        println("longValue : $longValue")
        println("doubleValue : $doubleValue")

        assertThat(booleanValue).isNotNull()
        assertThat(stringValue).isNotNull()
        assertThat(longValue).isNotNull()
        assertThat(doubleValue).isNotNull()

        val forceUpdate = RemoteConfigData.isForceUpdate()
        val forceUpdateVersion = RemoteConfigData.getForceUpdateVersion()
        val noticeMsg = RemoteConfigData.getNoticeMsg()
        val marketUrl = RemoteConfigData.getMarketUrl()

        println("forceUpdate : $forceUpdate")
        println("forceUpdateVersion : $forceUpdateVersion")
        println("noticeMsg : $noticeMsg")
        println("marketUrl : $marketUrl")

        assertThat(forceUpdate).isNotNull()
        assertThat(forceUpdateVersion).isNotNull()
        assertThat(noticeMsg).isNotNull()
        assertThat(marketUrl).isNotNull()
    }

	delay(1000L)

}





강제 업데이트 적용

  • application 클래스 소스 일부
// init remote config
RemoteConfigManager.init()
  • intro splash 소스 일부

asis

when(공지) {
	is 강업 -> { 
    	강업체크 { 일반공지체크() }
    }
	else -> 일반공지체크()
}

tobe

when(공지) {
	is 강업 -> { 
		강업체크 { 일반공지체크() }
	}
    else -> {
    	// remote config 강제 업데이트 체크
        RemoteConfigManager.onReady {
        	if (!isRemoteConfig강업()) {
            	일반공지체크()
                return@onReady
            }

            remoteConfig강업데이터셋팅()

			강업체크 { 일반공지체크() }
        }
    }
}





문제점

  • 강제 업데이트 시간차 발생 (실시간으로 값을 동기화해서 가져오지 않음. 하지만 1시간 내외일 것)
profile
서비스 핵심 가치를 이해하고, 지속적인 개선을 이끄는 엔지니어(를 지향함)

0개의 댓글