지연 초기화(lazy initialization)는 Kotlin에서 객체의 속성이 생성 시점이 아니라 처음 접근할 때 초기화되는 메커니즘입니다. JetBrains, 2024에 따르면, lateinit과 lazy는 이 전략을 구현하기 위한 두 가지 내장 도구입니다. 둘 다 지연 초기화 문제를 해결하지만, 작동 메커니즘과 적용 범위에서 근본적으로 다릅니다.
핵심 요약
지연 초기화는 클래스 속성이 객체 생성 시점이 아니라 나중에 필요할 때 값을 받는 패턴입니다. Kotlin에서는 이 패턴이 두 가지 근본적으로 다른 방식으로 구현됩니다: lateinit 수정자와 lazy 위임자입니다.
두 메커니즘 모두 공통 문제를 해결합니다 — 속성은 클래스에 존재해야 하지만, 그 값은 객체 생성 시점에 알 수 없거나 불필요하게 수행하기에는 리소스가 너무 많이 소모됩니다. Google I/O 2023에 따르면, 일반적인 Android 애플리케이션에서 최대 40%의 속성을 지연 초기화를 통해 최적화할 수 있으며, 시작 시간을 15~25% 단축할 수 있습니다.
lateinit과 lazy의 선택은 세 가지 요소에 의해 결정됩니다: 속성의 가변성(var 또는 val), 수명 주기(단일 또는 다중 할당), 스레드 안전성 요구 사항(단일 스레드 또는 다중 스레드 접근)입니다.
첫 번째이자 가장 일반적인 시나리오는 의존성 주입입니다. 프레임워크(Dagger, Hilt, Koin)는 객체 생성 후 의존성을 주입하므로, 속성을 생성자에서 초기화할 수 없습니다. lateinit이 없으면 모든 의존성을 nullable로 선언하고 사용할 때마다 확인해야 합니다.
두 번째 시나리오는 무거운 리소스입니다: 데이터베이스, 네트워크 클라이언트, 파일 관리자 등. 이러한 생성에는 시간과 메모리가 필요하므로 실제로 사용될 때만 초기화해야 합니다. lazy는 이러한 경우에 이상적이며, 단 한 번 생성을 보장합니다.
세 번째 상황은 Android 구성 요소(Activity, Fragment, ViewModel)로, 수명 주기가 운영 체제에 의해 관리됩니다. onCreate, onViewCreated 또는 ViewModel의 init 블록에 의존하는 속성은 생성자에서 초기화할 수 없습니다.
lateinit은 var 속성용 수정자로, Kotlin 컴파일러가 초기화를 연기할 수 있게 합니다. 컴파일러는 생성자에서 값 할당을 요구하지 않지만, 모든 접근 시 런타임 검사를 생성합니다: 속성이 초기화되지 않은 경우 UninitializedPropertyAccessException을 발생시킵니다.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
lateinit의 제한사항: 속성은 var(val 아님), 널 불가능, 기본 타입(Int, Double, Boolean 등)이 아니어야 합니다. 그 이유는 기본 타입이 JVM 기본형으로 컴파일되며, “초기화되지 않음” 상태가 없기 때문입니다. 널 가능 속성의 경우 지연 초기화가 필요하지 않습니다: null은 이미 값이 없음을 의미합니다.
lateinit 속성의 상태를 확인하려면 :: 연산자를 통해 내장 참조를 사용하세요: ::propertyName.isInitialized. 예외 위험 없이 속성이 초기화되었는지 확인하는 유일한 안전한 방법입니다. 이 확인은 동일한 클래스 또는 내부 클래스에서만 가능하며, 외부 코드에서는 불가능합니다.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit은 초기화 후 오버헤드를 추가하지 않습니다: 값이 할당된 후 속성 접근은 직접 필드 접근과 동일합니다. 유일한 비용은 할당 전 읽기마다 초기화 확인입니다. 초기화 후 JIT 컴파일러는 확인을 최적화합니다.
중요한 참고 사항: lateinit 속성은 인라인 클래스에서 사용할 수 없으며 사용자 정의 getter/setter가 있는 속성에서 지원되지 않습니다. 속성에 계산된 접근이 필요한 경우 lateinit 대신 lazy를 사용하세요.
lazy는 Kotlin 표준 라이브러리에 내장된 속성 위임자입니다. 속성에 처음 접근할 때 값을 계산하고 이후 모든 호출에 대해 결과를 캐시합니다. lateinit과 달리 lazy는 val에서만 작동하여 초기화 후 속성을 불변으로 만듭니다.
class UserRepository {
private val database: Database by lazy {
Database.create("users.db")
}
fun getUser(id: String): User {
return database.query("SELECT * FROM users WHERE id = ?", id)
}
}
lazy는 스레드 안전 메커니즘을 제어하는 선택적 매개변수 LazyThreadSafetyMode를 허용합니다. 기본값은 SYNCHRONIZED로, 잠금을 사용한 이중 확인 방식으로 여러 스레드의 동시 접근에서도 단일 초기화를 보장합니다.
PUBLICATION 모드는 병렬 초기화를 허용합니다: 여러 스레드가 동시에 초기화 블록을 실행할 수 있지만, 결과는 먼저 완료된 스레드에서만 허용됩니다. 높은 경합 상황에서 SYNCHRONIZED보다 빠르지만 리소스 소비가 증가합니다.
NONE 모드는 동기화를 완전히 비활성화합니다. 단일 스레드에서 접근이 보장되는 속성에만 사용하세요. 이 모드에서 lazy는 최소 오버헤드로 작동합니다 — 거의 직접 할당과 같습니다.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy는 한 번만 초기화되는 의존성(리포지토리, 네트워크 클라이언트, 캐시, 데이터베이스)에 적합한 선택입니다. val 의미론은 우발적인 덮어쓰기를 방지하고, 기본 스레드 안전성은 다중 스레드 환경에서 코드를 안전하게 만듭니다. lazy는 기본 타입에서도 올바르게 작동하며, 이는 lateinit에서는 불가능합니다.
Android에서 lazy는 by viewModels()를 통한 ViewModel 의존성 초기화나 Retrofit 클라이언트 생성에 자주 사용됩니다. 그러나 주의하세요: lazy 블록이 Activity나 Fragment에 대한 참조를 캡처하면 메모리 누수가 발생할 수 있습니다. 위임자가 속성의 수명 동안 클로저를 유지하기 때문입니다.
lateinit과 lazy의 선택은 선호도의 문제가 아니라 속성의 특성에 따라 결정되는 아키텍처 결정입니다. 각 메커니즘은 고유한 작업을 해결하며, 적용 영역이 부분적으로만 겹칩니다.
| 기준 | lateinit | lazy |
|---|---|---|
| 속성 타입 | var만 | val만 |
| 널 가능 | 불가 | 가능 |
| 기본 타입 | 불가 | 가능 |
| 스레드 안전성 | 보장되지 않음 | SYNCHRONIZED 기본 |
| 상태 확인 | ::x.isInitialized | 필요 없음 |
| 오류 시 예외 | UninitializedPropertyAccessException | init 블록 오류 |
| 캐싱 | 해당 없음 | 단일 계산 |
| Android Binding | View Binding, Data Binding | 사용되지 않음 |
| DI 프레임워크 | Dagger, Hilt, Koin | 수동 주입 |
lateinit은 속성이 초기화 후 변경되어야 하거나 생성이 외부 코드에 의해 관리되는 경우 사용합니다. 일반적인 예는 Android Activity의 View Binding입니다: binding은 onCreate에서 생성되지만 프레임워크가 이 시나리오에서 val을 지원하지 않으므로 var로 유지됩니다.
lazy는 속성이 한 번 초기화되고, 계산 비용이 크며, 객체 수명 동안 값이 변경되지 않는 경우 사용합니다. 전형적인 예는 리포지토리에 처음 접근할 때 Retrofit 클라이언트나 Room 데이터베이스를 지연 생성하는 것입니다.
두 메커니즘 모두 동일한 클래스 내에서 동시에 사용할 수 있습니다. 예를 들어, View Binding에는 lateinit, 리포지토리에는 lazy를 사용합니다. 이는 서로 다른 속성에 대한 서로 다른 요구 사항을 반영하는 정상적인 관행입니다. 핵심은 의미를 혼동하지 않는 것입니다: val이 필요한 곳에 lateinit을 사용하지 말고, 다시 할당해야 하는 속성에 lazy를 사용하지 마세요.
lateinit의 가장 흔한 실수는 속성이 초기화되기 전에 접근하는 것입니다. 이는 UninitializedPropertyAccessException을 발생시키며, Kotlin이 개발자가 올바른 초기화 순서를 보장할 것이라고 신뢰하기 때문에 컴파일 시점에 포착되지 않습니다. 해결책은 모호한 상황에서 접근 전에 ::property.isInitialized를 통해 항상 상태를 확인하는 것입니다.
두 번째 흔한 문제는 의미상 val인 속성에 lateinit을 사용하는 것입니다. 값이 한 번 설정되고 절대 변경되지 않는 경우 lazy가 더 올바른 선택입니다. 속성을 불변으로 만들고, 우발적인 덮어쓰기를 방지하며, 무료로 스레드 안전성을 추가합니다.
세 번째 실수는 부작용이 있는 lazy입니다. lazy 초기화 블록은 외부 상태를 수정하거나 다른 lazy 속성의 초기화 순서에 의존해서는 안 됩니다. 계산 순서는 첫 접근에 따라 달라지며 명확하지 않을 수 있기 때문입니다. lazy 속성이 서로를 참조하면 순환 의존성과 StackOverflowError가 발생합니다.
네 번째 문제는 Android에서 lazy를 통한 메모리 누수입니다. lazy 블록이 Activity나 Fragment에 대한 참조를 캡처하면 위임자가 클로저를 유지하여 가비지 수집기가 구성 요소를 파괴 후에도 해제할 수 없습니다. 해결책은 수명이 짧은 객체에만 lazy를 사용하거나 Activity 대신 Application 컨텍스트를 전달하는 것입니다.
다섯 번째 전형적인 실수는 기본 타입에 lateinit을 적용하려는 시도입니다. Kotlin 컴파일러는 구문 수준에서 이를 차단하지만, 개발자는 널 가능 래퍼를 통해 제한을 우회하려고 합니다. 이로 인해 불필요한 null 검사가 발생하고 지연 초기화의 이점이 완전히 무효화됩니다.
자주 묻는 질문
lateinit은 var 속성용 수정자로, 생성자 후 초기화를 허용합니다. lazy는 val 속성용 위임자로, 첫 접근 시 값을 계산하고 캐시합니다. lateinit은 기본 타입과 nullable을 지원하지 않는 반면, lazy는 기본적으로 스레드 안전합니다.
네, 내장 속성 참조를 통해 가능합니다: ::propertyName.isInitialized. 속성이 초기화된 경우 메서드는 true를 반환합니다. lateinit 필드로 작업할 때 UninitializedPropertyAccessException을 피하는 유일한 안전한 방법입니다.
기본 타입 — Int, Double, Boolean 등 — 은 JVM 기본형(int, double, boolean)으로 컴파일되며, “초기화되지 않음” 상태가 없습니다. lateinit은 null을 플래그로 사용하지만 기본형은 null이 될 수 없으므로 이러한 타입에서는 메커니즘이 물리적으로 불가능합니다.
기본값은 LazyThreadSafetyMode.SYNCHRONIZED입니다 — 잠금을 사용한 이중 확인 방식으로 여러 스레드의 동시 접근에서 단일 초기화를 보장합니다. 단일 스레드 시나리오에서는 NONE을, 높은 경합에서는 PUBLICATION을 사용하세요.
속성이 초기화 후 변경되어야 하거나 생성이 프레임워크에 의해 관리되는 경우입니다. 일반적인 예는 Android Activity의 View Binding입니다: binding은 onCreate에서 생성되며 var여야 합니다. 한 번만 초기화되는 val 의존성에는 lazy를 사용하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.