Java에서 Kotlin으로 넘어오는 순간 가장 체감이 빨리 느꼈던 부분이 변수 선언부분이었다.
Kotlin은 Java와 다르게 변수의 자료형을 자동으로 추론하는 기능이 생겼다.
// Java
String str = "Hello World";
// Kotlin
val str = "Hello World"
대신, 변수 앞에 자료형을 쓰지 않고 var나 val을 붙이면 된다. var는 variable의 약자로 뜻 자체로는 '변하기 쉬운'이지만 프로그래밍에선 '변수'라는 뜻을 가지고 있다. val는 value의 약자로 '값'이라는 의미인데 아마 Jetbrain에서 비슷한 두 단어를 가지고 오는게 깔끔하니 value를 가져오지 않았나 추측한다.
많은 경우에 전역변수가 필요한 순간이 온다. Java에서는 자료형을 앞에 쓰고 뒤에 변수명을 넣으면 됐지만, Kotlin은 선언과 동시에 초기화를 하지 않는다면 자료형을 뒤에 붙여야 하기에 아래와 같이 써보았다.
// Java
String receiveData;
// Kotlin
val receiveData: String
사용하려하니 컴파일 에러가 뜬다.
Property must be initialized or be abstract
속성은 무조건 초기화되어야하거나 추상화를 시켜야 해용(●'◡'●).
그래서 val 대신 var로 바꿀라 하니 이 또한 비슷한 컴파일 에러가 나온다.
Kotlin은 값의 상태를 상당히 중요하게 여기는 듯 하다.
Java에선 전역으로 선언해 놓으면 더미값이 들어가는데, Kotlin은 이러한 불확실한 상태를 싫어하는듯?
이러한 값의 상태를 명확하게 Kotlin은 변수에 대한 제약들을 만들었고, 그 중 전역변수와 관련된 lateInit과 by lazy에 대해 알아보려한다.
둘의 공통점은 선언과 동시에 초기화를 하지 않는다는점인데 다른것이 뭐가 있는지 궁금했다.
Java에서처럼 전역에 val, var로 동일하게 선언해서 사용하면 되지 않았을까? 이는 앞에서도 말한 '더미값'의 문제일 듯하다. 더미값이 들어간 변수를 미처 초기화하지 않고 사용하게 된다면 필연적으로 버그가 따라오고 에러로그에 잡히지 않기 때문에 추적하기 어려워 Kotlin에선 이를 사전에 방지하기 위함으로 나온듯 하다.
가끔 전역변수로 lateInit이나 by lazy를 사용하지 않는 경우가 있는데
var str: String? = null
위와 같이 null값을 미리 초기화하고 var로 선언하여 추후에 변화를 주는 법이있다. 하지만 이는 Kotlin이 극혐하는 null을 사용함과 더불어
나중에 초기화를 무조건 한다면 Nullable일 필요도 없고 코드에 가독성을 떨어지게 만드는 ?연산자를 사용해야 하는 번거로움이 있다.
또한 메모리에 대한 이유도 있다. 둘 다 늦은 초기화라는 점에서 클래스가 생성될 시점에 인스턴스가 부여되는 것이 아닌 lateInit은 초기화 된 코드의 시점으로, by lazy는 해당 변수를 사용할 시점에서 초기화가 이루어지기 때문에
클래스가 로딩되는 시간과 사용되지 않을 전역변수의 메모리 공간을 줄여주는 효과도 있다.
근데 뭐 요즘 폰에서 얼마나 큰 차이가 있지는 모르겠지만 이런 세세한 부분들이 모여 1초의 시간을 당길 수 있지 않을까..?
- 값의 상태 안정화
- Null 안정화
- 효율적인 메모리 관리
lateinit var str: String
이름의 의미는 '늦은 초기화'이다. 전역변수인 애들은 대부분 데이터 타입만 정해놓기에 아주 어울릴 만한 느낌이다.
사용법은 val나 var대신 lateinit var를 사용하면 된다. 이러면 지금 당장은 str에 값이 할당되지 않았지만, 추후에 무조건 str를 초기화한다는 제약이 생긴다. 만약 초기화를 하지않고 str를 사용하게 된다면 컴파일에선 에러가 없지만 런타임에서 crash가 나버린다.
이 조건으로 인해 lateInit은 뒤에 나올 by lazy와 함께 NonNull상태이다. 또한 val이 아닌 var이기에 str를 여러번 초기화가 가능하다.
겪었던 문제중 버튼을 누르면 dialog가 팝업되고, 연결된 블루투스 기기가 꺼지면 dialog를 dismiss하는 플로우를 짰는데,
dialog가 showing되지 않는 상태에서 블루투스 기기가 꺼져버려 dismiss를 호출하는 상황이 있었다. 이때 dialog가 초기화되지 않는 상태이기에 앱이 팡~하고 터졌다. 이러한 경우, dialog라는 변수에 접근을 할 수 없기에 더블콜론(::)을 사용하여
::dialog.isInitialized
를 사용하여 체크해주면 된다. 그러면 초기화전 lateInit은 NonNull이라 null이 아니면 Java와 같이 더미값이 들어간 상태일까?
찾아보니 '초기화되지 않은'상태라고 한다.
추가로 lateInit에는 기본형(primitive)타입은 사용할 수 없다. 참조형 타입만이 가능해보이는데, 그래서 String은 가능하다.!
그래서 기본형 타입을 전역변수로 사용하려면 by lazy나 값이 바뀔만한 is~ 같은 Boolean 타입들은
var value: Int? = null
var isSuccess: Boolean = false
와 같이 전역 시점에 초기화를 해줘야 한다.
lateInit과 비슷한 의미로 '게으른 초기화'이다.
실사용은 lateInit과 비슷한데 by lazy는 val형태로 초기화 후 바꿀수 없고, 초기화 시점이 해당 변수를 사용하는 시점이다.
로그를 찍어보니 by lazy로 선언된 변수가 사용된 시점에 initialized로그가 먼저 찍히는 모습이 보였다.
그럼 비슷한 이 둘을 어느 상황에 써야 알맞을까?
주로 나는 by lazy를 자주 사용하는 편이다. 전역변수로 쓰이는 애들이 대부분 인스턴스가 변하는 경우가 없기 때문이다.
2가지 경우에 lateInit을 사용한다.
물론 by lazy 특성이 중괄호를 사용해야 해서 혼자 3줄을 잡수시는데, 이를 한 줄로 줄여놓고 나중에 전체 코드 정렬(Ctrl + Alt + l)을 실수로 누르면 자동으로 라인이 늘어나 귀찮은 부분이 있기는 하다.
요약
끗~!