코틀린 & Java/코틀린인액션

delegated property 프로퍼티 접근자 로직 재활용

코딩하는후운 2023. 5. 9. 13:36
반응형

프로퍼티 접근자 로직 재활용

위임 프로퍼티(delegated property)

  • 값을 뒷받침하는 필드에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현할 수 있다.
  • 프로퍼티는 위임을 사용해 자신의 값을 필드가 아니라 데이터베이스 테이블이나 브라우저 세션, 맵 등에 저장할 수 있다.
  • 위임은 객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴을 말한다.
  • 이때 작업을 처리하는 도우미 객체를 위임 객체라고 부른다.
class Delegate {
    operator fun getValue(...) {...} //getValue는 게터를 구현하는 로직을 담는다.
    operator fun setValue(..., value: Type) {...} // setValue 메서드는 세터를 구현하는 로직을 담는다.
}

class Foo {
    var p : Type by Delegate() //"by" 키워드는 프로퍼티와 위임 객체를 연결한다.
}

>>> val foo = Foo()
>>> val oldValue = foo.p // foo.p라는 프로퍼티 호출은 내부에서 delegate.getValue(...)를 호출한다.
>>> foo.p = newValue //프로퍼티 값을 변경하는 문장은 내부에서 delegate.setValue(..., newValue)를 호출한다.

by lazy()를 사용한 프로퍼티 초기화 지연

  • 지연 초기화(lazy initialization)는 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 흔히 쓰이는 패턴이다.
  • 초기화 과정에서 자원을 많이 사용하거나 객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 지연 초기화 패턴을 사용할 수 있다.
class Email {/*...*/}
fun loadEmails(person: Person) : List<Email> {
    println("Load emails for ${person.name}")
    return listOf(/*...*/)
}

지연 초기화를 뒷받침하는 프로퍼티를 통해 구현하기

class Person(val name : String){
    private var _emails: List<Email>? = null // 데이터를 저장하고 emails의 위임 객체 역할을 하는 _emails프로퍼티
    val emails: List<Email>
        get() {
            if (_emails == null) {
                _emails = loadEmails(this) // 최초 접근 시 이메일을 가져온다.
            }
            return _emails!! // 저장해 둔 데이터가 있으면 그 데이터를 반환한다.
        }
}

>>> val p = Person("Alice")
>>> p.emails
Load emails for Alice
>>> p.emails
  • _emails 라는 프로퍼티는 값을 저장하고, 다른 프로퍼티인 emails는 _emails라는 프로퍼티에 대한 읽기 연산을 제공한다.
  • 이런 코드를 만드는 일은 성가시고, 스레드 안전하지 않아서 언제나 제대로 작동한다고 말할 수도 없다.
  • 위임 프로퍼티를 사용하면 이 코드가 훨씬 더 간단해진다.
  • 위임 프로퍼티는 데이터를 저장할 때 쓰이는 뒷받침하는 프로퍼티와 같이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화 해준다

지연 초기화를 위임 프로퍼티를 통해 구현하기

class Person(val name: String){
    val emails by lazy { loadEmails(this) }
}

 

위임 프로퍼티 구현

어떤 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내고 싶다.
PropertyChangeSupport를 사용하기 위한 도우미 클래스

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)
    fun addPropertyChangeListenter(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListenter(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }		
}

코틀린 인액션 책

리스트 7.20 프로퍼티 변경 통지를 직접 구현하기

  • 세터 코드를 보면 중복이 많이 보인다.
  • 프로퍼티의 값을 저장하고 필요에 따라 통지를 보내주는 클래스를 추출해보자.

리스트 7.21 도우미 클래스를 통해 프로퍼티 변경 통지 구현하기

  • 프로퍼티 값을 저장하고 그 값이 바뀌면 자동으로 변경 통지를 전달해주는 클래스를 만들었고, 로직의 중복을 상당 부분 제거했다.
  • 하지만 아직도 각각의 프로퍼티마다 ObservableProperty를 만들고 게터와 세터에서 ObservableProperty에 작업을 위임하는 준비코드가 상단 부분 필요하다.

리스트 7.22 ObservableProperty를 프로퍼티 위임에 사용할 수 있게 바꾼 모습

  • getValue와 setValue 함수에도 operator변경자가 붙는다.
  • getValue와 setValue는 프로퍼티가 포함된 객체와 프로퍼티를 표현하는 객체를 파라미터로 받는다.
  • KProperty 인자를 통해 프로퍼티 이름을 전달받으므로 주 생성자에서는 name프로퍼티를 없앤다.

리스트 7.23 위임 프로퍼티를 통해 프로퍼티 변경 통지 받기

  • by 키워드를 사용해 위임 객체를 지정하면 이전 예제에서 직접 코드를 짜야 했던 여러 작업을 코틀린 컴파일러가 자동으로 처리해준다.

리스트 7.24 Delegates.observable을 사용해 프로퍼티 변경 통지 구현하기

class Person (
		val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
	private val observer = {
		prop: KProperty<*>, oldValue: Int, newValue: Int -> 
		changeSupport.firePropertyChange(prop.name, oldValue, newValue)
	}
	var age: Int by Delegates.observable(age, observer)
	var salary: Int by Delegates.observable(salary, observer)
}
  • by의 오른쪽에 있는 식이 꼭 새 인스턴스를 만들 필요는 없다.

 

위임 프로퍼티 컴파일 규칙

class C {
    var prop: Type by MyDelegate()
}
val c = C()
  • 컴파일러는 MyDelegate 클래스의 인스턴스를 감춰진 프로퍼티에 저장하며 그 감춰진 프로퍼티를 <delegate>라는 이름으로 부른다.
  • 또한 컴파일러는 프로퍼티를 표현하기 위해 KProperty 타입의 <property>객체를 사용한다.
  • 컴파일러는 다음 코드를 생성한다.
class C {
    private val <delegate> = MyDelegate()
    var prop: Type
    get() = <delegate>.getValue(this, <property>)
    set(value: Type) = <delegate>.setValue(this, <property>, value)
}

 

프로퍼티 값을 맵에 저장

val name : String
get() = _attributes["name"]!! // 수동으로 맵에서 정보를 꺼낸다

val name : String by _attributes // 위임 프로퍼티로 맵을 사용한다

이런 코드가 작동하는 이유는 표준 라이브러리가 Map과 MutableMap 인터페이스에 대해 getValue와 setValue 확장 함수를 제공하기 때문이다.

프레임워크에서 위임 프로퍼티 활용

object Users : IdTable() { //객체는 데이터베이스 테이블에 해당한다.
    val name = varchar("name", length = 50).index()// 프로퍼티는 테이블 칼럼에 해당한다.
    val age = integer("age")
}

class User(id: EntityID) : Entity(id) { // 각 User 인스턴스 테이블에 들어있는 구체적인 엔티티에 해당한다.
    val name: String by Users.name //사용자 이름은 데이터베이스 "name" 칼럼에 들어있다.
    val age: Int by Users.age
}

 

반응형