-
7.1 ~ 7.4 부분
-
part 오인성 (7.5 프로퍼티 접근자 로직 재활용: 위임 프로퍼티)
-
7.5.1 위임 프로퍼티 소개
-
7.5.2 위임 프로퍼티 사용: by lazy()를 사용
-
이메일 데이터를 불러오는 함수
-
전통적인 방식: 뒷받침하는 프로퍼티(backing property) 사용
-
개선된 방식: by lazy 사용
-
7.5.3 위임 프로퍼티 구현
-
1단계: 리스너 기능을 담은 기본 클래스 정의
-
2단계: 기본 구현 - 중복 코드 존재
-
3단계: 공통 동작을 클래스로 추출
-
4단계: 위임 프로퍼티를 위한 관례 적용
-
5단계: 최종 구현 - 위임 프로퍼티 적용
-
표준 라이브러리의 Delegates.observable 사용
-
추가 설명
-
7.5.4 위임 프로퍼티 컴파일 규칙
-
7.5.5 프로퍼티 값을 맵에 저장
-
일반 구현 방식
-
위임 프로퍼티로 변경
-
7.5.6 프레임워크에서 위임 프로퍼티 활용
-
예제: 사용자 정보(User) 테이블 매핑
-
내부 구현: Column 클래스의 위임 로직
-
동작 방식 요약
-
도서 링크 바로가기
Kotlin in Action을 공부하며 정리한 내용입니다.
저작권에 문제가 될 시, 글을 모두 내리겠습니다.
제가 공부한 내용이 더 많은 분들에게도 도움이 되었으면 좋겠습니다. 부족한 부분은 댓글을 통해서 피드백을 주신다면 언제나 반영하겠습니다. 감사합니다.
책에 대한 링크는 맨 아래에 있습니다.
7.1 ~ 7.4 부분
Kotlin_In_Action_1/7장. 연산자 오버로딩과 기타 관례/7-1,2.md at main · Kotlin-Android-Study-with-SSAFY/Kotlin_In_A
SSAFY 13기 모바일 트랙 구미 5반 "코틀린 인 액션" 스터디(A). Contribute to Kotlin-Android-Study-with-SSAFY/Kotlin_In_Action_1 development by creating an account on GitHub.
github.com
Kotlin_In_Action_1/7장. 연산자 오버로딩과 기타 관례/7-3,4.md at main · Kotlin-Android-Study-with-SSAFY/Kotlin_In_A
SSAFY 13기 모바일 트랙 구미 5반 "코틀린 인 액션" 스터디(A). Contribute to Kotlin-Android-Study-with-SSAFY/Kotlin_In_Action_1 development by creating an account on GitHub.
github.com
part 오인성 (7.5 프로퍼티 접근자 로직 재활용: 위임 프로퍼티)
코틀린의 위임 프로퍼티(delegated property)는 단순한 값 저장 이상의 동작을 프로퍼티에 부여할 수 있는 강력한 기능이다. 위임 프로퍼티를 사용하면, 값을 특정 필드가 아닌 데이터베이스, 브라우저 세션, 맵 등 외부 저장소에 저장하거나, 공통된 접근자 로직을 재활용 할 수 있다.
이러한 동작은 위임(delegate) 이라는 디자인 패턴을 기반으로 하며, 실제 작업을 다른 객체(delegate)가 대신 처리한다.
7.5.1 위임 프로퍼티 소개
class Foo {
var p: Type by Delegate()
}
p
프로퍼티는 자신의 접근자 로직을 Delegate
객체에 위임한다. by
키워드 뒤에 위치한 객체가 위임 객체(delegate) 로 사용되며, 이 객체가 getValue, setValue 메서드를 제공해야 한다.
컴파일러는 내부적으로 아래와 같은 코드를 생성한다:
class Foo {
private val delegate = Delegate()
var p: Type
set(value: Type) = delegate.setValue(..., value)
get() = delegate.getValue(...)
}
즉, 프로퍼티에 접근하거나 값을 설정할 때마다 delegate 객체의 메서드를 호출하는 방식이다.
Delegate
클래스는 아래와 같이 정의할 수 있으며, operator
키워드를 통해 위임 관례에 맞춘다:
class Delegate {
operator fun getValue(...) {
...
}
operator fun setValue(..., value: Type) {
...
}
}
class Foo {
var p: Type by Delegate()
}
>>> val foo = Foo ()
>>> val oldValue = foo.p
>>> foo.p = newValue
이 예제에서 foo.p
는 일반적인 프로퍼티처럼 보이지만, 내부적으로는 Delegate
객체의 getValue
, setValue
가 호출된다.
이러한 구조를 통해 중복 없이 일관된 접근자 로직을 다양한 프로퍼티에 적용할 수 있으며, 이후에 설명할 지연 초기화(lazy initialization) 와 같은 패턴도 간결하게 구현할 수 있다.
7.5.2 위임 프로퍼티 사용: by lazy()를 사용
지연 초기화(lazy initialization) 는 어떤 값이 실제로 필요해질 때까지 초기화를 미루는 패턴이다. 초기화에 자원이 많이 들거나 매번 초기화할 필요가 없는 경우 유용하다.
예를 들어, Person
클래스가 작성한 이메일 목록을 제공한다고 가정하자. 이메일은 데이터베이스에서 가져와야 하므로 비용이 크며, 실제 사용할 때 한 번만 불러오고 싶다.
이메일 데이터를 불러오는 함수
class Email { /*...*/ }
fun loadEmails(person: Person): List<Email> {
println("${person.name}의 이메일을 가져옴")
return listOf(/*...*/)
}
전통적인 방식: 뒷받침하는 프로퍼티(backing property) 사용
class Person(val name: String) {
private var emails: List<Email>? = null
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
는 외부에서 접근 가능한 읽기 전용 프로퍼티- 초기화는 최초 접근 시 한 번만 수행됨
- 하지만 null 처리, 중복 코드, 스레드 안전성 부족 등의 단점이 있다
개선된 방식: by lazy 사용
코틀린은 위임 프로퍼티 기능을 통해 이 문제를 간단히 해결한다. lazy
는 값이 최초로 필요할 때 단 한 번만 람다를 호출해 초기화하며, 결과를 캐싱한다.
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
lazy
함수는getValue
메서드를 제공하는 객체를 반환하며, 이는 위임 프로퍼티에 적합하다.by lazy
를 사용하면 별도의 backing property 없이도 지연 초기화가 가능하다.- 기본적으로 스레드 안전하며, 필요에 따라 동기화 방식 지정도 가능하다.
요약하면, by lazy
는 지연 초기화를 간결하고 안전하게 구현할 수 있는 코틀린의 대표적인 위임 프로퍼티 활용 사례다. emails
같은 리소스 비용이 큰 데이터에 적합하며, 코드 간결성과 유지보수성을 모두 높여준다.
7.5.3 위임 프로퍼티 구현
프로퍼티의 값이 변경될 때마다 리스너에게 변경을 통지하는 기능을 구현하고, 이를 위임 프로퍼티로 점차 리팩토링하는 과정을 살펴본다.
1단계: 리스너 기능을 담은 기본 클래스 정의
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
changeSupport
는 리스너를 등록·제거하고 이벤트를 통지함- 이를 상속하면 다른 클래스에서 변경 통지 기능을 사용할 수 있음
2단계: 기본 구현 - 중복 코드 존재
class Person(
val name: String, age: Int, salary: Int
) : PropertyChangeAware () {
var age: Int = age
set(newValue) {
val oldValue = field
field = newValue
changeSupport.firePropertyChange("age", oldValue, newValue)
}
var salary: Int = salary
set(newValue) {
val oldValue = field
field = newValue
changeSupport.firePropertyChange("salary", oldValue, newValue)
}
}
field
는 프로퍼티의 실제 값을 저장하는 백킹 필드- 세터마다 동일한 변경 통지 코드가 반복됨
3단계: 공통 동작을 클래스로 추출
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
가 실제 값을 저장하고, 변경 시 통지- 반복되는 세터 로직을 제거했지만 여전히 보일러플레이트 코드가 존재
4단계: 위임 프로퍼티를 위한 관례 적용
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)
}
}
변경사항:
operator
키워드를 추가해 위임에 필요한 관례를 만족KProperty<*>
는 프로퍼티 메타정보를 나타내며, 이름 등을 가져올 수 있음
5단계: 최종 구현 - 위임 프로퍼티 적용
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
키워드를 사용해 위임 객체 지정- 컴파일러가 내부적으로
getValue
,setValue
호출 코드를 생성함 - 이전 단계들과 비교해 코드가 훨씬 간결해짐
표준 라이브러리의 Delegates.observable 사용
표준 라이브러리에는 ObservableProperty
와 유사한 기능을 하는 클래스가 존재하며, 이를 활용하면 더 간결하게 구현할 수 있다.
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)
}
Delegates.observable
은 값 변경을 감지하고, 콜백(observer)을 호출해줌- 콜백에서
changeSupport
를 이용해 직접 변경 통지를 보낼 수 있음
추가 설명
by
뒤에는 객체 생성뿐만 아니라 함수 호출, 프로퍼티 참조 등 다양한 식이 올 수 있다.- 위임 객체는 반드시
getValue
,setValue
를 제공해야 하며, 이는 멤버 메서드 또는 확장 함수로 구현 가능하다. - 현재 예제에서는
Int
타입만 다뤘지만, 위임 메커니즘은 모든 타입에 적용 가능하다.
이처럼 위임 프로퍼티를 활용하면 중복 코드 없이 변경 감지 로직을 추상화할 수 있으며, 표준 라이브러리와 결합하면 훨씬 더 간결하고 유연한 코드를 작성할 수 있다.
7.5.4 위임 프로퍼티 컴파일 규칙
위임 프로퍼티는 내부적으로 컴파일러가 특정 규칙에 따라 코드를 생성하여 동작한다. 다음과 같은 코드가 있을 때:
class C {
var prop: Type by MyDelegate()
}
val c = C()
컴파일러는 다음과 같이 코드를 변환한다:
class C {
private val <delegate> = MyDelegate()
var prop: Type
get() = <delegate>.getValue(this, <property>)
set(value: Type) = <delegate>.setValue(this, <property>, value)
}
<delegate>
는 위임 객체를 저장하는 감춰진 프로퍼티<property>
는KProperty
타입 객체로, 위임 대상 프로퍼티를 설명- 결과적으로
prop
접근/설정 시 위임 객체의getValue
,setValue
가 호출된다
이 메커니즘을 통해 다음과 같은 활용이 가능하다:
- 프로퍼티 저장소 변경: 맵, DB, 세션 등
- 읽기/쓰기 동작 정의: 값 검증, 변경 알림 등
간단한 문법만으로 다양한 동작을 구현할 수 있게 해준다.
7.5.5 프로퍼티 값을 맵에 저장
위임 프로퍼티는 프로퍼티 값을 맵에 저장하는 데도 활용할 수 있다. 특히 필수 정보 외에 동적으로 속성을 추가해야 하는 경우 유용하다.
일반 구현 방식
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
attributes
맵을 통해 동적 속성 저장name
은 맵에서 직접 값을 꺼내는 방식
위임 프로퍼티로 변경
class Person {
private val attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
attributes[attrName] = value
}
val name: String by attributes
}
by attributes
를 통해name
프로퍼티를attributes
맵에 위임attributes.getValue(this, property)
형태로 호출됨
왜 작동하는가?
코틀린 표준 라이브러리는Map
과MutableMap
에 대해getValue
와setValue
확장 함수를 제공하며, 이 함수들은 프로퍼티 이름을 자동으로 키로 사용한다.
val name: String by attributes
↓ 내부적으로 변환됨
val name = attributes.getValue(this, prop)
↓ 다시
val name = attributes[prop.name]
이 방식은 JSON 역직렬화, 사용자 정의 속성 처리 등에서 매우 유용하게 활용될 수 있다.
7.5.6 프레임워크에서 위임 프로퍼티 활용
위임 프로퍼티는 프레임워크 개발 시 객체의 프로퍼티 저장 방식이나 변경 방식을 유연하게 제어할 수 있어 매우 유용하다. 이 절에서는 데이터베이스 프레임워크에서 위임 프로퍼티가 어떻게 사용되는지를 살펴본다.
예제: 사용자 정보(User) 테이블 매핑
object Users : IdTable() {
val name = varchar("name", length = 50).index()
val age = integer("age")
}
class User(id: EntityID) : Entity(id) {
var name: String by Users.name
var age: Int by Users.age
}
Users
객체는 DB 테이블을 표현하며 싱글턴으로 선언됨name
,age
는 각 컬럼을 나타냄User
클래스는Entity
를 상속받으며, DB의 한 레코드(엔티티)를 표현- 프로퍼티 접근 시 내부적으로 DB에서 값을 읽고, 변경 시 DB에 값을 기록함
예:
user.age += 1
을 하면 DB에서도 해당User
의age
값이 업데이트됨
내부 구현: Column 클래스의 위임 로직
class User(id: EntityID) : Entity(id) {
var name: String by Users.name
var age: Int by Users.age
}
Users.name
, Users.age
는 각각 Column<String>
, Column<Int>
타입이며, 이들은 위임 객체로서 getValue
, setValue
를 구현하고 있다.
operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
// 데이터베이스에서 칼럼 값 가져오기
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
// 데이터베이스의 값 변경하기
}
getValue
는 DB에서 해당 컬럼의 값을 가져옴setValue
는 변경된 값을 DB에 기록하고, 해당 엔티티를 dirty 상태로 표시함
동작 방식 요약
user.age += 1
위 코드의 내부 동작은 다음과 같다:
getValue
로 현재age
값을 가져옴- 값에 1을 더함
setValue
로 변경된 값을 다시 DB에 저장
즉, 위임 프로퍼티를 통해 사용자는 일반 프로퍼티처럼 다루지만, 내부적으로는 데이터베이스와 연결된 로직이 자동으로 실행된다.
이런 방식은 복잡한 데이터 매핑 로직을 간결하게 감추고, 유지보수성과 가독성을 크게 높여준다. 위임 프로퍼티는 프레임워크 내부 동작을 사용자에게 투명하게 제공할 수 있는 강력한 수단이다.
도서 링크 바로가기
https://product.kyobobook.co.kr/detail/S000001804588
Kotlin in Action | 드미트리 제메로프 - 교보문고
Kotlin in Action | 코틀린이 안드로이드 공식 언어가 되면서 관심이 커졌다. 이 책은 코틀린 언어를 개발한 젯브레인의 코틀린 컴파일러 개발자들이 직접 쓴 일종의 공식 서적이라 할 수 있다. 코틀
product.kyobobook.co.kr
'Programming Language > Kotlin' 카테고리의 다른 글
[Kotlin In Action] 6장. 코틀린 타입 시스템 정리 feat. 내가 정리한 부분만 (0) | 2025.04.03 |
---|---|
[Kotlin In Action] 5장. 람다로 프로그래밍 정리 (1) | 2025.03.21 |
[Kotlin In Action] 4장. 클래스 , 객체 , 인터페이스 정리 (0) | 2025.03.21 |
[Kotlin In Action] 3장. 함수 정의와 호출 정리 (0) | 2025.03.20 |
[Kotlin In Action] 2장. 코틀린 기초 스터디 정리 (0) | 2025.03.01 |
Kotlin in Action을 공부하며 정리한 내용입니다.
저작권에 문제가 될 시, 글을 모두 내리겠습니다.
제가 공부한 내용이 더 많은 분들에게도 도움이 되었으면 좋겠습니다. 부족한 부분은 댓글을 통해서 피드백을 주신다면 언제나 반영하겠습니다. 감사합니다.
책에 대한 링크는 맨 아래에 있습니다.
7.1 ~ 7.4 부분
Kotlin_In_Action_1/7장. 연산자 오버로딩과 기타 관례/7-1,2.md at main · Kotlin-Android-Study-with-SSAFY/Kotlin_In_A
SSAFY 13기 모바일 트랙 구미 5반 "코틀린 인 액션" 스터디(A). Contribute to Kotlin-Android-Study-with-SSAFY/Kotlin_In_Action_1 development by creating an account on GitHub.
github.com
Kotlin_In_Action_1/7장. 연산자 오버로딩과 기타 관례/7-3,4.md at main · Kotlin-Android-Study-with-SSAFY/Kotlin_In_A
SSAFY 13기 모바일 트랙 구미 5반 "코틀린 인 액션" 스터디(A). Contribute to Kotlin-Android-Study-with-SSAFY/Kotlin_In_Action_1 development by creating an account on GitHub.
github.com
part 오인성 (7.5 프로퍼티 접근자 로직 재활용: 위임 프로퍼티)
코틀린의 위임 프로퍼티(delegated property)는 단순한 값 저장 이상의 동작을 프로퍼티에 부여할 수 있는 강력한 기능이다. 위임 프로퍼티를 사용하면, 값을 특정 필드가 아닌 데이터베이스, 브라우저 세션, 맵 등 외부 저장소에 저장하거나, 공통된 접근자 로직을 재활용 할 수 있다.
이러한 동작은 위임(delegate) 이라는 디자인 패턴을 기반으로 하며, 실제 작업을 다른 객체(delegate)가 대신 처리한다.
7.5.1 위임 프로퍼티 소개
class Foo {
var p: Type by Delegate()
}
p
프로퍼티는 자신의 접근자 로직을 Delegate
객체에 위임한다. by
키워드 뒤에 위치한 객체가 위임 객체(delegate) 로 사용되며, 이 객체가 getValue, setValue 메서드를 제공해야 한다.
컴파일러는 내부적으로 아래와 같은 코드를 생성한다:
class Foo {
private val delegate = Delegate()
var p: Type
set(value: Type) = delegate.setValue(..., value)
get() = delegate.getValue(...)
}
즉, 프로퍼티에 접근하거나 값을 설정할 때마다 delegate 객체의 메서드를 호출하는 방식이다.
Delegate
클래스는 아래와 같이 정의할 수 있으며, operator
키워드를 통해 위임 관례에 맞춘다:
class Delegate {
operator fun getValue(...) {
...
}
operator fun setValue(..., value: Type) {
...
}
}
class Foo {
var p: Type by Delegate()
}
>>> val foo = Foo ()
>>> val oldValue = foo.p
>>> foo.p = newValue
이 예제에서 foo.p
는 일반적인 프로퍼티처럼 보이지만, 내부적으로는 Delegate
객체의 getValue
, setValue
가 호출된다.
이러한 구조를 통해 중복 없이 일관된 접근자 로직을 다양한 프로퍼티에 적용할 수 있으며, 이후에 설명할 지연 초기화(lazy initialization) 와 같은 패턴도 간결하게 구현할 수 있다.
7.5.2 위임 프로퍼티 사용: by lazy()를 사용
지연 초기화(lazy initialization) 는 어떤 값이 실제로 필요해질 때까지 초기화를 미루는 패턴이다. 초기화에 자원이 많이 들거나 매번 초기화할 필요가 없는 경우 유용하다.
예를 들어, Person
클래스가 작성한 이메일 목록을 제공한다고 가정하자. 이메일은 데이터베이스에서 가져와야 하므로 비용이 크며, 실제 사용할 때 한 번만 불러오고 싶다.
이메일 데이터를 불러오는 함수
class Email { /*...*/ }
fun loadEmails(person: Person): List<Email> {
println("${person.name}의 이메일을 가져옴")
return listOf(/*...*/)
}
전통적인 방식: 뒷받침하는 프로퍼티(backing property) 사용
class Person(val name: String) {
private var emails: List<Email>? = null
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
는 외부에서 접근 가능한 읽기 전용 프로퍼티- 초기화는 최초 접근 시 한 번만 수행됨
- 하지만 null 처리, 중복 코드, 스레드 안전성 부족 등의 단점이 있다
개선된 방식: by lazy 사용
코틀린은 위임 프로퍼티 기능을 통해 이 문제를 간단히 해결한다. lazy
는 값이 최초로 필요할 때 단 한 번만 람다를 호출해 초기화하며, 결과를 캐싱한다.
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
lazy
함수는getValue
메서드를 제공하는 객체를 반환하며, 이는 위임 프로퍼티에 적합하다.by lazy
를 사용하면 별도의 backing property 없이도 지연 초기화가 가능하다.- 기본적으로 스레드 안전하며, 필요에 따라 동기화 방식 지정도 가능하다.
요약하면, by lazy
는 지연 초기화를 간결하고 안전하게 구현할 수 있는 코틀린의 대표적인 위임 프로퍼티 활용 사례다. emails
같은 리소스 비용이 큰 데이터에 적합하며, 코드 간결성과 유지보수성을 모두 높여준다.
7.5.3 위임 프로퍼티 구현
프로퍼티의 값이 변경될 때마다 리스너에게 변경을 통지하는 기능을 구현하고, 이를 위임 프로퍼티로 점차 리팩토링하는 과정을 살펴본다.
1단계: 리스너 기능을 담은 기본 클래스 정의
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
changeSupport
는 리스너를 등록·제거하고 이벤트를 통지함- 이를 상속하면 다른 클래스에서 변경 통지 기능을 사용할 수 있음
2단계: 기본 구현 - 중복 코드 존재
class Person(
val name: String, age: Int, salary: Int
) : PropertyChangeAware () {
var age: Int = age
set(newValue) {
val oldValue = field
field = newValue
changeSupport.firePropertyChange("age", oldValue, newValue)
}
var salary: Int = salary
set(newValue) {
val oldValue = field
field = newValue
changeSupport.firePropertyChange("salary", oldValue, newValue)
}
}
field
는 프로퍼티의 실제 값을 저장하는 백킹 필드- 세터마다 동일한 변경 통지 코드가 반복됨
3단계: 공통 동작을 클래스로 추출
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
가 실제 값을 저장하고, 변경 시 통지- 반복되는 세터 로직을 제거했지만 여전히 보일러플레이트 코드가 존재
4단계: 위임 프로퍼티를 위한 관례 적용
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)
}
}
변경사항:
operator
키워드를 추가해 위임에 필요한 관례를 만족KProperty<*>
는 프로퍼티 메타정보를 나타내며, 이름 등을 가져올 수 있음
5단계: 최종 구현 - 위임 프로퍼티 적용
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
키워드를 사용해 위임 객체 지정- 컴파일러가 내부적으로
getValue
,setValue
호출 코드를 생성함 - 이전 단계들과 비교해 코드가 훨씬 간결해짐
표준 라이브러리의 Delegates.observable 사용
표준 라이브러리에는 ObservableProperty
와 유사한 기능을 하는 클래스가 존재하며, 이를 활용하면 더 간결하게 구현할 수 있다.
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)
}
Delegates.observable
은 값 변경을 감지하고, 콜백(observer)을 호출해줌- 콜백에서
changeSupport
를 이용해 직접 변경 통지를 보낼 수 있음
추가 설명
by
뒤에는 객체 생성뿐만 아니라 함수 호출, 프로퍼티 참조 등 다양한 식이 올 수 있다.- 위임 객체는 반드시
getValue
,setValue
를 제공해야 하며, 이는 멤버 메서드 또는 확장 함수로 구현 가능하다. - 현재 예제에서는
Int
타입만 다뤘지만, 위임 메커니즘은 모든 타입에 적용 가능하다.
이처럼 위임 프로퍼티를 활용하면 중복 코드 없이 변경 감지 로직을 추상화할 수 있으며, 표준 라이브러리와 결합하면 훨씬 더 간결하고 유연한 코드를 작성할 수 있다.
7.5.4 위임 프로퍼티 컴파일 규칙
위임 프로퍼티는 내부적으로 컴파일러가 특정 규칙에 따라 코드를 생성하여 동작한다. 다음과 같은 코드가 있을 때:
class C {
var prop: Type by MyDelegate()
}
val c = C()
컴파일러는 다음과 같이 코드를 변환한다:
class C {
private val <delegate> = MyDelegate()
var prop: Type
get() = <delegate>.getValue(this, <property>)
set(value: Type) = <delegate>.setValue(this, <property>, value)
}
<delegate>
는 위임 객체를 저장하는 감춰진 프로퍼티<property>
는KProperty
타입 객체로, 위임 대상 프로퍼티를 설명- 결과적으로
prop
접근/설정 시 위임 객체의getValue
,setValue
가 호출된다
이 메커니즘을 통해 다음과 같은 활용이 가능하다:
- 프로퍼티 저장소 변경: 맵, DB, 세션 등
- 읽기/쓰기 동작 정의: 값 검증, 변경 알림 등
간단한 문법만으로 다양한 동작을 구현할 수 있게 해준다.
7.5.5 프로퍼티 값을 맵에 저장
위임 프로퍼티는 프로퍼티 값을 맵에 저장하는 데도 활용할 수 있다. 특히 필수 정보 외에 동적으로 속성을 추가해야 하는 경우 유용하다.
일반 구현 방식
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
attributes
맵을 통해 동적 속성 저장name
은 맵에서 직접 값을 꺼내는 방식
위임 프로퍼티로 변경
class Person {
private val attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
attributes[attrName] = value
}
val name: String by attributes
}
by attributes
를 통해name
프로퍼티를attributes
맵에 위임attributes.getValue(this, property)
형태로 호출됨
왜 작동하는가?
코틀린 표준 라이브러리는Map
과MutableMap
에 대해getValue
와setValue
확장 함수를 제공하며, 이 함수들은 프로퍼티 이름을 자동으로 키로 사용한다.
val name: String by attributes
↓ 내부적으로 변환됨
val name = attributes.getValue(this, prop)
↓ 다시
val name = attributes[prop.name]
이 방식은 JSON 역직렬화, 사용자 정의 속성 처리 등에서 매우 유용하게 활용될 수 있다.
7.5.6 프레임워크에서 위임 프로퍼티 활용
위임 프로퍼티는 프레임워크 개발 시 객체의 프로퍼티 저장 방식이나 변경 방식을 유연하게 제어할 수 있어 매우 유용하다. 이 절에서는 데이터베이스 프레임워크에서 위임 프로퍼티가 어떻게 사용되는지를 살펴본다.
예제: 사용자 정보(User) 테이블 매핑
object Users : IdTable() {
val name = varchar("name", length = 50).index()
val age = integer("age")
}
class User(id: EntityID) : Entity(id) {
var name: String by Users.name
var age: Int by Users.age
}
Users
객체는 DB 테이블을 표현하며 싱글턴으로 선언됨name
,age
는 각 컬럼을 나타냄User
클래스는Entity
를 상속받으며, DB의 한 레코드(엔티티)를 표현- 프로퍼티 접근 시 내부적으로 DB에서 값을 읽고, 변경 시 DB에 값을 기록함
예:
user.age += 1
을 하면 DB에서도 해당User
의age
값이 업데이트됨
내부 구현: Column 클래스의 위임 로직
class User(id: EntityID) : Entity(id) {
var name: String by Users.name
var age: Int by Users.age
}
Users.name
, Users.age
는 각각 Column<String>
, Column<Int>
타입이며, 이들은 위임 객체로서 getValue
, setValue
를 구현하고 있다.
operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
// 데이터베이스에서 칼럼 값 가져오기
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
// 데이터베이스의 값 변경하기
}
getValue
는 DB에서 해당 컬럼의 값을 가져옴setValue
는 변경된 값을 DB에 기록하고, 해당 엔티티를 dirty 상태로 표시함
동작 방식 요약
user.age += 1
위 코드의 내부 동작은 다음과 같다:
getValue
로 현재age
값을 가져옴- 값에 1을 더함
setValue
로 변경된 값을 다시 DB에 저장
즉, 위임 프로퍼티를 통해 사용자는 일반 프로퍼티처럼 다루지만, 내부적으로는 데이터베이스와 연결된 로직이 자동으로 실행된다.
이런 방식은 복잡한 데이터 매핑 로직을 간결하게 감추고, 유지보수성과 가독성을 크게 높여준다. 위임 프로퍼티는 프레임워크 내부 동작을 사용자에게 투명하게 제공할 수 있는 강력한 수단이다.
도서 링크 바로가기
https://product.kyobobook.co.kr/detail/S000001804588
Kotlin in Action | 드미트리 제메로프 - 교보문고
Kotlin in Action | 코틀린이 안드로이드 공식 언어가 되면서 관심이 커졌다. 이 책은 코틀린 언어를 개발한 젯브레인의 코틀린 컴파일러 개발자들이 직접 쓴 일종의 공식 서적이라 할 수 있다. 코틀
product.kyobobook.co.kr
'Programming Language > Kotlin' 카테고리의 다른 글
[Kotlin In Action] 6장. 코틀린 타입 시스템 정리 feat. 내가 정리한 부분만 (0) | 2025.04.03 |
---|---|
[Kotlin In Action] 5장. 람다로 프로그래밍 정리 (1) | 2025.03.21 |
[Kotlin In Action] 4장. 클래스 , 객체 , 인터페이스 정리 (0) | 2025.03.21 |
[Kotlin In Action] 3장. 함수 정의와 호출 정리 (0) | 2025.03.20 |
[Kotlin In Action] 2장. 코틀린 기초 스터디 정리 (0) | 2025.03.01 |