[mvvm] hilt를 쓰지 않은 뷰모델 정리

말랑돌·2025년 8월 13일

안드로이드 학습

목록 보기
12/17
post-thumbnail

ViewModel을 만들 때 object vs class

한줄 정리 : class를 사용

1. object ViewModel의 문제점

object는 싱글톤이기 때문에, 앱 전체에서 인스턴스가 하나만 존재

  • 수명 주기(Lifecycle) 관리 불가능

    • ViewModel은 일반적으로 ViewModelStoreOwner(예: Activity, NavBackStackEntry)에 바인딩되어, 화면이 사라질 때 자동으로 해제됩니다.
    • 하지만 object로 만들면 이 관리가 불가능해서 메모리가 필요 이상 오래 유지됩니다.
  • 다중 화면(여러 ViewModel 인스턴스) 불가

    • 동일한 화면을 여러 개 띄우거나(예: 멀티탭), 다른 데이터로 같은 화면을 재사용하려고 해도 인스턴스가 하나뿐이어서 데이터가 섞입니다.
  • 의존성 주입(DI) 불편

    • Hilt, Koin 같은 DI 프레임워크가 제공하는 viewModel() 스코프를 활용할 수 없습니다.
    • 대신 수동 초기화나 전역 상태로 관리해야 해서 테스트와 유지보수가 힘들어집니다.

2. class ViewModel의 장점

  • Lifecycle-aware

    • viewModel() 또는 hiltViewModel()을 쓰면, Compose가 화면 수명 주기에 맞춰 ViewModel을 생성/폐기합니다.
  • 화면별 상태 분리 가능

    • 같은 ViewModel 타입이라도 각 화면마다 독립된 인스턴스를 가질 수 있습니다.
  • 테스트 및 의존성 주입 용이

    • 생성자에 Repository, UseCase 등을 주입하기 쉬우며, Mock 객체로 테스트가 가능합니다.
  • 구조적 권장사항

    • Google 공식 문서에서도 ViewModel은 class로 선언하고 AndroidX ViewModel API를 이용하도록 권장합니다.
      출처: Android Developers - ViewModel

Hilt를 쓰지 않고도 class 기반 ViewModel 사용

핵심은 ViewModelProvider와 remember 계열 함수를 잘 쓰는 것

1. 기본 원칙

  • ViewModel은 class MyViewModel(...) : ViewModel() 형태로 선언

  • Activity에서 ViewModelProvider로 생성 혹은 Compose 함수에서 viewModel()로 생성

  • 필요한 의존성은 생성자에 직접 전달 (수동 DI)

2. 예시 코드

// 1.1 모델 정의
class MyRepository {
    fun getMessage() = "안녕하세요, Repository에서 가져온 데이터입니다."
}

// 1.2 ViewModel 정의
class MainViewModel(
    private val repository: MyRepository
) : ViewModel() {

    // Compose UI에서 관찰할 수 있는 상태
    var message by mutableStateOf("")
        private set

    fun loadMessage() {
        message = repository.getMessage()
    }
}


// 2. Activity에서 ViewModel 생성 (Hilt 없이)
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 의존성 수동 생성
        val repository = MyRepository()

        // ViewModelProvider로 ViewModel 생성
        val mainViewModel = ViewModelProvider(
            this,
            object : ViewModelProvider.Factory {
                @Suppress("UNCHECKED_CAST")
                override fun <T : ViewModel> create(modelClass: Class<T>): T {
                    return MainViewModel(repository) as T
                }
            }
        )[MainViewModel::class.java]

        setContent {
            MainScreen(viewModel = mainViewModel)
        }
    }
}

// 3. Composable에서 ViewModel 사용
@Composable
fun MainScreen(viewModel: MainViewModel) {
    // 상태 읽기
    val message = viewModel.message

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = message)
        Spacer(Modifier.height(16.dp))
        Button(onClick = { viewModel.loadMessage() }) {
            Text("메시지 불러오기")
        }
    }
}


의존성이 있는 경우 vs 없는 경우

한줄 정리 : 뷰모델에 의존성(매개변수) 있으면 팩토리, 없으면 그냥 viewmodel()

1. 의존성 없는 경우 — 가장 간단한 사용법

viewmodel()로도 간편하게 사용가능

class MainViewModel : ViewModel() {
    var count by mutableStateOf(0)
        private set

    fun increment() { count++ }
}

@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
    Column {
        Text("Count: ${viewModel.count}")
        Button(onClick = { viewModel.increment() }) {
            Text("증가")
        }
    }
}

2. 의존성이 있는 경우

뷰모델 팩토리가 반드시 필요함

class MainViewModel(private val repository: MyRepository) : ViewModel() {
    val message = mutableStateOf("")
    fun load() { message.value = repository.getMessage() }
}

class MainViewModelFactory(
    private val repository: MyRepository
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return MainViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

@Composable
fun MainScreen() {
    val repository = remember { MyRepository() }
    val factory = remember { MainViewModelFactory(repository) }

    val viewModel: MainViewModel = viewModel(factory = factory)

    Column {
        Text(viewModel.message.value)
        Button(onClick = { viewModel.load() }) { Text("불러오기") }
    }
}

viewmodel을 매개변수로 넘기기 vs 컴포저블 내부에서 생성하기

한줄 정리 : 가급적이면 외부(상위)에서 매개변수로 넘겨주기

1. 외부에서 매개변수로 넘겨주는 방식

@Composable
fun MainScreen(viewModel: MainViewModel) {
    // 사용
}

장점

  • UI 테스트 용이
    → 테스트 시 Mock ViewModel을 주입할 수 있습니다.

  • 의존성 주입 명확
    → ViewModel 생성 로직이 한 곳(상위)에서만 이루어집니다.

  • 재사용성 높음
    → 같은 Composable이 다른 ViewModel을 받아서 동작 가능.

단점

  • 상위에서 반드시 ViewModel을 생성 후 전달해야 해서 코드가 조금 길어질 수 있음.

  • Navigation Graph나 Activity에서 생성 위치를 고려해야 함.

언제 쓰나

  • 상태 관리의 주도권이 상위 계층에 있어야 할 때
    (예: 여러 화면이 같은 ViewModel을 공유해야 하는 경우)

  • 테스트/모듈화를 중시할 때

2. 내부에서 viewModel()로 생성하는 방식

@Composable
fun MainScreen() {
    val viewModel: MainViewModel = viewModel(factory = factory)
    // 사용
}

장점

  • 코드 간결, Composable 호출하는 곳이 단순해짐

  • 상위에서 ViewModel 생성 로직을 몰라도 됨

단점

  • 테스트 어려움
    → Composable 안에서 바로 생성하므로 Mock 주입이 힘듦

  • 의존성 관리 분산
    → ViewModel 생성 로직이 여러 Composable에 흩어질 수 있음

  • 재사용성 낮음
    → 해당 Composable은 특정 ViewModel에 강하게 결합됨

언제 쓰나

  • 해당 화면만 쓰는 ViewModel이고, 테스트보다는 빠른 개발이 목적일 때

  • 프로토타입, 개인 프로젝트, 화면-ViewModel가 1:1 고정 매칭일 때


0개의 댓글