이펙티브 코틀린 Item 21: 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라

woga·2023년 6월 18일
0

코틀린 공부

목록 보기
24/54
post-thumbnail

코틀린은 코드 재사용과 관련해서 프로퍼티 위임이라는 새로운 기능을 제공한다.
프로퍼티 위임을 사용하면 일반적인 프로퍼티의 행위를 추출해서 재사용할 수 있다.
ex) lazy

일반적으로 대부분의 언어(자바 스크립트 등)에서는 필요할 때마다 이를 복잡하게 구현해야 하지만, 코틀린에서는 프로퍼티 위임을 활용해 간단하게 구현할 수 있다. 코틀린의 stdliblazy 프로퍼티 패턴을 쉽게 구현할 수 있게 lazy 함수를 제공한다.

val value by lazy { createValue() }

프로퍼티 위임을 사용하면, 이외에도 변화가 있을 때 이를 감지하는 observable 패턴을 쉽게 만들 수 있다. 예를 들어 목록을 출력하는 리스트 어댑터가 있다면, 내부 데이터가 변경될 때마다 변경된 내용을 다시 출력해야 할 것이다. 또한 프로퍼티의 변경 사항을 로그로 출력하고 싶은 경우도 있을 것이다.
이러한 것들은 다음과 같이 obaservable 델리게이트를 기반으로 간단하게 구현할 수 있다.

var items: List<Item> by
	Delegates.observable(listOf()) { _, _, _ ->
    	notifyDataSetChanged()
 }
 
var key: String? by
	Delegates.observable(null) { _, old, new ->
    	Log.e("key changed from $old to $new")
}

lazy와 observable 델리게이터는 언어적인 관점에서 특별하는 것은 아니다. 일반적으로 프로퍼티 위임 매커니즘을 활용하면 다양한 패턴들을 만들 수 있다.
ex) 뷰, 리소스 바인딩, 의존성 주입, 데이터 바인딩 등

이런 패턴들 쓸 때 자바에서는 어노테이션을 많이 활용하는데 코틀린은 간단하고 Type-safe하게 구현할 수 있다.

// android view & resource binding
private val button: Button by bindView(R.id.button)
private val textSize by bindDimension(R.dimen.font_size)
private val doctor: Doctor by argExtra(DOCTOR_ARG)

// inject in kotlin
private val presenter: MainPresenter by inject()
private val repository: NetworkRepository by inject()
private val vm: MainViewModel by viewModel()

// databinding
private val port by bindConfiguration("port")
private val token: String by preferences.bind(TOKEN_KEY)

어떻게 이런 코드가 가능하고 프로퍼티 위임을 활용할 수 ㅜ있을까?

예를 들어 일부 프로퍼티가 사용될 떄, 간단한 로그를 출력하고 싶다고 해보자.

var token: String? = null
	get() {
    	print("token returned value $field")
        return field
    }
    set(value) {
    	print("token changed from $field to $value")
        field = value
    }
    
var attempts: Int = 0
	get() {
    	print("attempts returned value $field")
        return field
    }
    set(value) {
    	print("attempts changed from $field to $value")
        field = value
    }

두 프로퍼티 타입이 다르지만, 내부적으로 거의 같은 처리를 한다. 또한 프로젝트에서 자주 반복될 거 같은 패턴처럼 보인다.
따라서 프로퍼티 위임을 활용해서 추출하기 좋은 부분이다. 프로퍼티 위임은 다른 객체의 메서드를 활용해서 프로퍼티의 접근자(게터와 세터)를 만드는 방식이다. 이때 다른 객체의 메서드 이름이 중요한데요 게터는 getValue, 세터는 setValue 함수를 사용해서 만들어야 한다. 객체를 만든 뒤에는 by 키워드를 사용해서, getValue와 setValue를 정의한 클래스와 연결해주면 된다. 다음 코드는 이전에 살펴본 코드를 프로퍼티 위임을 활용해 변경한 예이다.

var token: String? by LoggingProperty(null)
var attempts: Int by LoggingProperty(0)

private class LoggingProperty<T>(var value: T) {
	operator fun getValue(
    	thisRef: Any?,
        prop: KProperty<*>
    ): T {
    	print("${prop.name} returned value $value")
        return value
    }
    
    operator fun setValue(
    	thisRef: Any?,
        prop: KProperty<*>,
        newValue: T
    ) {
    	val name = prop.name
        print("$name changed from $value to $newValue")
        value = newValue
    }
}

프로퍼티 위임이 어떻게 동작하는지 이해하려면 by가 어떻게 컴파일되는지 보는 것이 좋다. 위의 코드에서 token 프로퍼티는 다음과 비슷한 형태로 컴파일 된다.

@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)
    }

코드를 보면 알 수 있는 것처럼 getValuesetValue는 단순하게 값만 처리하게 바뀌는 것이 아니라 컨텍스트(this)와 프로퍼티 레퍼런스의 경계도 함께 사용하는 형태로 바뀐다. 프로퍼티에 대한 레퍼런스는 이름, 어노테이션과 관련된 정보 등을 얻을 때 사용된다. 그리고 컨텍스트는 함수가 어떤 위치에서 사용되는지와 관련된 정보를 제공한다.

이러한 정보로 인해 getValuesetValue 메서드가 여러 개 있어도 문제 없다. 이 메서드들은 여러 개 있어도 컨텍스트를 활용하므로 상황에 따라서 적절한 메서드가 선택된다. 이는 굉장히 다양하게 활용된다. 예를 들어 여러 종류의 뷰와 함께 사용할 수 있는 델리게이트가 필요한 경우를 생각해보자. 이는 다음과 같이 구현해서 컨텍스트의 종류에 따라서 적절한 메서드가 선택되게 만들 수 있다.

class SwipeRefreshBinderDelegate(Val id: Int) {
	private var cache: SwipeRefreshLayout? = null
    
    operator fun getValue(
    	activity: Activity,
        prop: KProperty<*>
    ): SwipeRefreshLayout {
    return cache ?: activity
    		.finViewById<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 연산이 필요하다. 이러한 연산은 지금까지 살펴본 거처럼 멤버 함수로도 만들 수 있지만 확장 함수로도 만들 수 있다. 예를 들어 다음 코드는 Map<String, *>를 사용하는 예이다.

val map: Map<String, Any> = mapOf(
	"name" to "Marcin",
    "kotlinProgrammer" to true
)
val name by map
print(name) // Marcin

이는 코틀린 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이 대표적인 예이다.

프로퍼티 위임은 프로퍼티 패턴을 추출하는 일반적인 방법이라 많이 사용된다 따라서 코틀린 개발자라면 프로퍼티 위임이라는 강력한 도구를 잘 알고 있자.
일반적인 패턴을 추출하거나 더 좋은 API를 만들 때 활용할 수 있다!

profile
와니와니와니와니 당근당근

0개의 댓글