ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • kotlin) Kotlin 의 지연과 위임
    kotlin 2024. 7. 18. 23:10

    lateinit과 lazy

    인스턴스화 시점과 프로퍼티 초기화 시점

    class Person(
        val name: String,
    ) {
        val isKim: Boolean
            get() = this.name.startsWith("JS")
    
    
        //JS -> J*
        val maskingName: String
            get() = name[0] + (1 until name.length).joinToString("") { "*" }
    }
    
    fun main() {
        val person = Person("JS")       //클래스 인스턴스화가 이루어지며, name 에 "JS"가 들어간다.
    }
    1. 클래스 인스턴스화 시점:
      • val person = Person("JS")에서 Person("JS")를 호출하는 시점에 클래스 인스턴스화가 발생
      • 이 시점에서 Person 클래스의 새로운 객체가 힙 메모리에 할당
    2. 프로퍼티 초기화 시점:
      • Person("JS")를 호출할 때, 생성자가 실행되면서 클래스의 프로퍼티 name이 초기화
      • 생성자의 매개변수 "JS"가 name 프로퍼티에 할당

    인스턴스화 시점과 프로퍼티 초기화 시점을 분리하고 싶다면

    1) name을 nullable 로 처리하자

    /**
     * 기본값이 "홍길동" 에서 null 로 변경되었고
     * 이에 따라 name 변수의 타입도 String? 로 변경되었다.
     */
    class Person {
        var name: String? = null
        
        //실제 null 이 될 수 없기에, 계속 널 처리(?./?:/!!)가 들어가게 된다.
        val isKim: Boolean
            get() = this.name!!.startsWith("JS")
        val maskingName: String
            get() = name!![0] + (1 until name!!.length).joinToString("") { "*" }
    }

     

     

    2) lateinit

    • 인스턴스화 시점과 변수 초기화 시점을 분리하는것.
    • 컴파일 단계에서 nullable 변수로 바뀌고, 변수에 접근하려고 할 때 null 이면 예외 발생
    • primitive type(int, long) 에 사용할 수 없다
    class Person {
        lateinit var name: String           //초기값을 지정하지 않고, null이 들어갈 수 없는 변수를 선언
    
        val isKim : Boolean
            get()=  this.name.startsWith("JS")      //null 이 들어갈 수 없으니 !! 같은 null 체크 구문이 사라짐
    
    	//null 이 들어갈 수 없으니 !! 같은 null 체크 구문이 사라짐
        val maskingName: String
            get() = name[0] + (1 until name.length).joinToString("") {"*"} 
    }
    
    fun main(){
        val p = Person()
        p.isKim     //에러 발생 : 초기화 시키지 않았는데 접근 
    }

     

    변수를 초기화 할 때 지정된 로직을 1회만 실행

    지연 초기화 1회 로직

    //name 을 사용하지 않으면, Thread.sleep()이 호출되지 않지만 
    class Person{
    	//name을 쓸 때마다 sleep이 호출된다.
        val name: String
            get() {
                Thread.sleep(2_000)
                return "JS"
            }
    }
    
    //Thread.sleep() 은 1회만 호출되지만
    class Person{
        val name: String
        //name 을 사용하지 않는 경우에도 sleep 이 호출된다.
        init {
            Thread.sleep(2_000)
            name = "JS"
        }
    }

     

    요구사항을 엄밀히 구현하려면, backing property 를 사용해야함 by lazy

    //원초 코드
    class Person{
        private var _name: String? = null
        
        //name 이 사용되는 경우에만 _name 을 초기화
        val name : String
            get(){
                if (_name == null){
                    //꼭 필요할 때 1회만 호출
                    Thread.sleep(2_000L)
                    this._name = "JS"
                }
                return this._name!!
            }
    }
    
    ->
    
    //by lazy
    class Person {
        val name: String by lazy{	//by lazy 를 통해 위 코드와 동일한 기능을 이용
            Thread.sleep(2_000)
            "JS"
        }
    }

    위임패턴

    클래스가 다른 객체에게 그 책임을 위임하여 코드를 간결하고 유연하게 작성할 수 있도록 도와주는 패턴

    class LazyInitProperty<T>(val init: () -> T){
        private var _value: T? = null
        val value: T
            get(){
                if(_value == null){
                    this._value = init()
                }
                return _value!!
            }
    }
    
    class Person{
        //LazyInitProperty 를 갖고있게 하고
        private val delegateProperty = LazyInitProperty{
            Thread.sleep(2_000)
            "JS"
        }
    
        //실제 name 은 delegateProperty 를 통해 갖고오도록 설정
        val name : String
            get()= delegateProperty.value   //Person 의 getter 가 호출되면 곧바로 LazyInitProperty 의 getter 를 호출
    }
    
    //위 코드를 "by" 로 간결하게 만들 수 있다. 
    class Person {
        //name : 위임 프로퍼티, Lazy() : 위임 객체
        val name: String by lazy {	// by 뒤에 위치한 클래스는 getValue 혹은 setValue 함수를 갖고있어야 한다.
            Thread.sleep(2_000)		// 즉 lazy 함수에는 getValue 가 제공되어있다.
            "JS"
        }
    }

     

     

    Kotlin 의 by 로 위임해주는 코드 변화

    //before
    class C {
        var prop: Type by MyDelegate()
    }
    
    //after
    class C {
        private val prop$delegate = MyDelegate()	//private 변수를 내부에 만들어두고
        var prop : Type
        //위임 프로퍼티의 getter / setter 가 호출될때 getVlaue / setValue 를 호출한다.
            get() = prop$delegate.getValue(this, this::prop)
            set(value: Type) = prop$delegate.setValue(this, this:prop,value)
    }

    코틀린의 표준 위임 객체

    notNull()

    • 데이터의 nullability를 처리하는 데 사용
    • Kotlin은 null 안전성을 언어 차원에서 지원하며, 이를 통해 null 포인터 예외(NullPointerException)를 피할 수 있음
    • notNull() 함수는 주로 kotlin.properties.Delegates 객체의 일부로 제공되며, 위임된 속성의 값을 초기화하지 않고 null이 아닌 값을 보장할 때 유용
    class Person{
        var age: Int by notNull()	//lateinit과 비슷한 역할
    }

    observable()

    • observable() 함수는 속성 위임을 통해 속성 값의 변화를 감지하고, 값이 변경될 때마다 특정 동작을 수행할 수 있도록 함
    • 객체의 상태 변화를 추적하고 반응할 필요가 있을 때 사용
    • 속성 위임을 사용하여 속성 값이 변경될 때마다 호출되는 콜백을 설정
    • 이 콜백은 속성 값이 변경되기 전의 값과 새로운 값을 매개변수로 받아, 값을 검증하거나, 로그를 남기거나, UI를 갱신하는 등의 작업을 수행
    class Person{
        var age: Int by observable(20) {_, oldValue, newValue ->
            println("옛날 값 : $oldValue , 새로운 값 : $newValue")
        }
    }
    
    fun mail(){
        val p = Person()
        p.age = 30      //옛날 값 : 20 , 새로운값 : 30
        p.age = 30      //옛날 값 : 30 , 새로운값 : 30
    }

     

    vetoable()

    • setter가 호출될 때 onChange 함수가 true 를 반환하면 변경 적용 false 를 반환하면 이전 값이 그대로 남는다.
    class Person{
        var age: Int by vetoable(20) {_,_, newValue -> newValue >= 1} //1 이상인 경우에만 적용되도록 설정 즉 -10 이면 미적용
    }

    또 다른 프로퍼티로 위임하기

    class Person{
        @Deprecated("age를 사용하세요!",ReplaceWith("age"))
        var num: Int = 0
        
        //코드 사용자들이 age 로 코드를 바꾸면 그때 num 을 제거
        var age: Int by this::num
    }

    Map

    //getter 가 호출되면["name"] 또는 map["age"] 를 찾는다.
    class Person(map: Map<String,Any>){
        val name: String by map
        val age : Int by map
    }
    
    fun main(){
        var person = Person(mapOf("name" to "ABC"))
        println(person.name)
        println(person.age)     //예외 발생
    }

    Iterable Sequence (feat. JMH)

    우리는 데이터를 조작할 때 Collection 을 사용한다.

    Iterable

    ex. 2,000,000 개의 랜덤 과일 중 사과를 골라 10,000개의 가격 평균을 계산해보자!

    fun main(){
        val fruits = listOf(
            Fruit("사과",1000L),
            Fruit("바나나", 3000L),
        )
        val avg = fruits
            .filter {it.name == "사과"}       //주어진 200만건의 과일중 사과를 골라 임시 List<Fruit>를 만든다.
            .map {it.price}    //List<fruit>에서 가격만 골라 List<Long>을 생성
            .take(10_000)      //List<Long> 에서 10000개를 골라
            .average()         //평균을 구한다.
    }
    
    data class Fruit(
        val name: String,
        val price: Long,
    )

    연산의 각 단계마다 중간 Collection 이 임시로 생성된다.

     

    Sequence

    중간 Collection을 만들지 않는 방법

    fun main(){
        val fruits = listOf(
            Fruit("사과",1000L),
            Fruit("바나나", 3000L),
        )
        val avg = fruits.asSequence()		//asSequence 추가
            .filter {it.name == "사과"}       //주어진 200만건의 과일중 사과를 골라 임시 List<Fruit>를 만든다.
            .map {it.price}    //List<fruit>에서 가격만 골라 List<Long>을 생성
            .take(10_000)      //List<Long> 에서 10000개를 골라
            .average()         //평균을 구한다.
    }
    
    data class Fruit(
        val name: String,
        val price: Long,
    )

    동작 원리

    • 각 단계(filter, map) 가 모든 원소에 적용되지 않을 수 있다.
    • 한 원소에 대해 모든 연산을 수행하고, 다음 원소로 넘어간다.
    • 또한, 최종연산이 나오기 전까지 계산 자체를 미리 하지 않는다.
    • 이를 지연 연산이라고 한다.

    결과

    • asSequence() 하나로 50배 정도 빠르다.
    • 하지만 수가 적으면 Iterable 이 더 빠르다.

    주의할 점

    • 연산 순서에 따라 큰 차이가 날 수 있다.

     

    'kotlin' 카테고리의 다른 글

    kotlin) 제네릭  (0) 2024.07.21
    kotlin) 복잡한 함수형 크로그래밍  (0) 2024.07.20
    kotlin) 추가적으로 알아두어야 할 코틀린 특성  (0) 2024.07.13
    kotlin) 코틀린에서의 FP  (0) 2024.07.09
    kotlin) 코틀린에서의 OOP  (0) 2024.07.06

    댓글

Designed by Tistory.