(Android/Retrofit) Retrofit 동적 BaseUrl 변경 패턴


1. 왜 동적 BaseUrl이 필요한가?

Retrofit.Builder().baseUrl(...)은 한 번 설정하면 변경할 수 없습니다. 하지만 다음처럼 런타임에 서버 주소가 바뀌는 경우가 있습니다.


2. 방법 1 — 호출별 @Url

특정 요청만 전체 URL을 지정합니다. 가장 단순합니다.

interface DownloadApi {
    @GET
    suspend fun download(@Url url: String): ResponseBody

    @GET
    suspend fun getJson(@Url url: String, @Query("id") id: Long): Item
}

api.download("https://cdn.example.com/files/a.zip")

@Url은 첫 번째 파라미터여야 하고, 주어진 값이 절대 URL이면 baseUrl을 완전히 무시합니다. 상대 경로면 baseUrl 기준으로 결합됩니다.

적합한 경우: 일부 요청만 다른 주소(CDN 파일, 페이지네이션의 next URL 등)를 쓸 때.


3. 방법 2 — Interceptor로 호스트 치환

모든 요청의 호스트를 인터셉터에서 현재 설정값으로 교체합니다. API 코드는 그대로 둡니다.

@Singleton
class BaseUrlProvider @Inject constructor() {
    @Volatile var baseUrl: HttpUrl = "https://api.example.com/".toHttpUrl()
}

class HostSelectionInterceptor @Inject constructor(
    private val provider: BaseUrlProvider
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val target = provider.baseUrl

        val newUrl = request.url.newBuilder()
            .scheme(target.scheme)
            .host(target.host)
            .port(target.port)
            .build()

        return chain.proceed(request.newBuilder().url(newUrl).build())
    }
}
// 사용
baseUrlProvider.baseUrl = "https://staging.example.com/".toHttpUrl()
// 이후 모든 호출이 staging 서버로 전송됨

장점: Retrofit/API 인스턴스를 다시 만들지 않고 즉시 전환, 구조가 단순합니다. 한계: 경로 접두사(/v1/)까지 바뀌는 경우는 encodedPath 조작이 더 필요합니다.


4. 방법 3 — Retrofit 인스턴스를 교체

주소가 바뀔 때 새 Retrofit을 만들어 API를 다시 생성합니다.

@Singleton
class ApiFactory @Inject constructor(private val client: OkHttpClient) {

    private val cache = ConcurrentHashMap<String, Retrofit>()

    fun <T> create(baseUrl: String, service: Class<T>): T {
        val retrofit = cache.getOrPut(baseUrl) {
            Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(client)                      // OkHttpClient는 재사용 (커넥션 풀 공유)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
        }
        return retrofit.create(service)
    }
}
val api = apiFactory.create("https://${userInputHost}/", UserApi::class.java)

장점: 주소마다 경로/컨버터를 다르게 설정할 수 있어 가장 유연합니다. 주의: OkHttpClient는 하나를 공유하세요. Retrofit마다 새로 만들면 스레드/커넥션 풀이 중복됩니다.


5. 방법 비교

방법 전환 방식 적합한 경우
@Url 호출마다 지정 일부 요청만 다른 주소
Interceptor 치환 전역 설정값 변경 환경 전환, 전체 요청 주소 변경
Retrofit 교체/팩토리 인스턴스 생성 주소마다 설정이 다르거나 여러 서버 동시 사용

6. 사용자 입력 주소 — 검증 필수

사용자가 입력한 주소로 요청을 보낼 때는 안전성 검증이 필요합니다.

fun parseBaseUrl(input: String): HttpUrl? {
    val normalized = if (input.endsWith("/")) input else "$input/"
    val url = normalized.toHttpUrlOrNull() ?: return null     // 형식 검증
    if (url.scheme != "https") return null                    // 평문 HTTP 금지
    return url
}

7. 정리



Related Posts