Kotlin의 프로퍼티는 Java의 필드보다 강력한 기능을 제공합니다. 특히 위임(Delegation) 패턴을 통해 프로퍼티의 동작을 쉽게 커스터마이징할 수 있습니다.

프로퍼티 기본 문법

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

커스텀 Getter

val isEmpty: Boolean
    get() = this.size == 0

backing field 사용

var item: Int = 0
    get() {
        field += 1
        return field
    }

커스텀 Setter

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value)  // 다른 프로퍼티에 값 설정
    }

// field 키워드로 backing field 접근
var counter = 0
    set(value) {
        if (value >= 0) field = value
    }

접근자의 가시성 수정

var setterVisibility: String = "abc"
    private set  // setter는 private, getter는 public

var setterWithAnnotation: Any? = null
    @Inject set  // setter에 어노테이션

var stringRepresentation: String
    get() = this.toString()
    private set(value) {
        // ...
    }

프로퍼티 오버라이딩

Kotlin에서는 프로퍼티도 오버라이드할 수 있습니다 (getter/setter를 오버라이드).

open class Foo {
    open val x: Int get() { ... }
}

class Bar1 : Foo() {
    override var x: Int = ...  // val을 var로 오버라이드 가능
}

// primary constructor에서 오버라이드
interface Foo {
    val count: Int
}

class Bar1(override val count: Int) : Foo

class Bar2 : Foo {
    override var count: Int = 0
}

주의: valvar로 오버라이드할 수 있지만, 반대는 불가능합니다.

상수 (const)

컴파일 타임 상수는 다음 조건을 만족해야 합니다:

  • Top-level 또는 object의 멤버
  • String 또는 primitive 타입으로 초기화
  • 커스텀 getter 없음
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED)
fun foo() { ... }

lateinit

초기화를 나중에 할 수 있는 non-null 프로퍼티입니다.

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp
    fun setup() {
        subject = TestSubject()
    }

    @Test
    fun test() {
        subject.method()  // null 체크 없이 직접 사용
    }
}

초기화 여부 확인

companion object {
    private lateinit var sInstance: StockLiveData

    @MainThread
    fun get(symbol: String): StockLiveData {
        sInstance = if (::sInstance.isInitialized) sInstance else StockLiveData(symbol)
        return sInstance
    }
}

인터페이스 위임 (Interface Delegation)

이미 구현이 있는 경우 중복 구현 없이 위임할 수 있습니다.

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
    val b = BaseImpl(10)
    Derived(b).print()  // 10 출력
}

프로퍼티 위임 (Delegated Properties)

프로퍼티의 getter/setter 로직을 위임할 수 있습니다.

class Example {
    var p: String by Delegate()
}

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name} in $thisRef.'")
    }
}

val e = Example()
println(e.p)  // "Example@33a17727, thank you for delegating 'p' to me!"

표준 위임

lazy

값을 처음 접근할 때 초기화합니다.

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)  // "computed!" 출력 후 "Hello" 출력
    println(lazyValue)  // "Hello"만 출력 (이미 계산됨)
}

get()과의 차이: lazy는 한 번만 호출되고, get()은 매번 호출됩니다.

val app: BaseApplication
    get() = BaseApplication.instance  // 매번 호출됨

val app: BaseApplication by lazy {
    BaseApplication.instance  // 한 번만 호출됨
}

지역 위임 프로퍼티

조건이 만족되지 않으면 계산하지 않습니다.

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

observable

값이 변경될 때마다 콜백을 호출합니다.

class User {
    var name: String by Delegates.observable("<no name>") { prop, old, new ->
        println("$old -> $new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "first"   // "<no name> -> first" 출력
    user.name = "second"  // "first -> second" 출력
}

Map에 위임

Map의 키를 프로퍼티처럼 사용합니다.

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
}

val user = User(mapOf(
    "name" to "John Doe",
    "age" to 25
))

println(user.name)  // "John Doe"
println(user.age)   // 25

// Mutable 버전
class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int by map
}

provideDelegate

프로퍼티에 따라 다른 위임을 제공합니다.

class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> {
    override fun getValue(thisRef: MyUI, property: KProperty<*>): T { ... }
}

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
        thisRef: MyUI,
        prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        return ResourceDelegate()
    }

    private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

class MyUI {
    fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

다음 단계

프로퍼티와 위임에 대해 알아보았습니다. 다음으로 제네릭에 대해 알아보세요.