본문 바로가기
안드로이드 공부 노트/Compose(컴포즈)

[Android-Compose] 안드로이드 컴포즈의 안정성(Stability) 2편 - @Stable, @Unstable

by 지게요 2025. 7. 26.
728x90
반응형

이번 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가 바뀐 값으로 안 불러와지는 현상이 종종 있었다.

이번 정리하면서 나는 왜 안 불러와지는지 알았고, 앞으로 개발 시 안정성 있게 개발을 해서 불필요한 리소스 줄여나가야겠다고 느꼈다.

반응형