Kotlin

Kotlin에서의 Delegation Pattern(2) - Delegated Properties

seokzoo 2022. 9. 13. 23:57
반응형

1편에서는 클래스에서 Delegation Pattern을 적용했다면, 이번에는 변수에 Delegation Pattern을 적용하는 방법을 알아보겠습니다.

Delegated properties

class Xample {
    var varValue: String by Delegate()
    val valValue: String by Delegate2()
}

getValue(), setValue()

이와 같이 변수를 선언한 뒤 자료형 뒤에 by와 함께 클래스를 적어주면, 해당 클래스에서 구현해줄 함수가 생깁니다. var 변수의 경우에는 getValue(), setValue()를 구현해주어야 하고, val 변수에 대해서는 getValue()만 구현해주면 됩니다.

import kotlin.reflect.KProperty

class Delegate {
    operator fun setValue(thisRef: Sample, property: KProperty<*>, value: String) {
        TODO("Not yet implemented")
    }

    operator fun getValue(thisRef: Sample, property: KProperty<*>): String {
        TODO("Not yet implemented")
    }

}

class Delegate2 : ReadOnlyProperty<Sample, String> {
    override operator fun getValue(thisRef: Sample, property: KProperty<*>): String {
        TODO("Not yet implemented")
    }
}

이제 이 코드를 setValue와 getValue를 작성한 뒤 사용해주면 됩니다.

operator fun getValue(thisRef: Sample, property: KProperty<*>): String {
        return "del1 getvalue"
    }

    operator fun setValue(thisRef: Sample, property: KProperty<*>, s: String) {
        println("$s")
    }

fun main() {
    val e = Sample()
    e.varValue = "3"
    print(e.varValue)
}

=> 3
   del1 getvalue

구현부

그렇다면 이러한 getValue(), setValue()는 어디서 왔을까요? 바로 kotlin.properties에 있는 ReadOnlyProperty와 ReadWriteProperty에서 만들어졌는데, 이는 인터페이스로 만들어져있습니다. (Delegate1 처럼 선언하는 경우 override가 없고, Delegate2 처럼 구현을 하는 경우 override를 붙여줍니다. 실행은 동일합니다.)

ReadOnlyProperty(val 변수)의 구현부를 보면 getValue만 가지고 있습니다.
thisRef에는 property owner의 superType이거나 같아야하고, property는 KProperty<*>타입이거나 이것의 superType이여야 합니다.

ReadWritePropery(var 변수)의 구현부를 보면 getValue, setValue를 가지고 있습니다.
thisRef와 property는 위와 동일하며, value에는 property와 같거나 서브타입이여야 합니다.

이런식으로 변수에서 또한 Delegation Pattern을 이용할 수 있습니다!

by Lazy

lazy또한 자주 쓰이는 패턴이기 때문에 정리를 해보겠습니다.
by 키워드를 통해 lazy 함수에 객체 생성을 위임하겠다는 의미입니다.

class MainActivity : AppCompatActivity() {
    private val messageView : TextView by lazy {
        // messageView의 첫 액세스에서 실행됩니다
        findViewById(R.id.message_view) as TextView
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
    fun messageText() {
        messageView.text = "message Text"
    }
}

위의 예제는 TextView를 Lazy Pattern을 통해 이용하는 방법입니다.
Delegate Pattern의 형식은 아래와 같습니다.

val/var <property name>: <Type> by <delegate>

이를 이용해 위의 lazy 선언부를 다시 보겠습니다!

val messageView: <TextView> by lazy { findViewById(R.id.message_view) as TextView }

이러한 lazy는 람다를 받아 저장한 Lazy의 인스턴스를 반환합니다. 그리고 최초 getter()의 실행은 lazy에 넘겨진 람다를 모두 실행한 뒤, 결과를 기록하고 후에 getter는 기록된 값만 반환해 줍니다.
이를 코드를 통해 확인해 보겠습니다!

class Demo {
    val myName : String by lazy { "John" }
}

이 코드를 디컴파일 해보면 아래와 같이 나옵니다.

public final class Demo {
   @NotNull
**   private final Lazy myName$delegate;
**
   @NotNull
   public final String getMyName() {
      Lazy var1 = this.myName$delegate;
      Object var3 = null;
      boolean var4 = false;
      **return (String)var1.getValue();**
   }

   public Demo() {
      this.myName$delegate = **LazyKt.lazy((Function0)null.INSTANCE);**
   }
}
  • myName에 $delegate를 붙인 필드가 생성됩니다.
  • myName의 타입이 String이 아닌 Lazy입니다.
  • getter에서 getValue를 수행합니다.
  • 생성자를 통해 LazyKt.lazy()를 실행해 myName$delegate를 할당해줍니다.

즉 객체의 생성을 lazy()에 위임하고, lazy()는 getValue 메소드가 들어있는 lazy<T>인스턴스를 반환해줍니다.

myName이라는 객체의 생성을 Lazy<T>에서 담당하게 만드는 by lazy()는 Delegate pattern이라고 볼 수 있다.

lazy()함수 구현부


lazy는 초기화 수행할 람다 함수를 쓰레드 실행 모드에 따라 조금씩 다른 방식으로 처리하는 객체를 반환합니다.
SYNCHRONIZED는 초기화가 최초 호출되는 단 하나의 쓰레드에서만 처리됩니다. 기본 값이며 다른 쓰레드는 이후에 그 값을 그대로 참조해줍니다.
PUBLICATION은 여러 쓰레드에서 동시에 호출될 수 있으며, 초기화도 모든 혹은 일부의 쓰레드들에서 동시에 실행이 가능합니다. 다만 다른 쓰레드에서 이미 초기화가 됐다면 별도 초기화 없이 그 값을 반환합니다.
NONE은 초기화 되지 않은 경우 무조건 초기화를 시행하여 값을 기록합니다. 멀티 스레딩에서는 NPE 발생 가능성이 있어 안전하지 않습니다.

실행 과정

실행 과정은 참고 블로그에서 너무 잘 설명되어 있어 그림을 가져왔습니다!

1. 전달된 초기화 람다를 initializer 프로퍼티에 저장합니다.

2. _value 프로퍼티를 통해 값을 저장할 것이지만, 초기화 전이므로 이 프로퍼티의 초기값은 UNINITIALIZED_VALUE 입니다.

3. 읽기 접근이 일어나 _value의 값이 UNINITIALIZED_VALUE라면 초기화가 되지 않은 상태로 판단, 초기화 블록을 실행합니다.

4. 3번이 수행됐다면 값이 UNINITIALIZED_VALUE가 아니기 때문에 이후 읽기 접근에 대해서 _value의 값이 반환됩니다.

초기화 블럭

 private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return **synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }**
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

초기화 블럭을 보자면 먼저 synchronized()를 통해 초기화 블럭을 실행해줍니다.

단 다른 쓰레드에서 synchronized() 블록에 들어가 이미 초기화가 끝났을 수도 있기 때문에 _value의 UNINITIALIZED_VALUE 여부를 체크하여 값이 있다면 해당 값을 반환합니다.

초기화가 되어있지 않다면, 람다식을 처리한 뒤 반환 값을 저장합니다. 그리고 초기화를 완료했기 때문에 필요없어진 initializer를 null로 처리해줍니다!

출처 및 참고

https://kotlinlang.org/docs/delegated-properties.html
https://readystory.tistory.com/204?category=815287
https://iosroid.tistory.com/72
https://medium.com/til-kotlin-ko/kotlin-delegated-property-by-lazy%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B0%80-74912d3e9c56

반응형