반응형
프로퍼티 접근자 로직 재활용
위임 프로퍼티(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
}
반응형
'코틀린 & Java > 코틀린인액션' 카테고리의 다른 글
인라인 함수: 람다의 부가 비용 없애기 (0) | 2023.05.23 |
---|---|
고차 함수 정의 (0) | 2023.05.23 |
Kotlin 구조 분해 선언과 component 함수(Pair, Triple) (0) | 2023.05.09 |
Kotlin 범위 관례 인덱스로 원소 접근 Get, Set (0) | 2023.05.09 |
Kotlin == 비교연산자 오버로딩 (0) | 2023.05.09 |