lateinit 이란 무엇이며 언제 사용해야 하는가?
다음과 같은 NonNullableValueStateHolder 클래스를 살펴보자.
*실제 코틀린에서는 아래와 같이 getter와 setter을 직접 설정하는 경우가 거의 없다.
class NonNullableValueStateHolder() {
private var nonNullableValue: String = "testValue"
fun set(value: String) {
nonNullableValue = value
}
fun get(): String {
return nonNullableValue
}
}
이 클래스에는 nonNullableValue 라 불리는 String 타입의 non-nullable한 값이 있고, 이 변수에는 "testValue"가 할당돼 있다. 이제 이 변수에 "testValue"를 할당한 것을 해제 해보고 어떤 일이 일어나는지 살펴보자. 그러면 다음과 같이 컴파일 오류가 생기는 것을 확인할 수 있다.
일반적으로 이에 대한 해결 책은 위의 코드를 다음과 같이 바꾸는 것이다. 이 코드에서는 nonNullableValue의 타입 값이 nullable한 'String?'으로 바꼈고 null이 할당되었다.
class NonNullableValueStateHolder() {
private var nonNullableValue: String? = null
fun set(value: String) {
nonNullableValue = value
}
fun get(): String {
return nonNullableValue.orEmpty()
}
}
하지만, 위와 같은 방식으로 사용하게 되면 우리가 참조하는 값이 nullable한 타입 값으로 바뀐다. 위에서는 nullable한 타입 값을 핸들링 하기 위해 get 함수에만 orEmpty() 함수를 추가했지만, 만약 해당 변수를 사용하는 곳이 여러 군데라면 모든 곳에 null 체크나 orEmpty 같은 함수를 사용해야 한다.
이런 문제를 해결하기 위해 lateinit 이라는 키워드가 등장한다. lateinit 키워드를 나중에 초기화가 되어야 하는 변수에 추가하면 해당 변수를 초기화 하지 않을 수 있다. 예를 들어 위의 NonNullableValueStateHolder 클래스를 lateinit을 사용해 바꾸면 다음과 같아진다.
class NonNullableValueStateHolder() {
private lateinit var nonNullableValue: String // 나중에 초기화 가능
fun set(value: String) {
nonNullableValue = value
}
fun get(): String {
return nonNullableValue.orEmpty()
}
}
이렇게 바뀐 클래스를 다음과 같이 만들어보자.
fun main() {
val nonNullableValueStateHolder = NonNullableValueStateHolder()
nonNullableValueStateHolder.set("DummyString")
val value = nonNullableValueStateHolder.get()
println(value) // DummyString 출력
}
그러면 코드를 실행했을 때 "DummyString"이 출력되는 것을 볼 수 있다.
lateinit var의 문제와 해결 방법
변수를 선언할 때 lateinit var을 사용하면, 해당 변수가 초기화 되지 않은 채로 사용이 될 위험이 존재한다. 예를 들어 다음 코드를 실행해보자.
fun main() {
val nonNullableValueStateHolder = NonNullableValueStateHolder()
val value = nonNullableValueStateHolder.get()
println(value)
// Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property nonNullableValue has not been initialized
}
이 코드에서는 nonNullableValueStateHolder의 set 함수를 호출하지 않고 get 함수를 호출한다. 따라서 다음과 같은 오류가 발생한다.
lateinit property nonNullableValue has not been initialized
lateinit으로 선언된 'nonNullableValue'가 초기화 되지 않았습니다.
이런 오류는 NullPointException 보다는 비교적 명확하지만, 여전히 오류를 발생시킨다.
이를 해결하기 위해서는 lateinit으로 선언된 변수를 사용하기 전, isInitialized 함수를 통해 초기화가 정상적으로 이루어졌는지 체크를 한 후, 초기화가 이뤄지지 않았을 때 처리 로직을 만들어줘야 한다.
class NonNullableValueStateHolder() {
private lateinit var nonNullableValue: String
fun set(value: String) {
nonNullableValue = value
}
fun get(): String {
return if(this::nonNullableValue.isInitialized) {
nonNullableValue
} else {
""
}
}
}
개인적인 의견
개인적으로 lateinit을 개발 중인 코드에 사용하는 것을 선호하지는 않는다. lateinit 을 사용하게 되면 초기화 시점이 언제인지 추적하기 어려워지기 때문이다. 또한 lateinit으로 선언된 변수를 안전하게 사용하려면 사용하기 전 isInitialized 체크를 해야 하는데, 이것이 null check를 하는 것과 다른 점이 없다고 생각한다. 이렇게 사용하기보다는 차라리 null 로 초기화 하는 것이 나은 것 같다.
lateinit var이 가장 유용한 경우는 테스트 코드를 만들 때 인 것 같다. 테스트 코드를 만들 때는 테스트 대상 클래스가 @BeforeEach가 붙은 함수에서 초기화 되는 경우가 많으며, 초기화 시점이 헷갈릴 일도 없기 때문에 아주 유용하게 사용할 수 있다.
fun TestClass() {
lateinit var sut: SomeClass
@BeforeEach
fun setUp() {
sut = SomeClass()
}
...
}
추가자료. lateinit은 무조건 var과 함께 써야 할까?
lateinit은 해당 변수에 나중에 객체를 할당하는 것이기 때문에 무조건 var과 써야 한다. val은 참조를 변경할 수 없는 변수 선언 방식이므로 사용할 수 없다. 만약 val 과 함께 사용하면 다음과 같은 오류가 난다.