티스토리 뷰

실제 업무를 하다보면, Build flavour와 Build type에 따라 각각 다른 endpoint에 접속할 경우가 많다. 예를 들면, 개발시에는 개발서버나 테스트 서버에 있는 API를 사용을 하고 릴리스 후에는 프로덕션에 있는 API를 사용할 필요가 생긴다. 또는 개발시에는 Base URL을 세팅에서 바꿀 수 있는 옵션을 제공할 때도 있다. 어떻게 하면 좀 더 깔끔하게 빌드환경에 따른 Base URl 관리를 할 수 있을까?

 

여러가지 다양한 구현 방법이 존재하겠지만, Bridge Pattern이 이런 경우에 적합할 것 같아 보인다.

 

dev와 prod flavour가 있고, Dev, UAT, PROD 환경이 있다고 하자.  그렇다면 다음과 같은 형태로 클래스를 설계해 볼 수 있을 것이다.

Bridge Pattern - Base URL 관리

 

Flavour는 인터페이스로 파라미터로 들어오는 URL이 허용이 되는지 체크를 한다. 각각의 flavour에 해당하는 구현을 해주면 되겠다. 

ServerEnv는 추상 클래스로 개발환경에 사용되는 BaseUrl을 가지고 있고 connect()를 할 수 있다. 마찬가지로 각각의 개발환경에 해당하는 서브 클래스를 구현해 준다.

 

이제 구현된 코드를 보자.

 

Flavour 구현:

interface Flavour {
    val name: String
    fun accept(url: String): Boolean
}

class Develop(override val name: String = "Develop build") : Flavour {
    override fun accept(url: String): Boolean {
        return url.startsWith("http://dev.server.com") || url.startsWith("http://uat.server.com")
    }
}

class Production(override val name: String = "Production build") : Flavour {
    override fun accept(url: String): Boolean {
        return url.startsWith("http://prod.server.com")
    }
}

코드에서 보듯이 Develop flavor에서는 Prod에는 접속할 수 없지만 Dev와 Uat는 허용해 주고 있다. 실제의 use case와 매우 흡사하다.

이렇게 다른 URL에 대한 접근 허용을 분리함으로써, 잘못된 접속을 방지할 수 있고 나중에 수정할 일이 생기면 어디를 고쳐야 하는지 금방 알 수 있다.

 

 

ServerEnv  구현이다.

abstract class ServerEnv {
    protected val connector = Connector()

    abstract val flavour: Flavour
    abstract fun baseUrl(): String
    abstract fun connect(url: String)
}

class DevEnv(
    override val flavour: Flavour
): ServerEnv() {

    override fun baseUrl(): String {
        return "http://dev.server.com/"
    }

    override fun connect(url: String) {
        if (flavour.accept(url) && url.startsWith(baseUrl())) {
            connector.connect(url)
            println("Connected to ${flavour.name} Dev environment.")
            return
        }
        throw IllegalArgumentException("$url is not allowed to connect Dev.")
    }
}

class UatEnv(
    override val flavour: Flavour
): ServerEnv() {

    override fun baseUrl(): String {
        return "http://uat.server.com/"
    }

    override fun connect(url: String) {
        if (flavour.accept(url) && url.startsWith(baseUrl())) {
            connector.connect(url)
            println("Connected to ${flavour.name} Uat environment.")
            return
        }
        throw IllegalArgumentException("$url is not allowed to connect Uat.")
    }
}

class ProdEnv(
    override val flavour: Flavour
): ServerEnv() {

    override fun baseUrl(): String {
        return "http://prod.server.com/"
    }

    override fun connect(url: String) {
        if (flavour.accept(url) && url.startsWith(baseUrl())) {
            connector.connect(url)
            println("Connected to ${flavour.name} Prod environment.")
            return
        }
        throw IllegalArgumentException("$url is not allowed to connect Prod.")
    }
}

ServerEnv의 구현에서는 각 환경은 해당 도메인만 허용하고 있다. 그리고 flavor에 대한 체크도 같이 하기 때문에 DevEnv와 UatEnv는 Develop flavor에서만 동작을 하게 된다. 만약 stage와 같은 서버 환경이 더 늘어난다면 ServerEnv를 상속한 클래스를 구현해주면 된다.

 

여기서 잠깐 한가지 더 생각해 보자. 실제 안드로이드 앱에서는 Gradle의 세팅에 따라 BuildConfig 클래스에 flavour가 자동으로 설정되게 된다. 따라서 위의 구현이 실제 앱에서 쓸모가 있으려면 BuildConfig에서 제공하는 flavour를 가지고 어떤 Flavour 상속 클래스를 사용하게 할 것인지 결정해야 한다. 여기에는 여러가지 방법이 쓰일 수 있겠지만, 간편하게 enum을 이용한 factory를 사용하겠다.

 

interface FlavorFactory {
    fun env(): Flavour
}

enum class FlavourEnum: FlavorFactory {
    DEV {
        override fun flavour(): Flavour {
            return Develop()
        }
    },
    PROD {
        override fun flavour(): Flavour {
            return Production()
        }
    },
}

이제 BuildConfig.flavour를 이용해 FlavourEnum에서 해당 팩토리 함수를 찾으면 된다.

 

val flavour = FlavourEnum.values().find { it.name.equals(BuildConfig.flavour, true) }!!.env()

이제 다음과 같이 테스트 코드를 만들고 확인해 보면 된다.

val buildFlavour = "dev"

val flavour = FlavourEnum.values().find { it.name.equals(buildFlavour, true) }!!.flavour()
val devEnv: ServerEnv = DevEnv(flavour)

devEnv.connect("http://dev.server.com/service1")
devEnv.connect("http://uat.server.com/service1")
devEnv.connect("http://prod.server.com/service1")

 

아마도 대부분을 Retrofit과 OkHttp를 이용하여 API를 호출할 것이다. 이 경우에는 ServerEnv에 connect은 굳이 필요없을 것이다. 대신 

Interceptor로부터 BaseUrlInterceptor를 구현하여 BaseUrlInterceptor에서 ServerEnv의 BaseUrl을 가져와서 사용하면 될 것이다.

 

class BaseUrlInterceptor(
    private val env: ServerEnv
): Interceptor {

    override fun intercept(chain: Chain): Response {
        val request = chain.request();
        val newUrl = request.url.newBuilder()
            .host(env.baseUrl())
            .build();

        return chain.proceed(request);
    }
    
}


// Add interceptor
var interceptor = BaseUrlInterceptor()


var okHttpClient: OkHttpClient = OkHttpClient.Builder()
          .addInterceptor(interceptor)
          .build()

 

 

만약 baseUrl이 환경설정 같은 데에 저장되어 사용되어야 하는 경우라면 아랫처럼 환경설정을 가지고 있는 클래스를 inject하여 사용하면 될 것이다.

abstract class ServerEnv (
    private val appConfig: AppConfig
) {
    abstract val flavour: Flavour
    fun baseUrl(): String {
       return appConfig.baseUrl
    }
}

 

깔끔한 코드를 위해서 abstraction과  encapsulation은 언제나 고려되어야 할 중요한 원칙 중의 하나인 것 같다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함