안드로이드와 코틀린. 내가 만난 문제
제가 속해 있는 Grab Taxi 는 Kotlin 코드가 90% 에 가까운 상태에 있습니다. XML(레이아웃, 벡터, 이미지)을 제외하면 거의 대다수의 코드가 사실상 코틀린이라고 보시면 됩니다.
Java 와 Kotlin 이 혼재 되어 있을 때 상황. Kotlin 만으로 코드를 구성했을 때 상황이런 경우 몇가지 어려움을 만날 수 있었는데 그에 대한 이야기를 해보도록 하겠습니다.
Java 가 Kotlin 을 호출할 때
가장 큰 문제점은 Optional 처리입니다. Kotlin 은 변수뒤에 ? 를 붙이지 않으면 Non-Null 객체로 보고 Null 이 전달되면 그 순간 에러를 발생합니다.
정말 간단한 문제이지만 뻔히 알면서도 당합니다. 컴파일러와 에디터가 미리 알려주는 경우도 있지만 모든 것을 검증해주지 않기 때문에 잠재적인 오류 가능성을 항상 가지고 있습니다. 그동안 Unit Test 에 단련되었기 때문에 당연히 Null 회피쯤은 만렙찍었겠지 생각했다가 에러를 보게 되어 굉장히 당황했습니다.
해결 방법
Nullable 과 NonNull 은 근본적인 해결책이 되기 어렵습니다. 사실상 협업과정에서 한명이라도 어노테이션을 실수로 기입하지 않으면 언제든 잠재적인 오류 가능성이 있습니다. 모든 코드를 Kotlin 으로 바꾸기 전까지 모든 참조 변수는 Optional 이어야 하는 기이한 방법밖에 없습니다.
Dagger 와 Databinding 이 만났을 때
Library 모듈에서 Databinding 을 사용하고 상위 모듈에서 Dagger 를 사용할 때 하위 모듈의 Databinding 과정에서 lateinit 으로 선언된 Binding 객체를 찾을 수 없다는 오류가 발생합니다. 이는 코드 생성 후 패키징 과정에서 라이브러리 모듈의 Databinding 결과물이 빠지면서 발생하는 것으로 Kotlin 과 KAPT 의 문제점이 아니라 안드로이드 빌드 툴 자체의 문제로 파악되어 있습니다.
해결 방법
JetBrain 의 답변에 의하면 lateinit 이라는 선언자가 가지는 특성때문에 컴파일 과정에서 타입체크를 하기 때문에 발생한다고 합니다. 따라서 타입체크를 하지 않도록 해서 패키징 과정에서 오류를 발생하지 않도록 우회할 수 있습니다.
저희는 Binding 모듈을 위한 Wrapper 를 별도로 만들어서 Binding의 타입체크를 피합니다.
class BindingWrapper<out T> constructor(val binding:T)
class BindingFragment() :Fragment {
lateinit val wrapper : BindingWrapper<FragmentLayoutBinding>
val binding : FragmentLayoutBinding
get() = wrapper.binding
}
JetBrain 의 해결책은 다음과 같습니다.
class BindingFragment() : Fragment {
@JvmField var binding : FragmentLayoutBinding? = null
}
Inline 함수와 람다 그리고 Proguard
Kotlin 으로 코드를 작성하다보면 let, run, apply 함수를 굉장히 많이 사용하게 됩니다. 그리고 SAM Conversion 에 익숙해지면 함수의 마지막 인자 값으로 함수형 인자를 사용하는 일도 잦아집니다.
특정지을 순 없지만 몇몇 경우에 람다의 깊이가 3중이상이고 위의 let,run,apply 를 Optional 객체와 사용했을 때 Proguard 의 obfuscation 과정에서 레퍼런스를 찾을 수 없다는 오류를 뿜어내게 됩니다.
해결 방법
Kotlin 코드를 컴파일 하는 과정에서 람다 표현식들은 함수 객체로 바뀌게 됩니다. 그 과정에서 의도치 않은 코드가 난독화되는 것이기 때문에 람다 깊이를 줄여주면 됩니다.
// 오류가 발생할 수 있는 코드
data?.let {
doSomething(it) {
doSomething2(it) {
// do something
}
}
}
위의 코드는 아래와 같이 변경합니다.
// 오류 회피 코드
val temp = data
if (temp != null) {
doSomething(temp) {
doSomething2(it) {
// do something
}
}
}
거짓말처럼 난독화 과정에서 오류가 발생하지 않습니다.