728x90
반응형
✅ 프로퍼티 접근자 로직 재활용 : 위임 프로퍼티
- 코틀린이 제공하는 관례에 의존하는 특성 중 독특하면서 강력한 기능으로 위임 프로퍼티(delegated property)가 있다.
- 위임 프로퍼티를 사용하면 값을 뒷받침하는 필드에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현할 수 있다.
- 이러한 과정에서 접근자 로직을 매번 재구현할 필요가 없다.
- 예를 들어 프로퍼티는 위임을 사용해 자신의 값을 필드가 아닌 데이터베이스 테이블이나 브라우저 세션, 맵 등에 저장할 수 있다.
- 이러한 특성의 기반에는 위임이 있다.
- 위임은 객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴이다.
- 이때 작업을 처리하는 도우미 객체를 위임 객체(delegate)라 부른다.
- 이번 절에서는 위임 패턴을 프로퍼티에 적용하여 접근자 기능을 도우미 객체가 수행하게 위임한다.
- 도우미 객체를 직접 작성할 수도 있지만 더 나은 방법으로 코틀린 언어가 제공하는 기능을 활용하는 것이다.
- 이번 절을 통해 위임 프로퍼티에 대해 알아보고, 위임과 관련된 내용을 확인하고 싶다면 아래 글들을 참고하길 바란다.
📌 위임 프로퍼티 소개
- 위임 프로퍼티의 일반적인 문법은 다음과 같다.
class Foo {
var p: Type by Delegate()
}
- p 프로퍼티는 접근자 로직을 다른 객체에게 위임한다.
- 위 코드에서는 Delegate 클래스의 인스턴스를 위임 객체로 사용한다.
- by 뒤에 있는 식을 계산하여 위임에 쓰일 객체를 얻는다.
- 프로퍼티 위임 객체가 따라야 하는 관례를 따르는 모든 객체를 위임에 사용할 수 있다.
- 프로퍼티 위임 관례를 따르는 Delegate 클래스는 getValue와 setValue 메소드를 제공해야 한다.
- Delegate 클래스를 단순화하면 아래와 같다.
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)를 호출한다.
- 위의 예제에서 foo.p는 일반 프로퍼티처럼 쓸 수 있고, 일반 프로퍼티처럼 보인다.
- 하지만 실제 p의 게터나 세터는 Delegate 타입의 위임 프로퍼티 객체에 있는 메소드를 호출한다.
- 실제로 이런 구조가 어떻게 작동하는지 살펴보기 위해 코틀린의 지연 초기화(by lazy()) 에제를 살펴보자.
📌 위임 프로퍼티 사용 : by lazy()를 사용한 프로퍼티 초기화 지연
- 지연 초기화(lazy initalization)는 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요한 경우 초기화할 때 흔히 쓰이는 패턴이다.
- 초기화 과정에 자원을 많이 사용하거나 객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 지연 초기화 패턴을 사용할 수 있다.
- 예를 들어 person 클래스가 자신이 작성한 이메일의 목록을 제공한다고 할 때, 이메일은 데이터베이스에 들어있고 불러오려면 시간이 오래 걸린다.
- 따라서 이메일 프로퍼티의 값을 최초로 사용할 때 단 한 번만 이메일을 데이터베이스에서 가져오고 싶다고 해보자.
class Email { /* ... */ }
fun loadEmails(person: Person): List<Email> {
println("${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 // 최초로 emails를 읽을 때 단 한 번만 이메일을 가져옴
// Load emails for Alice
- 위 예제의 경우 뒷받침하는 프로퍼티(backing property)라는 기범을 사용하였다.
- _emails라는 프로퍼티는 값을 저장하고, 다른 프로퍼티인 emails는 _emails라는 프로퍼티에 대한 읽기 연산을 제공한다.
- _emails는 널이 될 수 있는 타입인 반면 emails는 널이 될 수 없는 타입이므로 프로퍼티를 두 개 사용해야 한다.
- 이러한 기법은 자주 사용되기 때문에 알아두는 것이 좋다.
- 하지만 이런 코드를 만드는 것은 약간 성가신 작업일 수 있다.
- 지연 초기화해야 하는 프로퍼티가 많아지면 코드가 복잡해게 된다.
- 도한 이러한 구현은 스레드 안전하지 않아 언제나 제대로 작동한다고 볼 수도 없다.
- 여기서 코틀린은 더 나은 해법을 제공한다.
- 위임 프로퍼티를 사용할 경우 위의 코드를 훨씬 더 간단하게 작성할 수 있다.
- 위임 프로퍼티는 데이터를 저장할 때 쓰이는 뒷받침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화해준다.
- 이러한 경우를 위한 위임 객체를 반환하는 표준 라이브러리 함수가 바로 lazy이다.
/* 지연 초기화를 위임 프로퍼티를 통해 구현하기 */
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
- lazy 함수는 코틀린 관례에 맞는 시그니처의 getValue 메소드가 들어있는 객체를 반환하며 기본적으로 스레드 안전하다.
- 그리고 lazy를 by 키워드와 함께 사용해 위임 프로퍼티를 만들 수 있다.
- 이제 이러한 위임 프로퍼티가 어떻게 작동하는지 살펴보고 어떤 관례를 사용하는지 알아보자.
📌 위임 프로퍼티 구현
- 위임 프로퍼티를 구현하는 방법을 알아보기에 앞서 새로운 예제를 생각해보자.
어떤 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내고 싶다. 예를 들어 어떤 객체를 UI에 표시하는 경우 객체가 바뀌면 자동으로 UI도 바뀌어야 한다. 자바에서는 PropertyChangeSupport와 PropertyChangeEvent 클래스를 사용해 이런 통지를 처리하는 경우가 자주 있다. 이제 코틀린에서 위임 프로퍼티 없이 이런 기능을 구현하고 나중에 그 코드를 위임 프로퍼티를 사용하게 리팩토링해보자.
- PropertyChangeSupport 클래스는 리스너의 목록을 관리하고 PropertyChangeEvent 이벤트가 들어오면 목록의 모든 리스너에게 이벤트를 통지한다.
- 필드를 모든 클래스에 추가하고 싶지는 않기에 PropertyChangeSupport 인스턴스를 changeSupport라는 필드에 젖아하고 프로퍼티 변경 리스너를 추적해주는 작은 도우미 클래스를 만들어보자.
- 리스너 지원이 필요한 클래스는 이 도우미 클래스를 확장해 changeSupport에 접근할 수 있다.
/* PropertyChangeSupport를 사용하기 위한 도우미 클래스 */
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener : PropertyChangeListener) {
chageSupport.removePropertyChangeListener(listener)
}
}
- 이제 Person 클래스를 작성해보자.
- 읽기 전용 프로퍼티와 변경 가능한 프로퍼티 둘을 정의한다.
- 이 클래스는 나이나 급여가 바뀌면 그 사실을 리스너에게 통지한다.
/* 프로퍼티 변경 통지를 직접 구현하기 */
class Person(
val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
var age: Int = age
set(newValue) {
valoldValue = field // 뒷받침하는 필드에 접근할 때 "field" 식별자를 사용한다.
field = newValue
changeSupport.firePropertyChange ( // 프로퍼티 변경을 리스너에게 통지한다.
"age", oldValue, newValue)
}
var salary: Int = salary
set(newValue) {
val oldValue = field
field = newValue
changeSupport.firePropertyChange {
"salary", oldValue, newValue)
}
}
val p = Person("Dmitry", 34, 2000)
p.addPropertyChangeListener( // 프로퍼티 변경 리스너를 추가한다.
PropertyChangeListener { event ->
println("Property ${event.propertyName} changed" +
"from ${event.oldValue} to ${event.newValue}")
}
)
p.age = 35
// Property age changed from 34 to 35
p.salary = 2100
// Property salary changed from 2000 to 2100
- 위 코드는 field 키워드를 사용해 age와 salary 프로퍼티를 뒷받침하는 필드에 접근하는 방법을 보여준다.
- 하지만 세터 코드를 보면 중복이 많아 보인다.
- 이제 프로퍼티의 값을 저장하고 필요에 따라 통지를 보내주는 클래스를 추출해보자.
/* 도우미 클래스를 통해 프로퍼티 변경 통지 구현하기 */
class ObservableProperty(
val propName: String, var propValue: Int,
val changeSupport: PropertyChangeSupport
) {
fun getValue() : Int = propValue
fun setValue(newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(propName, oldValue, newValue)
}
}
class Person(
val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
val _age = ObservableProperty("age", age, changeSupport)
var age : Int
get() = _age.getValue()
set(value) { _age.setValue(value) }
val _salary = ObservableProperty("salary", salary, changeSupport)
var salary : Int
get() = _salary.getValue()
set(value) { _salary.setValue(value) }
}
- 위 코드는 코틀린의 위임이 실제로 작동하는 방식과 비슷하다.
- 프로퍼티 값을 저장하고 그 값이 바뀌면 자동으로 변경 통지를 전달해주는 클래스를 만들었고, 로직의 중복을 상당 부분 제거했다.
- 하지만 위임 프로퍼티를 사용하기 전에 ObservableProperty에 있는 두 메소드의 시그니처를 코틀린의 관례에 맞게 수정해야 한다.
/* ObservableProperty를 프로퍼티 위임에 사용할 수 있게 바꾼 모습 */
class ObservableProperty(
var propValue: Int, val changeSupport: PropertyChangeSupport
) {
operator fun getValue(p: Person, prop: KProperty<*>) : Int = propValue
operator fun setValue(p: Person, prop:KProperty<*>, newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
}
- 이전 코드와 비교해보면 다음과 같은 차이가 있다.
- 코틀린 관례에 사용하는 다른 함수와 마찬가지로 getValue와 setValue 함수에도 operator 변경자가 붙는다.
- getValue와 setValue는 프로퍼티가 포함된 객체(여기선 Person 타입인 p)와 프로퍼티를 표현하는 객체를 파라미터로 받는다.
- 코틀린 KProperty 타입의 객체를 사용해 프로퍼티를 표현한다.
- 지금은 KProperty.name을 통해 메소드가 처리할 프로퍼티 이름을 알 수 있다는 점만 기억하자.(10장에서 상세히 다룸)
- KProperty 인자를 통해 프로퍼티 이름을 전달받으므로 주 생성자에는 name 프로퍼티를 없앤다.
- 드디어 코틀린이 제공하는 위임 프로퍼티라는 마법을 사용할 수 있게 되었다.
- 위임 프로퍼티를 적용한 다음 예제를 통해 코드가 얼마나 짧아졌는지 한번 살펴보자.
/* 위임 프로퍼티를 통해 프로퍼티 변경 통지 받기 */
class Person(
val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
var age: Int by ObservableProperty(age, changeSupport)
var salary: Int by ObservableProperty(salary, changeSupport)
}
- by 키워드를 사용해 위임 객체를 지정하면 이전 예제에서 직접 코드를 짜야 했던 여러 작업을 코틀린 컴파일러가 자동으로 처리해준다.
- 이때, by 오른쪽에 오는 객체를 위임 객체(delegae)라고 부른다.
- 코틀린은 위임 객체를 감춰진 프로퍼티에 저장하고, 주 객체의 프로퍼티를 읽거나 쓸 때마다 위임 객체의 getValue와 setValue를 호출해준다.
- 이러한 위임 메커니즘은 모든 타입에 두루두루 사용할 수 있다.
📌 위임 프로퍼티 컴파일 규칙
- 위임 프로퍼티가 어떤 방식으로 동작하는지 정리해보자.
- 다음과 같은 위임 프로퍼티가 있는 클래스가 있다 가정해보자.
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)
}
- 다시 말해 컴파일러는 모든 프로퍼티 접근자 안에 아래 그림과 같이 getValue와 setValue 호출 코드를 생성해준다.
- 이 메커니즘은 상당히 단순하지만 유용하게 활용할 수 있다.
- 프로퍼티 값이 저장될 장소를 바꿀 수도 있고(맵, 데이터베이스 테이블, 사용자 세션의 쿠키 등)
- 프로퍼티를 읽거나 쓸 때 벌어질 일을 변경할 수도 있다. (값 검증, 변경 통지 등)
- 이러한 작업 모두를 간결한 코드로 달성할 수 있다.
- 다음으로 표준 라이브러리가 제공하는 위임 프로퍼티를 사용하는 방법을 하나 더 살펴보고, 자신의 프레임워크에서 위임 프로퍼티를 활용하는 방법에 대해 살펴보자.
📌 프로퍼티 값을 맵에 저장
- 자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 위임 프로퍼티를 활용하는 경우가 자주 있다.
- 그런 객체를 확장 가능한 객체라고 부르기도 한다.
- 다음 예제를 생각해보자.
연락처 관리 시스템에서 연락처별로 임의의 정보를 저장할 수 있게 허용하는 경우를 살펴보자. 시스템에 저장된 연락처에는 특별히 처리해야 하는 일부 필수 정보가 있고, 사람마다 달라질 수 있는 추가 정보가 있다.
- 이러한 시스템을 구현하는 방법 중에는 정보를 모두 맵에 저장하되 그 맵을 통해 처리하는 프로퍼티를 통해 필수 정보를 제공하는 방법이 있다.
/* 값을 맵에 저장하는 프로퍼티 정의하기 */
class Person {
// 추가 정보
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
// 필수 정보
val name : String
get() = _attributes["name"]!! // 수동으로 맵에서 정보를 꺼낸다.
}
val p = Person()
val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
for ((attrName, value) in data)
p.setAttribute(attrName, value)
println(p.name)
// Dmitry
- 위 코드는 추가 데이터를 저장하기 위해 일반적인 API를 사용하고, 특정 프로퍼티를 처리하기 위해 구체적인 개별 API를 제공한다.
- 이를 아주 쉽게 위임 프로퍼티를 활용하여 변경할 수 있다.
- by 키워드 뒤에 맵을 직접 넣기만 하면 된다.
/* 값을 맵에 저장하는 위임 프로퍼티 사용하기 */
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String by _attributes // 위임 프로퍼티로 맵을 사용한다.
}
- 이런 코드가 작동하는 이유는 표준 라이브러리가 Map과 MutableMap 인터페이스에 대해 getValue와 setValue 확장 함수를 제공하기 때문이다.
- getValue에서 맵에 프로퍼티 값을 저장할 때는 자동으로 프로퍼티 이름을 키로 활용한다.
📌 프레임워크에서 위임 프로퍼티 활용
- 객체 프로퍼티를 저장하거나 변경하는 방법을 바꿀 수 있으면 프레임워크를 개발할 때 유용하다.
- 다음 예제를 생각해보자.
데이터베이스에 User라는 테이블이 있고 그 테이블에는 name이라는 문자열 타입의 칼럼과 age라는 정수 타입의 열이 있다고 가정하자. Users와 User라는 클래스를 코틀린에서 정의할 수 있다. 그리고 데이터베이스에 들어있는 모든 사용자 엔터티를 User 클래스를 통해 가져오고 저장할 수 있다.
/* 위임 프로퍼티를 사용해 데이터베이스 칼럼 접근하기 */
object Users : IdTable() { // 객체는 데이터베이스 테이블에 해당한다.
val name = varchar("name", length = 50).index() // 프로퍼티는 테이블 칼럼에 해당한다.
val age = integer("age")
}
class User(id: EntityID) : Entity(id) { // 각 User 인스턴스는 테이블에 들어있는 구체적인 엔티티에 해당한다.
var name : String by Users.name // 사용자 이름은 데이터베이스 "name" 칼럼에 들어 있다.
var age : Integer by Users.age
}
- Users 객체는 데이터베이스 테이블을 표현한다.
- 데이터베이스 전체에 단 하나만 존재하는 테이블 표현이기 때문에 싱글턴 객체로 선언한다.
- 객체의 프로퍼티는 테이블 칼럼을 표현한다.
- User의 상위 클래스인 Entity 클래스는 데이터베이스 칼럼을 엔티티의 속성 값으로 연결해주는 매핑이 있다.
- 각 user의 프로퍼티 중에는 데이터베이스에서 가져온 name과 age가 있다.
- 이 프레임워크를 사용하면 User의 프로퍼티에 접근할 때 자동으로 Entity 클래스에 정의된 데이터베이스 매핑으로부터 필요한 값을 가져오므로 편리하다.
728x90
반응형