주완 님의 블로그
[Kotlin] Stable 과 Immutable 톺아보기 본문
Android Compose 의 @Immutable 와 @Stable
Android Compose 의 @Immutable 와 @Stable
Jetpack Compose에서 @Immutable와 @Stable 어노테이션은 컴포저블 함수의 리컴포지션을 최적화하는 데 중요한 역할을 합니다. 각각의 어노테이션은 특정한 상황에서 사용되며, 사용하는 것과 사용하지
choi-dev.tistory.com
Kotlin Collections Immutable: ImmutableList vs PersistentList 이해하기
Kotlin Collections Immutable: ImmutableList vs PersistentList 이해하기
Kotlin의 kotlinx.collections.immutable는 불변 컬렉션을 효율적으로 다루기 위한 전용 컬렉션 구현체를 제공합니다. 특히 ImmutableList와 PersistentList는 API 설계와 내부 구현에서 각각 다른 목적을 갖습니
velog.io
해당글에서 참조했습니다)
Jetpack Compose 에서 Immutable , Stable 어노테이션은 컴포져블 함수의 리컴포지션을 최적화하는 데 중요한 역할을 합니다. 각각의 어노테이션은 특정한 상황에서 사용되며, 사용하는 것과 사용하지 않는 것에 따라 성능에 미치는 영향이 달라집니다.
Immutable
클래스가 불변임을 명시. 불변 객체는 상태가 변경되지 않기 때문에, Jetpack Compose 는 해당 객체가 변경되지 않는다고 가정하고 리컴포지션을 피할 수 있습니다.
객체가 불변이며, 내부의 모든 필드도 불변 객체일 때 사용합니다. 이는 data class 와 같은 경우에 자주 사용됩니다.
@Immutable
data class User(val name: String, val age: Int)
stable
객체가 안정적임을 명시한다. 이 말은 객체의 특정 프로퍼티가 변경될 수 있지만, 객체 자체는 변경되지 않거나 자주 변경되지 않는다는 것을 의미합니다. 객체가 안정적이기 때문에, Jetpack Compose는 이 객체가 자주 변경되지 않는다고 가정하고 불필요한 리컴포지션을 피하려고 시도합니다.
@Stable
class Counter(var count: Int)
프젝하다보면 헷갈리는게 좀 있는데 계층별로 한 번 알아봅시다.
1. Data Layer (Repository)
// API Response - 어노테이션 불필요
data class UserResponse(
val name: String,
val age: Int,
val hobbies: List<String>
)
// Room Entity - 어노테이션 불필요
@Entity
data class UserEntity(
@PrimaryKey val id: String,
val name: String,
val age: Int
)
Data 계층에서는 어노테이션 불필요합니다. 이 계층은 Compose와 직접 관련이 없고, 단순히 데이터 전송/저장 목적이기 때문이죠.
2. Domain Layer
// Domain Model - 선택적으로 사용
@Immutable // 또는 어노테이션 없이
data class User(
val id: String,
val name: String,
val age: Int,
val hobbies: List<String> // 일반 List도 OK
)
Domain 계층도 선택적입니다. 하지만 이 모델이 Presentation까지 그대로 사용된다면 여기서부터 붙이는 게 좋습니다.
3. Presentation Layer
// UI State - 반드시 사용!
@Immutable
data class UserUiState(
val user: User,
val isLoading: Boolean = false,
val hobbies: ImmutableList<String> // 또는 kotlinx.collections.immutable.List
)
// 또는 Stable이 필요한 경우
@Stable
class UserListState(
private val _users: SnapshotStateList<User> = mutableStateListOf()
) {
val users: List<User> = _users
fun addUser(user: User) {
_users.add(user)
}
}
언제 어노테이션을 붙일까?
// 추천: Presentation 계층의 UiState
@Immutable
data class ProductUiState(
val products: ImmutableList<Product>,
val selectedCategory: String,
val isLoading: Boolean
)
// Domain 모델이 UI에서 직접 사용될 때
@Immutable
data class Product(
val id: String,
val name: String,
val price: Int
)
// 불필요: Data 계층
data class ProductResponse(...) // 어노테이션 없음
ImmutableList vs List - 언제 사용할까?
// 1. 일반 List - Compose가 불변으로 인식 못함
data class State1(
val items: List<String> // 불안정
)
// 2. ImmutableList - Compose가 불변으로 인식
@Immutable
data class State2(
val items: ImmutableList<String> // 안정적
)
// 3. persistentListOf 사용 예시
import kotlinx.collections.immutable.*
val myList = persistentListOf("A", "B", "C")
val newList = myList.add("D") // 새로운 리스트 반환 (불변)
ViewModel에서 변환
class UserViewModel : ViewModel() {
private val repository: UserRepository
// MutableStateFlow로 관리
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUsers() {
viewModelScope.launch {
repository.getUsers()
.map { userResponse ->
// Data -> Domain 변환
userResponse.toDomain()
}
.collect { users ->
// List -> ImmutableList 변환
_uiState.value = UserUiState(
users = users.toImmutableList(),
isLoading = false
)
}
}
}
}
// Mapper
fun UserResponse.toDomain(): User = User(
id = id,
name = name,
age = age
)
@Immutable
data class UserUiState(
val users: ImmutableList<User> = persistentListOf(),
val isLoading: Boolean = true
)
정리하자면…
- Presentation 계층에서 신경을 쓰자 - Composable에 전달되는 데이터만 최적화
- ImmutableList는 List 타입이 UiState에 포함될 때 사용 - 단순 데이터 전달은 일반 List도 괜찮다
- persistentListOf는 리스트 수정이 자주 일어날 때 - 효율적인 복사본 생성
- 과도한 최적화 주의 - 성능 문제가 실제로 발생할 때 적용
// 간단한 경우 - 어노테이션 없어도 OK
@Composable
fun UserScreen(users: List<User>) { ... }
// 복잡한 State - 어노테이션 필수!
@Composable
fun UserScreen(uiState: UserUiState) { ... }
List는 변경이 잦으면 ImmutableList, 아니면 일반 List로도 충분
+++ 추가로 Stablity에 대해서 보자면
Compose가 자동으로 Stable하다고 판단하는 타입
Compose Compiler는 특정 타입들을 자동으로 stable하다고 간주합니다:
// 자동으로 Stable - 어노테이션 불필요
data class User(
val name: String, // 원시 타입
val age: Int, // 원시 타입
val isActive: Boolean, // 원시 타입
val score: Double // 원시 타입
)
@Composable
fun UserCard(user: User) { // Skippable!
Text(user.name)
}
Compose가 Stable하다고 보는 타입들
- 원시 타입: Int, Long, Float, Double, Boolean, Char, Byte, Short
- String
- 함수 타입: () -> Unit, (Int) -> String 등
- Enum
- @Immutable / @Stable 붙은 클래스
문제는 List….
// Unstable - List는 자동으로 stable하지 않음!
data class UserState(
val name: String, // Stable
val age: Int, // Stable
val hobbies: List<String> // Unstable!
)
@Composable
fun UserProfile(state: UserState) { // Non-skippable!
// hobbies가 List라서 전체가 unstable
}
왜 List는 Unstable일까?
// List는 인터페이스이고, 구현체가 mutable일 수 있음
val list1: List<String> = mutableListOf("A", "B")
val list2: List<String> = listOf("A", "B")
// Compose 입장에서는 둘을 구분 못함
// 따라서 안전하게 unstable로 판단
Compose Compiler 리포트로 확인하기
// build.gradle.kts
kotlinCompilerExtensionVersion = "1.5.0"
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
"${project.buildDir}/compose_metrics"
)
}
}
실제 리포트 예시
// Stable한 경우
stable class User {
stable val name: String
stable val age: Int
<runtime stability> = Stable
}
// Unstable한 경우
unstable class UserState {
stable val name: String
stable val age: Int
unstable val hobbies: List<String> // ← 이것 때문에!
<runtime stability> = Unstable
}
해결 방법들
1. ImmutableList 사용 (권장)
@Immutable // 명시적으로 표시
data class UserState(
val name: String,
val age: Int,
val hobbies: ImmutableList<String> // Stable!
)
2. @Immutable 어노테이션 (간단한 경우)
@Immutable
data class UserState(
val name: String,
val age: Int,
val hobbies: List<String> // Compose에게 "믿어줘"라고 알림
)
3. @Stable + 주의해서 사용
@Stable
data class UserState(
val name: String,
val age: Int,
val hobbies: List<String>
) {
// 내부에서 hobbies를 변경하지 않도록 주의!
}
Ex)
// Non-skippable
data class ProductList(
val products: List<Product>, // Unstable!
val total: Int
)
@Composable
fun ProductGrid(state: ProductList) { // 매번 리컴포지션!
LazyColumn {
items(state.products) { product ->
ProductCard(product)
}
}
}
// Skippable
@Immutable
data class ProductList(
val products: ImmutableList<Product>, // Stable!
val total: Int
)
@Composable
fun ProductGrid(state: ProductList) { // Skip 가능!
LazyColumn {
items(state.products) { product ->
ProductCard(product)
}
}
}
타입 Stability Skippable 비고
| Int, String | Stable | skip가능 | 자동 판단 |
| List<T> | Unstable | 불가 | 문제 |
| ImmutableList<T> | Stable | 가능 | 권장 |
| @Immutable data class | Stable | 가능 | 명시적 표시 |
// 정리
data class Good(
val id: Int, // 자동 Stable
val name: String // 자동 Stable
) // → 전체 Stable, Skippable!
data class Bad(
val id: Int, // Stable
val tags: List<String> // Unstable
) // → 전체 Unstable, Non-skippable!
@Immutable
data class Fixed(
val id: Int,
val tags: ImmutableList<String> // Stable
) // → 전체 Stable, Skippable!
'Android' 카테고리의 다른 글
| 결제 요청 시 collectLatest 와 collect 의 사용 (0) | 2025.12.17 |
|---|---|
| Hilt 의 정의와 사용법 (0) | 2025.12.10 |
| Android에서 Domain Layer와 UseCase: 언제 필요하고 언제 생략할까? (0) | 2025.12.03 |
| Android - jetpack navigation 사용시 중첩 네비게이션에 대하여 (0) | 2025.11.11 |
| android- 이미지 라이브러리 Coil (1) | 2025.11.07 |