코틀린 스터디/이펙티브 코틀린

아이템21) 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라

막이86 2024. 8. 8. 10:34
728x90
  • 코틀린은 코드 재사용과 과련해서 프로퍼티 위임이라는 새로운 기능을 제공
    • 프로퍼티 위임을 사용하면 일반적인 프로퍼티의 행위를 추출해서 재사용 가능
  • 대표적인 예로 지연 프로퍼티가 있음
    • lazy 프로퍼티는 처음 사용하는 요청이 들어올 때 초기화되는 프로퍼티를 의미
  • 코틀린에서는 프로퍼티 위임을 활용해 간단하게 구현할 수 있음
    • stdlib는 lazy프로퍼티 패턴을 쉽게 구현할수 있는 함수 제공
    val value by lazy { createValue() }
    
  • 변화가 있을 때 감지하는 observable 패턴을 쉽게 만들 수 있음
    • stdlib의 observable 델리게이트를 기반으로 간단하게 구현할 수 있음
    var items: List<Item> by Delegates.observable(listOf()) { _, _, _ ->
    	notifiyDataSetChanged()
    }
    
    var key: String? by Delegates.observable(null) { _, old, new -> 
    	Log.e("Key changed from $old to $new")
    }
    
  • 프로퍼티 위임 메커니즘을 활용하면, 다양한 패턴들을 만들 수 있음
    • 뷰, 리소스 바인딩, 의존성 주입, 데이터 바인딩 등
  • 자바에서는 어노테이션을 많이 활용하지만 코틀린은 프로퍼티 위임을 사용해서 구현 가능
// 안드로이드에서의 뷰와 리소스 바인딩
private val button: Button by vindView(R.id.button)
private val textSize by bindDimension(R.dimen,font_size)
private val doctor: Doctor by argExtra(DOCTOR_ARG)

// Koin에서 종속성 주입
private val persenter: MainPresenter by inject()
private val repository: NetworkRepository by inject()
private val vm: MainViewModel by viewModel()

// 데이터 바인딩
private val port by bindConfiguration("port")
private val token: String by preferences.bind(TOKNE_KEY)
  • 프로퍼티가 사용될 때 간단한 로그를 출력하는 예제
    • 타입이 다르지만, 내부적으로는 거의 같은 처리를 함
    var token: String? = null
    	get() {
    		print("token returned value $field")
    	}
    	set(value) {
    		print("token changed from $field to $value")
    	}
    
    var attempts: Int = 0
    	get() {
    		print("attempts returnd value $field")
    	}
    	set(value) {
    		print("attempts changed from $field to $value")
    		field = value
    	}
    
  • 프로퍼티 위임을 활용해 변경한 예제
var token: String? = null
	get() {
		print("token returned value $field")
	}
	set(value) {
		print("token changed from $field to $value")
	}

var attempts: Int = 0
	get() {
		print("attempts returnd value $field")
	}
	set(value) {
		print("attempts changed from $field to $value")
		field = value
	}
  • 프로퍼티 위임이 어떻게 동작하는지 이해하려면, by가 어떻게 컴파일되는지 보는것이 좋음
    • 컨텍스트(this)와 프로퍼티 레퍼런스의 경계도 함께 사용하는 형태로 바뀜
    • 프로퍼티에 대한 레퍼런스는 이름, 어노테이션과 관련된 정보를 얻을 때 사용됨
    @JvmField
    private val `token$delegate` = LoggingProperty<String?>(null)
    var token: String?
    	get() = `token$delegate`.getValue(this, ::token)
    	set(value) {
    		`token$delegate`.setValue(this, ::token, value)
    	}
    
  • getValue와 setValue 메서드가 여러개 있어도 컨텍스트를 활용하므로, 상황에 따라서 적절한 메서드가 선택됨
    • 컨텍스트의 종류에 따라서 적절한 메서드가 선택되게 만들 수 있음
    class SwipeRefreshBinderDelegate(val id: Int) {
    	private var cache: SwipeRefreshLayout? = null
    
    	operator fun getValue(activity: Activity, prop: KProperty<*>): SwipeRefreshLayout {
    		return cache ?: activity.findViewById<SwipeRefreshLayout>(id).also { cache = it }
    	}
    
    	operator fun getValue(fragment: Fragment, prop: KProperty<*>): SwipeRefreshLayout {
    		return cache ?: fragment.view.findViewById<SwipeRefreshLayout>(id).also { cache = it }
    	}
    }
    
  • 객체를 프로터피 위임하려면 val의 경우 getValue 연산
    • var의 경우 getValue, setValue 연산이 필요
  • 멤버 함수로도 만들 수 있지만, 확장 함수로 만들어 사용 가능
val map: Map<String, Any> = mapOf("name" to "Marcin", "KotlinProgrammer" to true)
val name by map
print(name)
  • 코틀린 stdlib에 확장 함수가 정의 되어 있음
inline operator fun <V, V1 : V> Map<in String, V>.getValue(thisRef: Any?, property:KProperty<*>): V1 = getOrImplicitDefault(property.name) as V1
  • 코틀린 stdlib에 프로퍼티 델리게이터를 알아 두면 좋음
    • lazy
    • Delegates.observable
    • Delegates.vetoable
    • Delegates.notNull
  • 법용적으로 사용되는 패턴들에 대한 프로퍼티 델리게이터이므로 알아 두면 좋음

정리

  • 프로퍼티 델리게이트는 프로퍼티와 관련된 다양한 조작을 할 수 있으며, 컨텍스트와 관련된 대부분의 정보를 갖음
  • 프로퍼티의 동작을 추출해서 재사용할 수 있음
    • 표준 라이브러리의 lazy와 observable이 대표적인 예
  • 프로퍼티 위임은 프로퍼티 패턴을 추출하는 일반적인 방법
728x90