이번 2편에서는 컴파일러가 항상 완벽하게 stable을 판단해 주지 않기 때문에 내가 만든 클래스 혹은 객체가 실제로는 stable 한데 컴파일러는 unstable로 판단할 수 있는 현상을 해결하기 위한 방법을 알아보겠다.
이해를 위해서 1편을 보고 오는 걸 추천한다.
# @Stable, @Immutable 어노테이션
컴파일러가 항상 완벽하게 판단해 주지 않기 때문에 내가 만든 클래스 혹은 객체가 실제로는 stable 한데 컴파일러는 unstable로 판단할 수 있다.
그래서 @Stable, @Immutable 어노테이션을 우리가 적절히 활용해 직접 알려줘야 정확히 stable로 인식시킬 수 있다.
## @Stable
🔸 @Immutable 보다는 조금 느슨한 약속
🔸 잠재적인 변경 가능성에도 불구하고 예측 가능한 동작을 보장한다는 것을 의미
위 어노테이션은 1편의 stable로 간주되는 유형에서 봤듯이 해당 조건을 만족하면 자동으로 stable로 간주한다.
<예시 코드>
@Stable // 이미 stable 조건을 만족하므로 생략 가능
class UiState(
val name: String,
val age: Int
)
그럼 저 어노테이션은 언제 사용하는 걸까?
1️⃣ 내부에 mutable 상태를 가지고 있지만 그 상태 변화가 Compose에 의해 추적 가능한 경우 (예: MutableState)
class CounterState {
var count by mutableStateOf(0)
}
위 코드분석해 보면
count는 mutableStateOf로 감싸져 있어 컴포즈는 값의 변화를 추적가능
하지만 var로 선언되어 있기 때문에 컴파일러에서 unstable로 처리
따라서 위 코드는 unstable이다.
우리는 mutableStateOf를 사용해서 count 값의 변화를 추적할 수 있음에도 var를 사용해서 굳이 unstable로 만들었다. 이럴 때 @Stable 어노테이션을 사용해 준다.
@Stable // 추가
class CounterState {
var count by mutableStateOf(0)
}
2️⃣ 구조상 자동 분석이 어렵거나, 커스텀 getter를 사용한 경우
class User(
private val firstName: String,
private val lastName: String
) {
val fullName: String
get() = "$firstName $lastName"
}
위 코드를 보면 언뜻보면 매개변수들이다 원시 타입이고 val 형식이기 때문에 stable처럼 보이지만 컴파일러는 unstable로 판단한다.
그 이유는 컴파일러는 fullName의 결과를 정확히 예측가능한 값인지 확신을 할 수 없기 때문이다.
fullName의 값이 랜덤값, 시스템 시간등을 사용 할 수 있다고 생각한다.
이런 경우에도 @Stable 어노테이션을 사용 한다.
@Stable // 추가
class User(
private val firstName: String,
private val lastName: String
) {
val fullName: String
get() = "$firstName $lastName"
}
## @Immutable
🔹 @Stable 보다 더 강한 불변(Immutable)의 약속
🔹 객체가 생성된 이후 내부 상태가 절대 변하지 않는다는 것을 보장
모든 원시타입들(String, Int, Float...)은 Immutable 간주한다.
그럼 @Immutable은 언제 사용하는 걸까?
1️⃣ 의도를 명확히 하고 싶을 때
@Immutable
data class Config(
val apiUrl: String,
val timeout: Long
)
해당 코드는 원시타입으로 이루어져 있기 때문에 @Immutable 생략이 가능하다.
하지만 해당 어노테이션을 붙여줌으로써 코드 의도가 명확해지는 효과가 나타난다.
2️⃣ 완전히 해당 코드가 불변임을 확신할 수 있는 경우만
// ✅ 확실히 불변 - 모든 프로퍼티가 불변 타입
@Immutable
data class Config(
val apiUrl: String,
val timeout: Long,
val isEnabled: Boolean
)
// ✅ 불변 컬렉션만 포함
@Immutable
data class UserList(
val users: List<User> // List는 불변 인터페이스
)
@Immutable은 개발자가 안정성을 약속하는 것이기 때문에 내부적으로 var를 사용하거나, 변경 가능한 구조를 포함하고 있다면 리컴포지션을 의도하지 않게 건너뛰어 화면이 업데이트되지 않을 수 있으니 정말 불변임을 확신할 수 있는 경우만 사용해야 한다.
# @Stable VS @Immutable
정리를 하다 보니 나는 이런 생각이 들었다.
"둘 다 비슷한 역할을 하는 거 같은데 무슨 차이지?"
그래서 둘의 차이점을 정리해 봤다.
| 특징 | @Stable | @Immutable |
| 변경 가능성 | 예측 가능한 변경 허용 | 완전 불변 |
| 프로퍼티 타입 | var 사용 가능(단, 상태 추적 필요) | 모든 프로퍼티가 val 이고 불변 타입 |
| 사용 사례 | 상태 변경이 필요, 사용자 입력 상태 | 순수 데이터만 보관, 외부 API 응답 |
표로 정리하면 위와 같고 간단히 말하면 변화할 가능성이 있는 상황이다.
코드로 정리하자면 아래와 같다.
데이터만 담는다 -> @Immutable
상태를 변경한다 -> @Stable
// @Immutable - 데이터만
@Immutable
data class User(val name: String, val age: Int)
// @Stable - 상태 변경
@Stable
class Counter {
var count by mutableStateOf(0)
fun increment() { count++ }
}
평소에 나는 분명히 값을 바꿨는데 UI가 바뀐 값으로 안 불러와지는 현상이 종종 있었다.
이번 정리하면서 나는 왜 안 불러와지는지 알았고, 앞으로 개발 시 안정성 있게 개발을 해서 불필요한 리소스 줄여나가야겠다고 느꼈다.