Compose 컴파일러는 parameter를 stable, unstable 2가지로 분류하며, 이는 compose 런타임이 composable 함수의 재구성을 필요로 하는지 결정하는데 사용된다.
// stable
data class User(
val id: Int,
val name: String,
)
// unstable
data class User(
val id: Int,
var name: String,
)
컴파일러는 모든 속성의 안정성을 집합적으로 평가하여 결정되기 때문에 발생하며, 하나의 가변 속성만 있어도 class 전체가 unstable 하게 간주될 수 있다.
val myList: List<String> = listOf("A", "B", "C")
(myList as MutableList).add("D")
myList는 한번만 할당할 수 있지만, 내부적으로는 가변적이기 때문에 요소를 변경할 수 있다. val은 재할당될 수 없음을 보장하지만, 가변 데이터 구조를 통해 변경 가능성이 여전히 존재한다.
이럴 때, @Immutable을 사용하면 이러한 불변성을 더 강력하게 보장할 수 있다.
@Immutable을 통해 효과적으로 안정성을 챙기려면 아래 내용을 준수하면 된다.
@Immutable 어노테이션은 위와 같이 불변성 규칙을 준수하는 클래스에 효과적이며, 불필요한 재구성을 건너뛰고 성능 개선에 중요한 역할을 한다.
하지만, @Immutable 어노테이션을 신중하게 적용하는게 중요하다. 부적절하게 사용하면 재구성이 의도치 않게 건너뛰어져 Compose 레이아웃이 예상대로 업데이트 되지 않을 수 있다. 따라서 클래스의 실제 불변성을 고려하고 불변성의 기준을 충족하는지 확인하는게 중요하다.
@Stable은 @Immutable에 비해 Compose 컴파일러에 대한 강력하지만, 약간 덜 엄격한 약속을 나타낸다.
함수나 속성에 적용될 때, @Stable은 해당 유형이 가변적일 수 있음을 의미한다. 역설적으로 보일 수 있지만, 여기서 "Stable"이라는 용어는 같은 입력에 대해 함수가 일관되게 동일한 결과를 반환한다는 것을 의미하며, 잠재적인 가변성에도 불구하고 예측 가능한 동작을 보장한다.
즉, @Stable을 사용하면 속성이 가변적일 수 있지만, 그 속성의 값을 사용하는 방식이나 그 값이 제공하는 결과는 항상 일관되게 유지된다는 의미이다. 이러한 방식으로 Compose는 안정성을 보장하면서도 유연성을 제공할 수 있다.
따라서, @Stable 어노테이션은 공개 속성이 불변이지만 클래스 자체가 안정성 기준을 충족하지 않을 수 있는 클래스에 가장 적합하다. ex) Compose의 State 인터페이스는 value라는 불변 속성만을 노출한다. 그러나 기본값은 일반적으로 MutableState를 생성하여 setValue 함수를 통해 여전히 수정될 수 있다.
MutableState에 의해 생성된 State의 인스턴스는 getValue 함수(즉, value 속성의 getter)에서 항상 동일한 값을 가져오며, 이는 setValue 함수에 동일한 입력에 대해 동일한 결과를 생성한다.
@Stable
interface State<out T> {
val value: T
}
@Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
State는 불변성을 보장하면서도 MutableState를 통해 가변성을 허용할 수 있는 유연성을 제공한다.
[결론]
@Stable 어노테이션을 클래스에 추가함으로써 해당 클래스가 가진 속성이 안정적이라는 것을 보장할 수 있다. 이는 같은 입력에 대해 같은 결과를 제공하는 일관성을 의미한다.
이러한 안정성 덕분에 클래스 내부에서 속성이 가변적일 수 있음에도 불구하고 외부에서는 이 속성을 예측 가능하고 안정적으로 사용할 수 있게 된다. 즉, 내부적으로 값이 변경될 수 있지만 외부 사용자에게는 변하지 않는 것처럼 보이기 때문에 안정적인 인터페이스를 제공할 수 있다.
이로 인해 Compose에서 재구성을 수행할 때 불필요한 업데이트를 피하고 성능을 유지하는데 도움을 주며, 성능 개선에 도움이 된다.
@Immutable과 @Stable의 차이점 및 어떤 것을 사용할지 결정하는 것은 혼란스러울 수 있다.
앞서 언급했듯이,
@Immutable은 클래스의 모든 공개 속성이 불변임을 나타내며, 즉 클래스가 생성된 이후에는 상태가 변경될 수 없다는 의미이다.@Immutable
data class User(
val id: String,
val name: String,
val email: String
)
[불변성 보장]
User data class는 @Immutable 로 표시되어 있으며, 모든 속성이 val로 선언되어 있어 불변성을 보장한다. 따라서 이 클래스의 인스턴스는 생성된 이후에 상태가 변경될 수 없다. 이를 통해 Compose에서 안정적이고 예측 가능한 방식으로 사용될 수 있다.
[data class 특성]
data class는 기본적으로 equals(), hashCode(), toString()과 같은 메소드를 자동으로 생성하며, @Immutable 이 추가되면 이러한 특성이 더 강조된다. 인스턴스가 불변이기 때문에 데이터 비교 및 사용이 일관되고 안정적으로 이루어진다.
[재구성 최적화]
Compose에서는 불변 객체를 활용하여 불필요한 재구성을 피할 수 있다. User 클래스의 인스턴스가 불변이므로, 동일한 User 객체를 참조하는 UI 컴포넌트는 상태 변화가 없을 경우 재구성을 건너뛰고 성능을 최적화할 수 있다.
예를 들어, 2개의 User 객체가 동일한 id, name, email 값을 가지면 Compose는 이를 동일한 객체로 간주하고, 같은 객체를 참조한다면(즉, 동일한 메모리 주소라면) 재구성을 하지 않는다.(건너뛴다.)
[스레드 안정성]
불변성 덕분에 User 클래스의 인스턴스는 여러 스레드에서 안전하게 공유될 수 있다. 데이터가 변경되지 않기 때문에 동시성 문제를 걱정할 필요가 없어, 멀티 스레드 환경에서도 안정적으로 작동한다.
[간단한 데이터 모델링]
@Immutable을 사용함으로써 User 클래스는 간단한 데이터 모델링을 가능하게 한다. 즉, 상태가 변경되지 않는 도메인 모델을 설계할 수 있으며, 이로 인해 코드의 유지보수성과 가독성이 향상된다.
@Stable은 가변 객체에 적용할 수 있으며, 동일한 입력에 대해 일관된 결과를 생성해야 한다는 조건이 있다.@Stable
interface UiState<T : Result<T>> {
val value: T?
val exception: Throwable?
val hasSuccess: Boolean
get() = exception == null
}
[안정성 보장]
@Stable 어노테이션을 통해 UiState 인터페이스를 구현할 때, 구현체가 동일한 입력에 대해 일관된 결과를 제공해야 함을 나타낸다. 이는 UI 컴포넌트가 이 인터페이스를 사용하여 상태를 관리할 때, 안정적으로 동작하도록 보장한다.
[가변성과 일관성]
UiState 인터페이스는 내부적으로 value와 exception 2개의 속성을 가지며, 이 속성들은 각각 성공적인 결과와 오류 상태를 나타낸다. 이들 속성은 가변적일 수 있지만, @Stable이 적용되었기 때문에 외부에서는 항상 예측 가능한 방식으로 사용해야 한다.
예를 들어, value가 변경되거나 exception이 발생하더라도 hasSuccess 속성은 항상 그 상태를 기반으로 올바른 결과를 반환해야 한다.
[효율적인 재구성]
Compose는 @Stable이 붙은 인터페이스를 활용하여 불필요한 재구성을 피할 수 있다. UiState가 안정적인 상태를 유지한다면, UI는 상태가 변경되지 않았을 경우 재구성을 건너뛰고 이전 결과를 재사용할 수 있다.
예를 들어, UI가 UiState의 hasSuccess 속성을 사용하여 조건부 렌더링을 수행할 경우, exception이 null인 상태가 변경되지 않았다면 해당 UI 컴포넌트는 재구성 없이 이전 상태를 그대로 유지할 수 있다.
[유연한 데이터 관리]
@Stable을 사용하면 가변성을 허용하면서도 일관성을 유지할 수 있어, 다양한 상태를 표현할 수 있다. 즉, UiState는 성공적인 결과(value)와 오류(exception)를 동시에 관리할 수 있으며, 이 모든 것이 안정적이고 예측 가능한 방식으로 이루어진다.