본문 바로가기

[Android] immutable 구조 State 알아보자

@hyeon.s2025. 7. 8. 12:43

MVI에서 다루는 State의 immutable에 대해 이번 포스팅에서 다뤄보고자 합니다.

MVI의 State는 반드시 immutable해야한다.

→ MVI에서 State는 immutable해야하고, 모든 상태 변경은 .copy를 통해 수행되어야 한다.

왜 immutable해야할까?

1. Compose는 값이 변경되었는지를 기준으로 U를 그린다.

Compose는 내부적으로 eqauls()또는 reference 비교로 변화 여부를 감지한다.

mutable상태 (var, val 내부 값 변경)는 변경을 감지하지 못해 재조합이 안일어날 수 있다.

mutable 상태 예시

data class LoginState(
    val email: StringBuilder = StringBuilder(), //mutable 객체
)

email.append(…) 처럼 내부 상태만 바꾸면, equals()는 동일 객체로 간주하기 때문에, Compose는 변화로 인식을 못한다

→ Compose가 변화를 인식하지 못하면 UI recomposition도 안 일어난다!

2. Immutable 객체는 추론과 테스트가 쉽다.

  • 상태 변경 전, 후가 분명하게 처리된다.
  • 디버깅할 때 어떤 인텐트가 어떤 상태를 유발했는지 로그로 추적하기가 쉽다.
  • 테스트 코드에서 assertEquals(expected, actual)로 비교가 쉽다.

3. 동시성 문제를 방지할 수 있다.

동시성 문제란?

→ 여러 작업(코루틴, 스레드)이 동시에 하나의 상태를 변경하거나 읽으려고할 때 발생하는 오류이다.

위험상황 예시

data class MutableState(
    var counter: Int
)

val sharedState = MutableState(0)

viewModelScope.launch {
    sharedState.counter += 1 // 코루틴 A
}

viewModelScope.launch {
    sharedState.counter += 1 // 코루틴 B
}
두 코루틴이 같은 객체(sharedState)의 내부 값을 동시에 바꾸면,
덮어쓰기(Race condition), 잘못된 결과, 충돌이 발생할 수 있다.
  • 따라서 viewModel에서 상태 변경할 때 MutableStateFlow를 사용한다.
  • 만약 상태가 불변이면, 코루틴 간 동시 접근도 안전하게 처리된다.
  • 하지만 내부가 가변 객체이면, 여러 코루틴이 같은 객체를 참조하므로 동기화 이슈가 발생할 수 있다.

State는 어떻게 구현하면 안전할까?

State 선언

data class LoginState(
    val email: String = "",
    val password: String = "",
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)

ViewModel 내부에서는 .copy활용

_state.update { current ->
    current.copy(
        email = intent.value,
        errorMessage = null
    )
}
  • 이 코드는 LoginState 객체를 복사하면서 일부 필드만 변경한다.
  • Compose는 새로운 객체로 감지 → recomposition 일어남

.copy메서드는

  • kotlin의 data class는 자동으로 .copy()메서드를 제공한다.
  • .copy()는 원래 겍체를 복사하면서, 특정 속성만 변경한 새로운 객체를 만드는 함수이다.
  • 상태를 직접 변경하면 Compose가 감지하지 못하기 때문에, .copy()로 변경이 필요한 필드만 바꾸고, 새로운 객체를 만들어 emit할 수 있다.

Compose의 equals()비교

val old = LoginState(email = "a")
val new = old.copy(email = "b")

println(old == new) // false → Compose는 재조합 발생시킴

즉 data class, copy구조가 Compose와 연결되어있다.

hyeon.s
@hyeon.s :: 개발로그
목차