코틀린이 제공하는 관례에 의존하는 특성 중에 독특하면서 강력한 기능인 위임 프로퍼티(delegated property)에 대해 알아보자.

위임 프로퍼티를 사용하면 값을 뒷받침하는 필드에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현할 수 있다. 또한 그 과정에서 접근자 로직을 매번 재구현할 필요도 없다. 예를 들어 프로퍼티는 위임을 사용해 자신의 값을 필드가 아니라 데이터베이스 테이블이나 브라우저 세션, 맵 등에 저장할 수 있다.

위임은 객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴이다. 이때 작업을 처리하는 도우미 객체를 위임 객체(delegate)라고 부른다.

위임 프로퍼티 소개

위임 프로퍼티의 일반적인 문법은 다음과 같다.

class Foo {
    var p: Type by Delegate()
}

p 프로퍼티는 접근자 로직을 다른 객체에게 위임한다. 여기서는 Delegate 클래스의 인스턴스를 위임 객체로 사용한다. by 뒤에 있는 식을 계산해서 위임에 쓰일 객체를 얻는다.

다음과 같이 컴파일러는 숨겨진 도우미 프로퍼티를 만들고 그 프로퍼티를 위임 객체의 인스턴스로 초기화한다. p 프로퍼티는 바로 그 위임 객체에게 자신의 작업을 위임한다. 설명을 편하게 하기 위해 이 감춰진 프로퍼티 이름을 delegate라고 하자.

class Foo {
    private val delegate = Delegate() // 컴파일러가 생성한 도우미 프로퍼티
    var p: Type
        set(value: Type) = delegate.setValue(..., value)
        get() = delegate.getValue(...)
}

p 프로퍼티를 위해 컴파일러가 생성한 접근자는 delegate의 getValue와 setValue 메소드를 호출한다. 프로퍼티 위임 관례를 따르는 Delegate 클래스는 getValue와 setValue 메소드를 제공해야 한다(변경 가능한 프로퍼티만 setValue를 사용).

관례를 사용하는 다른 경우와 마찬가지로 getValue와 setValue는 멤버 메소드이거나 확장 함수일 수 있다. 일단은 설명을 단순화하기 위해 이 두 메소드의 파라미터를 생략한다. Delegate 클래스를 단순화하면 다음과 같다.

class Delegate {
    operator fun getValue(...) { ... } // 게터를 구현하는 로직을 담는다.
    operator fun setValue(..., value: Type) { ... } // 세터를 구현하는 로직을 담는다.
}
class Foo {
    var p: Type by Delegate() // by 키워드는 프로퍼티와 위임 객체를 연결한다.
}

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

foo.p는 일반 프로퍼티처럼 쓸 수 있고, 일반 프로퍼티 같아 보인다. 하지만 실제로 p의 게터나 세터는 Delegate 타입의 위임 프로퍼티 객체에 있는 메소드를 호출한다.

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

지연 초기화(lazy initialization)는 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 흔히 쓰이는 패턴이다. 초기화 과정에 자원을 많이 사용하거나 객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 지연 초기화 패턴을 사용할 수 있다.

class Email { /* ... */ }

fun loadEmails(person: Person): List<Email> {
    println("${person.name}의 이메일을 가져옴")
    return listOf(/* ... */)
}

다음은 이메일을 불러오기 전에는 null을 저장하고, 불러온 다음에는 이메일 리스트를 저장하는 _emails 프로퍼티를 추가해서 지연 초기화를 구현한 클래스를 보여준다.

class Person(val name:String) {
    private var _emails: List<Email>? = null
    val emails: List<Email>
        get() {
            if (_emails == null) {
                _emails = loadEmails(this)
            }
            return _emails!!
        }
}

fun main() {
    val p = Person("Alice")
    p.emails // 최초로 emails를 읽을 때 단 한 번만 이메일을 가져온다.
}