Primary/Kotlin

[Kotlin] Computed Property

해스끼 2022. 10. 6. 14:40

간단한 직사각형 클래스를 정의해 보자. 직사각형은 너비, 높이, 넓이 속성을 갖는다.

data class Rectangle(
    val width: Int,
    val height: Int,
    val area: Int,
)

끝~

 

...일 리가 없지. 사실 이 구현은 매우 잘못된 구현이다. ``area``가 ``width``와 ``height``의 곱으로 주어진다는 보장이 없기 때문이다.

 

약간 개선해 보면, 생성자에서 ``width``와 ``height``를 입력받아 ``area``를 내부적으로 계산하게 정의할 수 있다.

data class Rectangle(
    val width: Int,
    val height: Int,
) {
    val size: Int = width * height
}

``width``와 ``height``가 모두 ``val``이므로 값이 변할 염려도 없으니, 이제 ``size``가 항상 ``width``와 ``height``의 곱과 같다고 확신할 수 있다. 설령 ``copy`` 함수를 사용하더라도 괜찮다. ``copy`` 함수는 내부적으로 생성자를 호출하기 때문에 ``size``도 다시 계산된다.

``var``이라면?

그런데 ``width``와 ``height``가 ``var``이라면 어떨까?

data class Rectangle(
    var width: Int,
    var height: Int,

) {
    val size: Int = width * height
}

``size``는 객체를 생성될 때 한 번만 계산되는데, ``width``와 ``height``가 ``var``이므로 객체가 만들어진 후에도 얼마든지 변경될 수 있다. 너비 또는 높이가 바뀔 때 ``size``도 같이 바뀌게 할 수는 없을까?

Computed Property

Computed Property를 사용하면 된다. Computed property는 (이름에서도 알 수 있듯이) 다른 값에서부터 계산되는 값으로, 계산하는 식은 custom getter에 정의하면 된다.

data class Rectangle(
    var width: Int,
    var height: Int,
) {
    val size: Int
        get() = width * height
}

``size`` 값이 참조될 때마다 custom getter가 실행된다. 따라서 ``width``와 ``height``가 다른 값으로 할당되더라도 ``size``의 값이 항상 올바르게 유지된다.

 

그런데 이렇게 하면 ``size``라는 이름의 함수를 정의한 것과 같지 않은가?

data class Rectangle(
    var width: Int,
    var height: Int,
) {
    fun size() = width * height
}

같긴 한데

사실 내부적으로 custom getter는 자바의 getter 함수와 같은 역할을 한다. 따라서 custom getter와 ``size()`` 모두 내부적으로 함수 호출로 구현된다.

 

하지만 Kotlin을 사용하는 우리 입장에서 보면, 에 접근하고 있으니 아무래도 함수보단 속성처럼 사용하는 편이 좋다. 

// recommended
val size = rect1.size

// how about this?
val size = rect1.size()

사용 예시

내부적으로 ``MutableState``를 사용하는 ``abstract class AbstractViewModel``을 정의했다. 

abstract class <S> AbstractViewModel(defaultState: S) {
    private val mutableStates = MutableStateFlow(defaultState)
    val states = mutableStates.asStateFlow()
}

이제 ``MainViewModel``을 구현해 보자.

class MainViewModel: AbstractViewModel(SomeState()) {
    
    // ...
    
    fun onNameUpdate(newName: String) {
        mutableStates.value = mutableStates.value.copy(name = newName)
    }
}

값을 업데이트하는 부분이 뭔가 어색하지 않은가? ``mutableStates``에 매번 접근하다 보니 코드가 너무 장황해진다.

 

Computed property와 custom getter/setter를 사용하면 더 간결한 코드를 작성할 수 있다. ``AbstractViewModel``을 수정하고, 바뀐 정의에 맞게 ``MainViewModel``을 업데이트하자.

abstract class <S> AbstractViewModel(defaultState: S) {
    private val mutableStates = MutableStateFlow(defaultState)
    private val states: StateFlow<S> = mutableStates.asStateFlow()
    protected var state: S
        get() = mutableStates.value
        set(value) {
            mutableStates.value = value
        }
}
class MainViewModel: AbstractViewModel(SomeState()) {
    
    // ...
    
    fun onNameUpdate(newName: String) {
        state = state.copy(name = newName)
    }
}

훨씬 깔끔하지 않은가? ``state``의 구현 방법을 숨기는 동시에 값을 사용하기도 쉬워졌다. 좋은데? 내 코드도 당장 고치러 가야지