하지만 안드로이드의 View 바인딩이나 외부 라이브러리의 의존성 주입처럼 "변수는 미리 선언해 둬야 하는데 값은 나중에 들어오는" 상황이 많습니다. 이때 Nullable(?)을 쓰면 매번 널 체크를 해야 하는 불편함이 생기므로, 코틀린은 lateinit과 lazy라는 훌륭한 지연 초기화 기법을 제공합니다.
💡 핵심 비유: 지각생과 게으름뱅이
lateinit (지각생): "지금은 값을 못 주지만, 내가 쓰기 전까지는 무조건 책임지고 값을 채워놓을게! 그러니까 일단 Null 말고 var로 믿고 선언해 줘!" lazy (게으름뱅이): "나는 복잡한 계산식이라 미리 만들어 두기 귀찮아. 누군가 나를 처음으로 부르는 순간에만 val을 만들어서 줄게!"
⏳ 지연 초기화 전략 시각화
📊 lateinit 과 lazy 완벽 비교
비교 항목
lateinit
lazy
허용 변수
var (가변) 전용
val (불변) 전용
작동 방식
선언만 해두고, 나중에 외부에서 값을 = 로 대입해줌.
자신이 선언된 블록 코드를 최초 접근 시 실행하고 그 결과를 가짐.
위험성
값을 셋팅하기 전에 읽으면 Uninitialized... 크래시 발생
안전함 (부르면 무조건 생성되므로)
class Rectangle(var width: Int, var height: Int) {
// 1. 커스텀 Getter
// 필드처럼 보이지만, 읽을 때마다(get) 매번 계산식이 실행되어 값을 반환합니다.
val isSquare: Boolean
get() = (width == height)
val area: Int
get() = width * height
}
class Profile {
// 2. lateinit: 객체를 미리 선언은 해야 하지만 지금 당장 값을 넣을 수 없을 때 씁니다.
// 안드로이드의 뷰(View) 바인딩 등에서 필수적으로 사용됩니다. (null을 피하기 위한 궁여지책)
lateinit var nickname: String
// 3. lazy: 프로그램이 시작될 때 미리 만들어두면 메모리와 시간이 아까운 무거운 데이터를 다룰 때 씁니다.
// 'heavyData'가 코드 상에서 최초로 불려지는 순간, 딱 1번만 중괄호 안의 코드가 실행됩니다.
val heavyData: String by lazy {
println("=> 복잡하고 오래 걸리는 데이터 로딩 중...")
"거대한 용량의 데이터셋"
}
}
fun main() {
val rect = Rectangle(10, 10)
println("정사각형인가? " + rect.isSquare) // true
println("면적: " + rect.area) // 100
val p = Profile()
// 만약 여기서 닉네임을 할당하기 전에 println(p.nickname)을 부르면 크래시가 납니다!
// lateinit 은 개발자가 책임지고 쓰기 전에 할당하겠다는 약속입니다.
p.nickname = "코틀린러버"
println("설정된 닉네임: " + p.nickname)
// lazy 동작 확인
println("lazy 데이터는 아직 로딩 안 됨.")
// 아래에서 처음으로 heavyData를 부르는 순간, lazy 블록이 실행되어 로그가 찍힙니다.
println("1차 호출: " + p.heavyData)
// 두 번째 부를 땐 로직을 다시 실행하지 않고, 메모리에 저장해둔 결과값만 빠르게 리턴합니다.
println("2차 호출: " + p.heavyData)
}