-
🦥 lateinit 과 lazy🔖 Kotlin 2024. 12. 29. 15:33
💡 지연초기화
초기화를 미뤘다 실제 사용하는 시점에 초기화하는 기법
- 초기화 과정에서 자원을 많이 사용하거나 오버헤드가 발생하는 경우 지연초기화를 사용하는 것이 유리
❗ 코틀린에서는 두 가지의 다른 방식의 지연초기화를 제공
- lateinit, lazy
- 변수를 초기화하는 시점을 제어해 성능 최적화와 코드 가독성을 높인다.
📌 lazy
class HelloBot { var greeting: String? = null fun sayHello() = println(greeting) } fun getHello() = "안녕하세요" fun main() { val helloBot = HelloBot() helloBot.greeting = getHello() helloBot.sayHello() }
⚠️ 위 코드의 문제점?!
- 지연할당을 위해서 var 키워드를 이용해 변수를 선언해주었다.
값 변경 가능성 var로 선언된 변수는 가변적이기 때문에, 코드 어느 지점에서든 값이 변경될 수 있다. 의도치 않게 변경되면 프로그램 동작이 비정상적으로 변할 수 있고 디버깅을 어렵게 한다. 초기 상태와 사용 지점의 불일치 초기화되지 않은 상태(null 인 상태)에서 사용될 위험이 있다. null을 허용하기 때문에 개발자가 사용 시점에 null 체크를 누락시, NPE 발생할 수 있다. 📌 val과 lazy를 활용해 해결
- 코틀린에서는 이와 같은 상황에서 lazy 라는 기능을 사용해 불변성을 유지하면서 지연 초기화를 할 수 있다.
- 첫 호출시점에 초기화를 해주고 이후에는 값이 불변함을 보장
class HelloBot { val greeting: String by lazy { getHello() } fun sayHello() = println(greeting) } fun getHello() = "안녕하세요" fun main() { val helloBot = HelloBot() helloBot.sayHello() }
👋 결과
- 가변 변수를 사용하지 않고, 불변 변수(val)과 lazy를 활용하면 변수가 한 번만 초기화되고 이후 변경되지 않도록 보장할 수 있다.
- 초기화는 실제 필요한 시점까지 지연되어 메모리 사용도 최적화된다.
👍 by lazy는 멀티스레드 환경에서도 안전하게 동작하도록 설계가 되어 있다.
fun main() { val helloBot = HelloBot() for (i in 1 until 5) { Thread { helloBot.sayHello() }.start() } }
- 이럼에도 초기화 로직은 단 한번만 실행이 된다.
- 기본적으로 멀티스레드에 안전하게 설계가 되어 있다는 것을 의미
❗ by lazy 같은 경우 이렇게 멀티스레드 환경에서 안전하게 사용할 수 있지만, 해당 락에 대한 처리를 끄고서도 동작시킬 수 있다.
- by lazy의 default = LazyThreadSafetyMode.SYNCHRONIZED
LazyThreadSafetyMode.NONE 를 사용한 코드
class HelloBot { val greeting: String by lazy(LazyThreadSafetyMode.NONE) { println("초기화 로직 수행") getHello() } fun sayHello() = println(greeting) } fun getHello() = "안녕하세요" fun main() { val helloBot = HelloBot() for (i in 1 until 5) { Thread { helloBot.sayHello() }.start() } }
- 초기화 로직이 한 번만 수행되지 않는다.
- 실행할 때마다 일관성 없게 동작
⚠️ 멀티 스레드 환경이 아니라면 동기화 작업이 오히려 오버헤드일 수 있다.
만약 멀티 스레드 환경인 경우에도 동기화 작업이 필요없다면? ⇒ PUBLICATION 옵션을 사용할 수 있다.
📌 lateinit
특정 상황, 불변 객체에 대한 초기화를 제공하지 않아 가변 변수를 써야하는 상황이 있을 수 있다.
이런 경우에서 지연 초기화가 필요하다면 lateinit 기능을 사용
ex) JUnit 테스트 환경
@Autowired lateinit var service: TestService lateinit var subject: TestTarget @SetUp fun setup() { service = TestTarget() }
- setup 메서드 안에서 객체를 초기화하는 경우 lazy를 사용할 수 없다.
class LateInit { lateinit var text: String // 무조건 var (val 타입은 컴파일에러) }
📖 특징
- lateinit var 로 선언한다. (var, 가변 변수만 가능)
- 변수를 선언할 때 초기화하지 않고 적절한 시점에 초기화
- lateinit은 nullable이 아닌 타입에 사용할 수 있다.
- nullable 하게 선언시 컴파일 에러 발생 (ex. String?)
- 항상 Non-Null 이라 생각하면 된다.
- ⚠️ 사용 전에 반드시 초기화해야 하며 초기화되지 않은 상태에서 접근 시 UninitializedPropertyAccessException 발생
- 컴파일러가 초기화 여부를 확인하지 못하기 때문에 관리를 해주지 않으면 런타임 에러 발생
- Primitive Type(Int, Float, Double, Long 등)에 사용할 수 없다.
📍 isInitialized
특정 프레임워크나 라이브러리에서 DI나 외부에서 초기화를 해주는 경우를 염두한 후 만든 기능이기 때문에 초기화 전에 사용하더라도 컴파일 에러가 발생하지 않는다.
- late init으로 된 변수에 대한 초기화 여부를 확인하기 위해서는 isInitialized 라는 프로퍼티를 사용해주어야 한다.
class LateInit { lateinit var text: String fun printText() { if(this::text.isInitialized) { println("초기화됨") } else { text = "안녕하세요" } println(text) } }
🤔 왜 non-null 프로퍼티로만 사용가능할까?
1️⃣ 초기화 지연의 목적
- 객체가 반드시 초기화될 때까지 null 상태를 허용하지 않고 접근을 방지
- null을 허용할 시 변수를 사용하기 전에 매번 명시적으로 null 체크가 필요
- lateinit은 반드시 초기화가 이루어져야 하는 변수에 대해서만 사용하도록 설계되었기 때문에 null을 할당 할 수 없도록 제한
2️⃣ 변수의 초기화 보장
- lateinit 변수는 초기화되지 않은 상태로 접근 시 예외가 발생한다 (UninitializedPropertyAccessException)
- 이때, 만약 null을 허용하게 된다면 초기화되지 않은 변수에 접근 시 null이 반환되거나 예외가 발생하지 않아 초기화가 되지 않은 상태를 체크하지 않은채 사용될 수 있다.
- 따라서, 초기화되지 않은 상태에서 안전하지 않은 동작을 방지하기 위해 null 할당을 허용하지 않는다.
이렇게 lateinit은 null을 허용하지 않음으로써, null 체크나 초기화 여부를 강제하지 않고 프로퍼티가 꼭 초기화되도록 보장하는 역할을 한다.
🤔 왜 Primitive Type에 사용할 수 없을까?
private lateinit var x: Int
lateinit의 내부 구현에는 null 과 비교하여 초기화여부를 판단한다.
하지만 primitive type에는 null을 넣을 수 없다.
private lateinit var x: Int? // 그럼 nullable한 타입으로 변경을 하면?
lateinit 변수가 null 값을 가질 수 있게 된다면 아까 null을 허용했을 때의 문제들이 발생하기 때문에 nullable type을 사용할 수 없다.
📖 참고자료
[Kotlin] Kotlin Lazy Initialization
Why Kotlin lateinit Can’t Be Used With Primitive Types | Baeldung on Kotlin
'🔖 Kotlin' 카테고리의 다른 글
😎 Infix Function (0) 2024.12.30 널 안정성 (0) 2024.12.29 Kotlin Coroutines (0) 2024.12.28 Unit / Nothing (0) 2024.11.25 Gradle Kotlin DSL (2) 2024.11.21