프로퍼티 접근자 로직 재활용: 위임 프로퍼티

유우선·2026년 2월 16일

Kotlin Study📚

목록 보기
25/32
  • 프로퍼티 접근자 로직을 매번 재구현할 필요 없이 쉽게 구현할 수 있음
    • 자신의 값을 필드가 아닌 DB 테이블, 브라우저 세션, 맵 등에 저장할 수 있음
  • 위임 → 작업을 도우미 객체가 처리하도록 맡기는 디자인 패턴
    • 도우미 객체 = 위임 객체
  • 도우미 객체를 직접 작성할 수도 있지만 코틀린 언어가 제공하는 기능을 활용할 수도 있음

위임 프로퍼티의 기본 문법과 내부 동작

import java.lang.reflect.Type

class Delegate(){
    operator fun getValue(/*...*/) { /*...*/ } // getter를 구현하는 로직

    operator fun setValue(/*...*/, value: Type) { /*...*/ } // setter를 구현하는 로직
    
    operator fun provideDelegate(/*...*/): Delegate { /*...*/ } // 위임 객체 생성, 제공
}

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

fun main() {
    val foo = Foo() // provideDelegate 호출
    val oldValue = foo.p // delegate.getValue 호출
    foo.p = newValue // delegate.setValue 호출
}
  • 관례에 따라 Delegate 클래스는 getValue와 setValue를 제공해야 함
  • 변경 가능한 프로퍼티만 setValue응 사용
  • provideDelegate 함수는 선택적으로 구현
    • 최초 생성시 검증 로직
    • 위임이 인스턴스화 되는 방식 변경
  • foo.p → Delegate 타입의 위임 프로퍼티 객체에 있는 메서드를 호출

by lazy()를 사용한 지연 초기화

  • 객체의 일부분을 초기화하지 않고 나중에 필요할 때 초기화하는 패턴
    • 초기화 과정에서 자원을 많이 사용하거나 객체를 사용할 때마다 꼭 초기화 하지 않아도 되는 프로퍼티에 사용할 수 있음

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

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의 위임 객체 역할을 하는 프로퍼티

    val emails: List<Email>
        get() {
            if(_emails == null) {
                _emails = loadEmails(this) // 최초 접근시 이메일을 가져옴
            }
            return _emails!! // 저장해놓은 데이터가 있으면 그 데이터를 반환
        }
}

fun main() {
    val p = Person("Alice")
    p.emails // 최초로 emails에 접근할 때만 이메일을 가져옴
    // Alice의 이메일을 가져옴
    p.emails
}
  • 뒷바침하는 프로퍼티를 사용

    • 외부로는 읽기 전용 프로퍼티를 제공하고 내부에는 가변 프로퍼티를 사용
    • 데이터 캡슐화 강화, 가변 데이터의 안전한 관리, 읽기 전용 API 제공
  • _emails → 값을 저장, null이 될 수 있는 타입

  • emails → _emails에 대한 읽기 연산을 제공, null이 될 수 없는 타입

  • 뒷바침하는 프로퍼티의 이름은 관례를 따름

    • 비공개 프로퍼티 앞에 언더바( _ )를 붙임
    • 공개 프로퍼티는 아무것도 붙이지 않음
  • 뒷받침하는 프로퍼티 사용의 단점

    1. 코드가 복잡함
    2. 스레드 안전한 구현이 아님
      • loadEmails 함수에 여러 스레드에서 동시 접근할 경우 일관성이 망가질 수 있음
    • 이 문제들을 위임 프로퍼티를 사용해 해결할 수 있음

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

  • 위임 프로퍼티를 사용하면 간결하게 구현할 수 있음
    • 데이터를 저장할 때 쓰이는 뒷파침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화해줌
class Person(val name: String) {
		val emails by lazy { loadEmails(this) }
}
  • lazy 함수
    • 코틀린 관례에 맞는 시그니처의 getValue 메서드가 들어있는 객체를 반환함
    • by 키워드를 함께 사용해 위임 프로퍼티를 만들 수 있음
    • 인자 → 값을 초기화 할 때 호출할 람다임
    • 기본적으로 thread-safe한 함수임
    • 필요하면 동기화 락을 lazy 함수에 전달할 수 있음
    • 다중 스레드 환경에서 사용하지 않을 프로퍼티를 위해 동기화를 생략할 수도 있음

위임 프로퍼티 구현

observable 예시

  • observable : 어떤 객체를 UI에 표시하는 경우 객체가 바뀌면 자동으로 UI도 바뀌는 프로그램
  1. 위임 프로퍼티 없이 구현
fun interface Observer {
		fun onChange(name: String, oldValue: Any?, newValue: Any?)
}

open class Observable {
		val observers = mutableList<Observer>()
		fun notifyObservers(propName: String, oldValue: Any?, newValue: Any?) {
				for(obs in observers){
						obs.onChange(propName, oldValue, newValue)
				}
		}
}
  • Observable 클래스 → Observer 리스트 관리
  • NotifyObservers → 등록된 모든 Observer의 onChange 함수를 통해 프로퍼티의 이전 값과 새 값을 전달
  • Observers → onChage에 대한 구현만 제공하면 됨
class Person(val name: String, age: Int, salary: Int): Observable() {
		var age: Int = age
				set(newValue){
						val oldValue = field // 뒷받침하는 프로퍼티에 접근할 때 field 식별자를 사용
						field = newValue
						notifyObservers("age", oldValue, newValue)
				}
				
		var salary: Int = salary
				set(newValue){
						val oldValue = field
						field = newValue
						notifyObservers("salary", oldValue, newValue)
				}
}

fun main() {
		val p = Person("seb", 28, 1000) 
		p.observers += Observer {propName, OldValue, newValue -> 
				// 함수형 인터페이스에 대한 간편한 구문을 사용해
				// 옵저버를 생성하고 이를 등록하여 프로퍼티의 변경을 기다림
				println(                                             
					"""
					Property $propName changed from $oldValue to $newValue!
					""".trimIndent()
				)
		} 
		p.age = 29
		// Property age changed from 28 to 29
		p.salary = 1500
		// Property salary changed from 1000 to 1500
}
  • Field 키워드를 사용해 age, salary 프로퍼티를 뒷받침하는 필드에 접근
  • Setter코드의 중복이 많음
  1. 도우미 클래스를 통해 프로퍼티 변경 통지 구현
class ObservableProperty(
		val propName: String,
		var propValue: Int,
		val observable: Observable
) {
		fun getValue(): Int = propValue
		fun setValue(newValue: Int) {
				val oldValue = propName
				propValue = newValue
				observable.notyiObservers(propName, oldValue, newValue)
		}
}

class Person(val name: String, age: Int, salary: Int): Observable() {
		val _age = observableProperty("age", age, this)
		var age: Int
				get() = _age.getValue()
				set(newValue) {
						_age.setValue(newValue)
				}
		val _salary = ObservableProperty("salary", salary, this)
		var salary: Int
				get() = _salary.getValue()
				set(newValue) {
						_salary.setValue(newValue)
				}
}
  • 프로퍼티 값을 저장하고 그 값이 바뀌면 자동으로 변경을 통지
  • 중복을 상당 부분 제거했으나 아직 각각의 프로퍼티마다 ObservableProperty를 만들고 작업을 위임하는 코드가 중복됨
  • 코틀린 위임 프로퍼티 기능ㅇ로 이런 중복 조차 없앨 수 있음
  1. 프로퍼티 위임 객체
import kotiln.reflect.KProperty 

class ObservableProperty(var propValue: Int, val observable: Obvservable) { 
		operator fun getValue(thisRef: Any?, prop: KProperty<*>): Int = propValue
		operator fun setValue(thisRef: Any?, prop: KProperty<*>, newValue: Int) { 
				val oldValue = propValue
				propValue = newValue
				observable.notifyObservers(prop.name, oldValue, newValue)
		}
}
  • 코틀린 관례에 사용하는 다른 함수처럼 getValue, setValue 함수에도 operator 변경자가 붙음
  • GetValue와 setValue는 파라미터 2개를 받음
    • ThisRef → 설정하거나 읽을 프로퍼티가 들어있는 인스턴스
    • Prop → 프로퍼티를 표현하는 객체
      • KProperty 타입의 객체를 사용하여 프로퍼티를 표현
  • KProperty 인자를 통해 프로퍼티 이름을 전달받으므로 주 생성자에서는 name 프로퍼티를 없앰
class Person(val name: String, age: Int, salary: Int): Observable() { 
		var age by ObservableProperty(age, this)
		var salary by ObservableProperty(salary, this)
}
  • By 키워드를 사용하여 위임 객체를 지정
    • Getter/setter를 직접 지저하는 등의 작업을 컴파일러가 자동으로 해줌
  1. Delegates.observable을 사용해 프로퍼티 변경 통지 구현하기
  • ObservableProperty 클래스를 직접 구현하는 대신 표준 라이브러리 기능 활용하기
  • 앞에서 구현한 Observable 클래스오는 연결되어 있지 않음
import kotiln..properties.Delegates

class Person(val name: String, age: Int, salary: Int): Observable() { 
		private val onChange = { 
		property: KProperty<*>, 
		oldValue: Any?, 
		newValue: Any? -> notifyObservers(property.name, oldValue, newValue)
		}
		
		var age by Delegates.observable(age, onChange)
		var salary by Delegates.observable(salary, onChange)
}
  • 프로퍼티 값의 변경을 통지받을 때 쓰일 람다를 Delegates 라이브러리 클래스에 넘겨야 함
  • By의 오른쪽에 있는 식 → 꼭 새 인스턴스를 만들 필요는 없음
    • 함수 호출, 다른 프로퍼티, 다른 식 등도 올 수 있음
    • 다만 결과 객체는 컴파일러가 호출할 수 있는 올바른 타입의 getValue와 setValue를 반드시 제공해야 함

프로퍼티 위임은 완전히 제네릭하여 모든 타입에 사용할 수 있음


위임 프로퍼티의 동작 방식

위임 프로퍼티 클래스 예시

class C { 
		var prop: Type by MyDelegate()
}

val c = C()
  • MyDelegate 클래스의 인스턴스 → 감춰진 프로퍼티에 저장됨
    • 라는 이름으로 부름
  • 프로퍼티 표현 → KProperty 타입의 객체를 사용
    • 라는 이름으로 부름

컴파일러는 다음의 코드를 생성

class C { 
		private val <delegate> = MyDelegate()
		
		var prop: Type
				get() = <delegate>.getValue(this, <property>)
				set(value: Type) = <delegate>.setValue(this, <property>, value)
}

  • 프로퍼티 값이 저장될 장소를 바꿀 수 있음 (맵, db 테이블, 웹 쿠키 등)
  • 프로퍼티를 읽거나 쓸 때 실행할 작업을 변경할 수 있음

맵에 위임해서 동적으로 애트리뷰트 접근

자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 위임 ㅍ로퍼티를 활용하는 경우가 자주 있음

  • C++에선 그런 객체를 확장 가능한 객체(expando object)라고 부름
  • 속성을 모두 맴에 저장하되 특별한 처리가 필요한 정보에 접근하도록 프로퍼티를 제공할 수 있음
class Person { 
		private val _attributes = mutableMap<String, String>()
		
		fun setAttributes(attrName: String, value: String) {
				_attributes[attrName] = value
		}
		
		var name: String
				get() = _attributes["name"]!!
				set(value) { 
						_attributes["name"] = value
				}
}

fun main() { 
		val p = Person()
		val data = mapOf("name" to "Seb", "company" to "JetBrains")
		for((attrName, value) in data)
				p.setAttribute(attrName, value)
		println(p.name)
		// Seb
		p.name = "Sebastian"
		println(p.name)
		// Sebastian	
}
  • 추가 데이터를 객체에 읽어 들이기 위해 일반적인 API를 사용
  • 한 프로퍼티를 처리하기 위해 구체적인 API를 제공

값을 맵에 저장하는 위임 프로퍼티 사용

By 키워드 뒤에 맵을 직접 넣음

class Person {
		private val _attributes = mutableMapOf<String, String>()
		
		fun setAttribute(attrName: String, value: String) {
				_attribute[attrName] = value
		}
		
		var name: String by _attributes // 위임 프로퍼티로 맵 사용
}
  • 코드가 작동하는 이유 → 표준 라이브러리가 Map과 MutableMap 인터페이스에 getValue와 setValue 확장 함수를 제공하기 때문
  • P.name → _attribute.getValue(p, prop)을 호출
  • _attribute.getValue(p, prop)는 -attributes[prop.name]을 통해 구현

실전 프레임워크가 위임 프로퍼티를 활용하는 방법

  • 객체 프롶티를 저장하거나 병경하는 방법을 바꿔 프레임워크 개발에 활용

위임 프로퍼티를 사용해 데이터베이스 칼럼에 접근

  • User라는 데이터베이스 테이블이 있음
    • Name이라는 문자열 타입의 칼럼
    • Age라는 정수 타입의 칼럼
  • User와 Users라는 클래스를 정의해 데이터베이스에 있는 모든 사용자 엔티티를 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 객체 → 데잍베이스 테이블을 표현
    • 데이터베이스는 전체에 단 하나만 존재하는 테이블을 표현하므로 싱글턴 객체로 선언
    • 객체의 프로퍼티는 칼럼을 표현
  • User의 상위 클래스인 Entity 클래스 → 데이터베이스 칼럼을 엔티티의 속성값으로 연결해주는 매핑
    • 데이터베이스에서 가져온 name, age가 있음
    • User의 프로퍼티에 접근할 때 자동으로 Entity 클래스에 정의된 데이터베이스 매핑으로부터 필요한 값을 가져옴
    • 객체를 변경하면 객체가 변경됨(dirty) 상태로 변하고 나중에 데이터베이스에 적절히 변경을 반영해줌

데이터베이스 접근 분석

  • Users의 칼럼 타입을 명시적으로 지정한 모습
object Users: IdTable() {
		val name: Column<String> = varchar("name", 50).index()
		val age: Column<Int> = integer("age")
}
  • 프레임워크는 Column 클래스 안에 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) { 
      		// 데이터베이스의 값 변경하기
      }

Column 프로퍼티를 위임 프로퍼티에 대한 위임 객체로 사용할 수 있음

  • User.age += 1 → user.ageDelegate.setValue(user.Delegate.getValue()+1)와 비슷한 코드로 변환됨
  • GetValue/setValue → 데이터베이스에서 데이터를 가져오고 기록하는 작업을 처리

0개의 댓글