Android 4.4에서 Retrofit의 최신버전을 사용하는 방법

Walter Jeon·2022년 2월 12일
0

이 글은 Android 5.0 미만 버전을 지원하는 앱에서 Retrofit의 최신 버전을 사용하는 방법을 다룹니다.

Retrofit은 안드로이드 앱 개발에 널리 사용되는 네트워킹 라이브러리 입니다. 현재 Retrofit의 최신버전은 2.9.0 인데, 이 버전을 Android 5.0 미만에서 실행시키면 런타임 에러가 발생합니다. 그 원인을 알아보고 해결방법 2가지를 소개하겠습니다.

에러 로그

java.lang.ExceptionInInitializerError
    at okhttp3.OkHttpClient.newSslSocketFactory(OkHttpClient.java:263)
    at okhttp3.OkHttpClient.<init>(OkHttpClient.java:229)
    at okhttp3.OkHttpClient$Builder.build(OkHttpClient.java:1015)

원인

Retrofit은 2.7.0 버전부터 최소요구사항으로 Java8, Android 5.0 (minSdk 21)을 요구합니다. 그 이유는 Retrofit은 2.7.0 버전부터 OkHttp 3.14.4를 사용하고, OkHttp 3.14.4는 TLS 1.2에서 동작하기 때문입니다. 구글은 안드로이드 5.0 버전부터 TLS 1.2를 지원하고 있습니다.

Version 2.7.0 (2019–12–09)
This release changes the minimum requirements to Java 8+ or Android 5+.
https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-270-2019-12-09

Google added support for TLSv1.2 in Android 5.0. Oracle added it in Java 8. In OkHttp 3.13 we require that the host platform has built-in support for TLSv1.2.
https://code.cash.app/okhttp-3-13-requires-android-5

만약 내가 개발하는 앱의 minSdk가 19인데 Retrofit의 최신버전을 쓰고 싶은 경우 해결방법은 2가지가 있습니다.

방법 1 : OkHttp 버전을 3.13 미만으로 내리기

build.gradle에서 OkHttp 버전만 3.12.13버전으로 내립니다.

implementation('com.squareup.retrofit2:retrofit:2.9.0')
implementation('com.squareup.okhttp3:okhttp') { version { strictly '3.12.13' } }

이 방법의 단점은 앞으로 Retrofit의 버전이 올라가도 내 앱의 OkHttp 버전은 여전히 3.13 미만으로 유지된다는 것입니다. 게다가 TLS 1.1을 허용하기 때문에 TLS 1.1의 보안 취약점을 계속 안고 가게 됩니다.

방법 2: TLS 1.2로 업데이트 하는 코드 추가하기

Security Provider를 업데이트 하는 코드를 추가하면 런타임에 Google Play Service를 통해 TLS Provider를 다운로드 합니다. 다운로드에서 업데이트까지 보통 30~50밀리초 정도의 시간이 걸립니다. 구형 기기는 350밀리초 정도 걸린다고 합니다.

구글에서 제공하는 함수는 2가지 방식이 있는데, 앱의 특성에 맞게 골라서 쓰면 될 것 같습니다.

  • 동기(sync) 방식
    installIfNeeded(Context: context)

  • 비동기(async) 콜백 방식
    installIfNeededAsync(Context context, ProviderInstaller.ProviderInstallListener listener)

구현 예시

구글 예제를 바탕으로 비동기 방식으로 처리하는 코드를 작성해보았습니다.

먼저 build.gradle 파일에 Google Play Service 사용을 위한 의존성을 추가합니다.

implementation('com.google.android.gms:play-services-basement:18.0.0')

SSL Provider를 업데이트 하기에 적당한 Activity를 고른 후 installIfNeededAsync() 함수를 호출하고, 응답을 받을 콜백을 구현했습니다. 이 예시에서는 MainActivity가 ProviderInstaller.ProviderInstallListener를 구현하고 있으므로, 리스너로 this를 지정해 주었습니다.

import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.common.GoogleApiAvailabilityLight
import com.google.android.gms.security.ProviderInstaller
import com.wally.gallery.R

private const val ERROR_DIALOG_REQUEST_CODE = 1

class MainActivity : AppCompatActivity(), ProviderInstaller.ProviderInstallListener {
    private var retryProviderInstall: Boolean = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            ProviderInstaller.installIfNeededAsync(this, this)
        }
    }

    override fun onProviderInstallFailed(errorCode: Int, recoveryIntent: Intent?) {
        Log.w(TAG, "onProviderInstallFailed!")
        GoogleApiAvailabilityLight.getInstance().apply {
            if (isUserResolvableError(errorCode)) {
                Log.w(TAG, "onProviderInstallFailed!")
                showErrorDialogFragment(
                	this@MainActivity, 
                    errorCode, 
                    ERROR_DIALOG_REQUEST_CODE) {
                    onProviderInstallerNotAvailable()
                }
            } else {
                onProviderInstallerNotAvailable()
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == ERROR_DIALOG_REQUEST_CODE) {
            retryProviderInstall = true
        }
    }

    override fun onProviderInstalled() {
        Log.d(TAG, "onProviderInstalled!")
    }

    override fun onPostResume() {
        super.onPostResume()
        if (retryProviderInstall) {
            ProviderInstaller.installIfNeededAsync(this, this)
        }
        retryProviderInstall = false
    }

    private fun onProviderInstallerNotAvailable() {

    }

    companion object {
        private const val TAG = "MainActivity"
    }
}

이 방법도 단점이 있습니다. Google Play Service가 설치되어 있지 않은 기기에서는 동작하지 않는다는 것입니다. 그리고 Google 서비스가 막혀있는 국가에서는 동작하지 않을 수 있으니, 글로벌 서비스를 하는 앱은 실제 동작을 꼭 확인해야 합니다.

참고자료

profile
Software engineer

0개의 댓글