[Kotlin] Delegated Property와 Class Delegation

silver·2022년 11월 16일
0

Delegated Property

프로그램을 작성하다 보면 Int 타입의 프로퍼티에 음수가 저장되는 것을 방지하는 Setter를 정의할 때가 자주있다.

var age: Int set(value) if(value >= 0) field = value
var salary: Int set(value) if(value >= 0) field = value

그러나 이렇게 모든 프로퍼티의 Setter를 일일이 정의하는 것은 너무 번거롭다.

코틀린에서는 이런 상황을 위해 프로퍼티의 Getter/Setter 구현을 다른 객체에 맡길 수 잇는 문법을 제공한다.

class Sample {
	var number: Int by OnlyPositive()
}

프로퍼티 선언문 뒤에 by 객체를 적으면 해당 객체가 프로퍼티의 Getter/Setter를 대리하게 된다.

import kotlin.reflect.KProtperty

class OnlyPositive {
	private var realValue: Int = 0
    
    operator fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    	return realValue
    }
    
    operator fun setValue(thisRef: Any?, property: KPorperty<*>, value: Int) {
    	realValue = if(value>0) value else 0
    }
}

프로퍼티를 대리하는 객체는 operator fun getValue(this:Ref: Any?, property: KProperty<*>): T와 operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) 멤버 함수를 갖고 있어야 한다. 여기서 T는 대리할 프로퍼티의 타입이다.

Sample 클래스의 number 프로퍼티의 Getter는 OnlyPositive의 getValue로 대체되고, Setter는 OnlyPositive의 setValue로 대체된다.

fun main(args: Array<String>) {
	fun sample = Sample()
    
    sample.number = -50
    println(sample.number)
    
    sample.number = 100
    println(sample.number)
}

sample.number = -50이 실행되는 순간 OnlyPositivie의 setValue가 호출되며 OnlyPositive의 realValue에 0이 저장된다.
sample.number가 실행되는 순간에는 OnlyPositivie의 getValue가 호출되며 0이 출력된다.

출력결과

0
100

Class Delegation

코틀린에서는 인터페이스의 구현을 다른 클래스에 맡길 수 있는 문법도 제공한다.

interface Plusable{
	operator fun plus(other: Int): Int
}

Int 타입과 덧셈을 가능하게 하는 인터페이스를 선언하고 있다.

class ClassDelegator: Plusable {
	override fun plus(other: Int): Int {
    	println("기본 구현")
        return other
    }
}

Plusable 인터페이스를 구현하는 클래스를 선언하고 있다.

class Sample: Plusable by ClassDelegator()

인터페이스를 구현하면서 뒤에 by 객체를 지정하면 인터페이스의 구현을 해당 객체로 위임한다. 이때 객체는 대리할 인터페이스를 구현하고 있어야 한다.

앞으로 Sample의 plus 연산자 멤버 함수를 호출하면 ClassDelegator의 plus가 호출된다.

fun main(args: Array<String>) {
	println(Sample() + 10)
}

Sample() + 10을 수행하면 ClassDelegation의 plus가 호출된다.

출력 결과

기본 구현
10

by lazy

by lazy는 by 키워드를 활용한 Properties에서의 활용이다.
by lazy{}를 활용한 예제는 아래와 같다.

private val viewModel: MainViewModel by lazy {
	MainViewModel() 
}

lazy의 기본코드는 아래와 같다.

public actual fun<T> lazy(initialize: () -> T): Lazy<T> = SynchronizedLazyImpl(initialize)

결국 by이후에 오는 lazy에게 프로퍼티 생성을 위임하고, lazy의 내부 동작에 따라 코드를 초기화 한다.

lazy의 최상위는 interface로 구성되어 있고, 프로퍼티인 value와 함수인 isInitialized로 구성되어 있다. 결국 value는 Properties를 getter로 구성해 값을 리턴하는데, 이때 lazy 패턴을 활용하는 형태로 구성되어 있다.

public interface Lazy<out T> {
	public val value: T
    public fun isinitialized(): Boolean
}

SynchronizedLazyImple은 by lazy를 사용하면 가장 기본으로 생성된다. 내부적으로 값이 호출되기 전에는 temp 값을 담을 수 있도록 만들고, 외부에서는 value를 호출하면 value 안에 있는 get()에서 이를 늦은 처리하도록 한다.

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null): Lazy<T>, Serializable {
	// 생략
    
    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 !== UNITIALIZED_VALUE
    
    //생략
}

결국 by lazy 호출 시 lazy에게 위임해 내부 코드의 동작에 따라 delegation을 처리하는 것을 알 수 있다.

Android 게으른 초기화의 유용성
게으른 초기화는 프로퍼티가 힙 영역에 할당되는 순간 바로 이루어지는 것이 아니기 때문에 다음과 같은 상황에 유용하다.

val view: View? by lazy { findViewById(R.id.view) }

액티비티의 findViewById는 setContentView가 호출되기 전에 무조건 null을 반환한다. 따라서 프로퍼티를 선언과 동시에 초기화하면 안 된다. 그러나 프로퍼티를 val로 선언하고 싶으면 반드시 선언과 동시에 초기화해야 한다. 이때, 프로퍼티의 초기화를 Lazy<T>에 위임하고, setContentView를 수행하고 난 뒤 view 프로퍼티에 접근하면, 그제서야 view 프로퍼티를 findViewById(R.id.view)로 초기화하므로 제대로 View 인스턴스를 가져 올 수 있다.

참고
https://thdev.tech/kotlin/2020/11/27/kotlin_delegation/

0개의 댓글