주완 님의 블로그
MVVM , repository 패턴 본문
MVVM 패턴
세미나, 해커톤 준비를 위해 자주 사용하는 (?) 패턴에 대해서 공부해 보려고한다.
(공부하기 전에 간략하게 이해한 내용)
MVVM → model , view, viewmodel 이라는 구조를 통해서 공부를하자면
model : 간단하게 data , data 를 다루는 로직이라고 생각하면 편할 것 같다.
view : 흔히 보이는 UI 라고 간단하게 생각해보자
videmodel : view와 model 을 연결시켜주는 객체? 두 사이를 분리시켜 관리한다는 구조 정도로 생각해보자


Domain layer 사용 ( 필수는 아닌)
- ViewModel 을 사용하는 UI layer

ex) view에서 click 이라는 event 가 일어났을 때, ‘click 했다는 행동 ‘ 자체가 viewmodel 에 전달될 것이다. →
그럼 clickevent에 대응되는 처리를 하기위해 → model 에 있는 로직을 호출하여 하나의 과정을 이루고 결과값을 전달 받는다 → 이 전달된 하나의 data(나 상태?) 를 view에 composition 한다.
- Viewmodel 의 추가
dependencies {
// other dependencies
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
//...
}
- ViewModel 확장
import androidx.lifecycle.ViewModel
class GameViewModel : ViewModel() {
}
- ViewModel 사용의 이점
- 관심사의 분리 : UI 는 비즈니스 로직에서 분리되어 있다. 따라서 비즈니스와 데이터 로직을 독립적으로 유닛테스트 할 수 있어요.
- 반응형 UI : 반응형 데이터 타입을 컴포즈에 쉽게 통합시킬 수 있다.
StateFlow
StateFlow는 현재 상태와 새로운 상태 업데이트를 내보내는 관찰 가능한 데이터 홀더 흐름입니다. StateFlow의 value 속성은 현재 상태 값을 반영합니다. 상태를 업데이트하고 흐름에 전송하려면 MutableStateFlow 클래스의 value 속성에 새 값을 할당합니다.
Android에서 StateFlow는 관찰 가능한 불변 상태를 유지해야 하는 클래스에서 잘 작동합니다.
구성 가능한 함수가 UI 상태 업데이트를 리슨하고 구성 변경에도 화면 상태가 지속되도록 GameUiState에서 StateFlow를 노출할 수 있습니다.
GameViewModel 클래스에서 다음 _uiState 속성을 추가합니다.
import kotlinx.coroutines.flow.MutableStateFlow
// Game UI state
private val _uiState = MutableStateFlow(GameUiState())
데이터 전달하기
viewmodel 인스턴스를 UI로 전달한다. screen() 에서 collectatstate 를 통해 viewmodel 인스턴스를 사용하여 uistate 에 엑서스합니다.
collectasState 함수는 stateFlow에 값을 수집하고 state 를 통해 최신 값을 나타내게된다.
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun GameScreen(
gameViewModel: GameViewModel = viewModel()
) {
// ...
}
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@Composable
fun GameScreen(
// ...
) {
val gameUiState by gameViewModel.uiState.collectAsState()
// ...
}
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
// Game UI state
private val _uiState = MutableStateFlow(GameUiState())
val uiState: StateFlow<GameUiState> = _uiState.asStateFlow()
지원 속성
지원 속성을 사용하면 정확한 객체가 아닌 getter에서 무언가를 반환할 수 있습니다.
Kotlin 프레임워크는 var 속성별로 getter와 setter를 생성합니다.
getter 메서드와 setter 메서드 중 하나 또는 둘 모두를 재정의하여 고유한 맞춤 동작을 제공할 수 있습니다. 지원 속성을 구현하려면 읽기 전용 버전의 데이터를 반환하도록 getter 메서드를 재정의합니다. 다음 예는 지원 속성을 보여줍니다.
Composable 함수내에서 값을 수집할 때(collect), 수집 행위가 생명주기를 확실히 알게 하는 것은 중요합니다. 이말인 즉슨 수집할 때 화면에 가시성(visibility) 에 따라 보여질 때 수집을 실행하고, 보여지지 않을 때 자동으로 중단될 수 있음을 의미합니다. 어플리케이션이 백그라운드로 이동했을 때와 같이 다양한 케이스에서 필요없는 리소스가 낭비되는 것을 방지해줍니다.
이를 위해 StateFlow 의 확장함수인 collectAsStateWithLifecycle 를 사용할 수 있어요. 해당 함수는 호스트의 lifecycle owner 가 특정 생명주기 상태보다 높거나 낮아지면 수집을 시작하고 취소하는 함수에요.
https://developer.android.com/codelabs/basic-android-kotlin-compose-viewmodel-and-state?hl=ko#4
Compose의 ViewModel 및 상태 | Android Developers
이 Codelab에서는 아키텍처 구성요소 중 하나인 ViewModel을 사용하는 방법을 알아봅니다. 구성 변경 중에 앱 상태를 유지하도록 ViewModel을 구현합니다.
developer.android.com
- ViewModel 예시
class SampleViewModel{
fun show Main (contexxt:Context){
}
}
=> 이건 나쁘다
그렇다면
class mainviewmodel : ViewModel(){
private val _showMain MutableLiveData<item?>(null)
val showMain : LiveDate<item?>() get () = _showMain
_showMain.value = item
}
-> 그니까 얘 value 에 접근하는 거다.
=> 안드로이드 MVVM 은 결국 LifeCycle 을 알고는 있다. 하지만 Context를 직접 접근하지는 않아야 한다.
class MainViewModel : ViewModel() {
// LiveData로 UI와 연결될 데이터 정의
private val _accountBalance = MutableLiveData<String>()
val accountBalance: LiveData<String> = _accountBalance
private val _transactionHistory = MutableLiveData<List<Transaction>>()
val transactionHistory: LiveData<List<Transaction>> = _transactionHistory
// 데이터 로딩 함수 (API 호출 시뮬레이션)
fun loadAccountData() {
// 예시 데이터
_accountBalance.value = "3,242,000원" // 예시: 계좌 잔액
}
fun loadTransactionHistory() {
// 예시 데이터
val transactions = listOf(
Transaction("쏜 이자", "136원", "2025-04-18"),
Transaction("마이홈플랜", "주택청약 종합저축", "2025-04-17")
)
_transactionHistory.value = transactions
}
}
- private val _accontbalance = MutableLiveData<String>() ⇒ 언더스코어로 내부에서 값을 변경할 수 있게 하는 MutableLiveDate 이다.
- LiveDate 는 관찰 가능한 데이터 홀더 객체로 , UI 에서 자동으로 업데이트 받을 수 있게한다.
- val accountBalance : LiveDate<String> = _accountBalance
- 외부에서는 _accountBalance에 직접 접근하지 못하게 하고, 읽기 전용인 LiveData 로 제공 을 한다.
- 내부 값변경을 위해서 _accountBlance.value ⇒ .value를 통해 설정한다.
- Private 프로퍼티의 표시: _로 시작하는 변수는 private 멤버 변수라는 의미로 사용됩니다. 즉, 클래스 외부에서는 직접 접근할 수 없도록 하기 위해 사용합니다.
- 예를 들어, _accountBalance는 외부에서 직접 접근할 수 없고, 그 값은 LiveData 객체를 통해서만 외부에서 관찰 가능합니다.
- Encapsulation (캡슐화): 변수 이름 앞에 _를 붙이는 것은 캡슐화(encapsulation) 원칙을 따르기 위한 하나의 방법입니다. 캡슐화란 객체의 속성이나 상태를 보호하기 위해, 외부에서 직접적으로 접근하지 못하게 하고, 메서드나 프로퍼티를 통해 간접적으로 접근할 수 있도록 만드는 원칙입니다.
- LiveData는 데이터를 관찰할 수 있지만, 직접 수정할 수 없게 하기 위해 _accountBalance를 private로 두고, accountBalance라는 public 프로퍼티로 외부에 노출하는 방식입니다.
class HomeViewModel @Inject constructor(
): ViewModel() {
private val _selectedItem = mutableStateOf("HOME")
val selectedItem: State<String> = _selectedItem
private val _liveItems = mutableStateOf<List<LiveItem>>(emptyList())
val liveItems: State<List<LiveItem>> = _liveItems
private val _bannerItems = mutableStateOf<List<Int>>(emptyList())
val bannerItems: State<List<Int>> = _bannerItems
fun onItemSelected(item: String) {
_selectedItem.value = item
}
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
1. ViewModel의 정확한 책임
“UI의 상태를 관리하고, 유저의 액션을 처리한다”
ViewModel은 절대 직접 화면을 바꾸지 않고, 상태(State)를 가지고 있고 그걸 Composable이 구독해서 UI가 바뀌는 구조입니다.
2. Compose에서 ViewModel 상태 흐름
- StateFlow, MutableState, MutableStateFlow 등 상태를 어떻게 들고 있고
- by collectAsState() 또는 remember { mutableStateOf() }로 UI에 어떻게 바인딩하는지
kotlin
복사편집
val uiState by viewModel.uiState.collectAsState()
3. viewModelScope와 Coroutine 활용법
ViewModel에서 네트워크나 로직을 돌릴 때는 반드시 viewModelScope.launch 사용
→ 생명주기 안전하게 관리됨
kotlin
복사편집
fun onSubmit(email: String, pwd: String) {
viewModelScope.launch {
val result = useCase(email, pwd)
// 상태 업데이트
}
}
4. UiState 패턴: UI 상태 하나로 관리하는 구조
kotlin
복사편집
data class LoginUiState(
val isLoading: Boolean = false,
val success: Boolean = false,
val errorMessage: String? = null
)
→ ViewModel은 이 상태 하나만 업데이트
→ Compose는 이 하나만 구독
5. Event 처리 방식
일회성 이벤트 (Toast, Navigation 등)는 상태(State)로 표현하면 안 됨.
SharedFlow, Channel, LaunchedEffect 등을 사용해야 안전합니다.
kotlin
복사편집
private val _eventFlow = MutableSharedFlow<UiEvent>()
val eventFlow = _eventFlow.asSharedFlow()
sealed class UiEvent {
object Success : UiEvent()
data class ShowToast(val message: String) : UiEvent()
}
* MVVM 상태관리 (data class 형태일때)
1. uiState는 앱 상태 (현재 이름, 인사 문구 등)를 들고 있는 단일 데이터 덩어리입니다
kotlin
복사편집
data class GreetingUiState(
val name: String = "",
val greeting: String = ""
)
→ 이건 모든 UI 정보의 “스냅샷” 하나입니다.
→ Compose는 이 데이터 덩어리 하나만 관찰합니다.
2. var uiState by mutableStateOf(...)
kotlin
복사편집
var uiState by mutableStateOf(GreetingUiState())
private set
- mutableStateOf는 Compose가 관찰할 수 있는 변화 감지 가능한 값입니다.
- 즉, uiState가 바뀌면 자동으로 UI가 다시 그려집니다.
- by는 Kotlin의 **위임 문법 (delegation)**이고, 그냥 uiState.value 대신 uiState라고 쓸 수 있게 도와주는 문법입니다.
3. uiState = uiState.copy(...)
상태가 바뀌면, “기존 상태를 복사 + 새 값으로 일부만 바꿔서 다시 덮어쓰기” 하는 방식입니다.
kotlin
복사편집
uiState = uiState.copy(name = newName)
이렇게 해야 Compose가
“아! 상태가 진짜로 바뀌었네 → UI 다시 그려야겠다!” 라고 인식합니다.
❗ 그냥 uiState.name = newName 이렇게 하면 Compose는 변화를 감지하지 못합니다 → UI 안 바뀜
요약: 왜 이렇게 복사해서 다시 넣는가?
이유 설명
| Compose는 mutableStateOf()로 감싸진 전체 객체의 변경만 추적 | 내부 필드만 바꿔서는 안 감지됨 |
| copy()는 data class의 일부 필드만 바꾸는 안전한 방법 | 상태 불변성 유지 |
| 상태 흐름을 한눈에 보기 쉽게 유지 | 디버깅, 추적이 쉬움 |
kotlin
복사편집
uiState = uiState.copy(...)
“상태를 새로 만든다 → Compose에 알려준다 → Compose가 UI를 다시 그린다”
1. Repository Pattern
Repository Pattern은 다양한 데이터 소스(예: 로컬 DB, 원격 서버, 캐시 등)를 하나의 추상적인 인터페이스로 감싸는 패턴입니다. 이렇게 하면 ViewModel이나 UseCase 같은 상위 계층은 데이터가 어디서 오는지 몰라도 되고, 그냥 repositort.getDate()처럼 사용할 수 있습니다.
목적:
- 데이터 계층을 추상화한다.
- UI(ViewModel 등)는 데이터가 어디서 오든 신경 안 써도 됨.
- 다양한 데이터 소스(Room, Retrofit, 캐시 등)을 하나의 인터페이스로 통합.
구조 구성도
[ View / Composable ]
↓
[ ViewModel ]
↓
[ Repository ]
↙ ↘
[ Local DB ] [ API ]
핵심 구성 요소
- Repository Interface
- 데이터에 접근하는 메서드를 정의한 인터페이스.
- Repository 구현체 (Impl)
- 실제 데이터를 불러오는 구체 클래스 (예: Retrofit 호출, Room DB 등)
- DataSource (Local / Remote)
- API, DB, 파일 등 다양한 실제 데이터 저장소.
코드 예제 (기본 구조)
UserRepository.kt
kotlin
복사편집
interface UserRepository {
suspend fun getUserById(id: String): User
}
UserRepositoryImpl.kt
kotlin
복사편집
class UserRepositoryImpl(
private val api: ApiService,
private val dao: UserDao
) : UserRepository {
override suspend fun getUserById(id: String): User {
// 예시: API 우선, 실패 시 DB fallback
return try {
api.getUser(id).also {
dao.insertUser(it)
}
} catch (e: Exception) {
dao.getUserById(id)
}
}
}
장점
- ViewModel은 repository.getUser()만 호출하면 됨.
- API 구조가 바뀌어도 ViewModel은 영향 없음.
- 테스트할 때 FakeUserRepository 만들기 쉬움.
특징 설명
| 추상화 | 데이터 소스를 인터페이스로 감쌈 |
| 유연성 | 내부 구현(API, DB 등)을 교체해도 외부 영향 없음 |
| 테스트 | 유닛 테스트 시 Mock / Fake로 쉽게 대체 가능 |
| 재사용성 | 여러 ViewModel이 동일 Repository 사용 가능 |
1. Repository 예제
kotlin
복사편집
interface UserRepository {
suspend fun getUser(): User
}
2. ViewModel 구현
kotlin
복사편집
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user
init {
loadUser()
}
private fun loadUser() {
viewModelScope.launch {
val result = repository.getUser()
_user.value = result
}
}
}
3. Compose Composable
kotlin
복사편집
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val user by viewModel.user.collectAsState()
user?.let {
Text("Hello, ${it.name}")
} ?: CircularProgressIndicator()
}
3. AuthRepository.kt
- 인터페이스
kotlin
복사편집
interface AuthRepository {
suspend fun signup(request: SignupRequest): SignupResponse
}
4. AuthRepositoryImpl.kt
- Retrofit API를 호출하는 실제 로직
kotlin
복사편집
class AuthRepositoryImpl(
private val apiService: AuthApiService
) : AuthRepository {
override suspend fun signup(request: SignupRequest): SignupResponse {
return apiService.signup(request)
}
}
5. AuthApiService.kt
- Retrofit 인터페이스
kotlin
복사편집
interface AuthApiService {
@POST("signup")
suspend fun signup(@Body request: SignupRequest): SignupResponse
}
6. SignupRequest.kt / SignupResponse.kt
- DTO (Data Transfer Object)
- 서버에 보낼 데이터 구조와 응답 받을 구조
(Jetpack Compose + MVVM) 예
kotlin
복사편집
// 1. Repository 인터페이스
interface UserRepository {
suspend fun getUser(userId: String): User
}
// 2. Repository 구현체 (예: Retrofit 사용)
class UserRepositoryImpl(
private val apiService: ApiService
) : UserRepository {
override suspend fun getUser(userId: String): User {
return apiService.getUser(userId)
}
}
// 3. ViewModel
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _user = mutableStateOf<User?>(null)
val user: State<User?> = _user
fun loadUser(userId: String) {
viewModelScope.launch {
_user.value = userRepository.getUser(userId)
}
}
}
'Android' 카테고리의 다른 글
| 안드로이드 테스트 코드 작성 + Jetpack Compose UI 테스트 (1) | 2025.06.08 |
|---|---|
| DI - koin & Hilt (0) | 2025.05.25 |
| Kotlin Study (kotlin in action) (0) | 2025.04.29 |
| Android Studio - Jetpack Compose (0) | 2025.04.28 |
| Android study(1) (1) | 2025.02.28 |