들어가기 전
안드로이드 프로젝트를 하면서 MVVM , Clean Architecture, Dependency injection etc... 이런 키워드들을 정말 많이 들었다. Sopt 처음 시작할 때 Clean Architecture를 적용한 프로젝트를 진행하고 싶다고 말할 정도로 나는 이 부분에 대한 공부를 하고싶었다. 이번 앱잼에서 MVVM 패턴을 적용했다는 점(Hilt (x))에서 반은 이뤘지만, 어떻게보면 왜 이렇게 사용하는지 조차 모르고 사용한 부분들이 많았다. 이번기회에 제대로 이해되지 않은 부분들도 다 이해하고 넘어가고자 이 포스팅을 작성한다.
MVVM (Model - View - ViewModel)
MVVM 패턴은 MVP 패턴에서 파생된 패턴으로 비즈니스 로직과 프레젠테이션 로직을 사용자 UI로부터 분리하는 것이 목표이다.
Businees Logic과 Presentation Logic의 분리
Presentation Logic
프레젠테이션 로직은 비지니스 오브젝트(비지니스 로직)가 팝업 화면과 드롭 다운 매뉴 중어떤 것을 선택할 것인지와 같이소프트웨어 사용자에게 표시되는 로직을 말한다. 프레젠테이션 로직에서 비지니스 로직을 분리하는 것은소프트웨어 개발 및 프레젠테이션과 컨텐츠를 분리하려하는 사례의(an instance of) 중요한 관심사이다.—위키피디아(영문판), Presentation Logic
브라우저에 보이는 화면처럼 GUI 화면을 구성하는 코드를 의미한다.
Businees Logic
비지니스 로직(Business Logic) 또는 도메인 로직(Domain Logic)은현실 세계에서 어떻게 데이터를 만들고 저장하고 바꿀 것인지에 대한 비지니스 규칙(Business Rules)을 인코드(Encodes) 한,소프트 웨어 안의 프로그램의 한 부분이다.—위키피디아(영문판), Business Logic
데이터 생성, 저장 변경에 대한 비즈니스 규칙을 담은 코드를 의미한다.
Presentation Logic과 Business Logic을 한 클래스에 모두 구현하는 코드를 작성하면 유지보수 측면에서 어렵기 때문에 모듈화 시켜서 구현한다.
MVVM에서 어떻게 두 로직을 분리하는가?
MVVM 구성요소
MVVM 은 사진과 같이 View - ViewModel - Model 로 구성되어 있다.
사용자가 화면에 입력을 하면 View에서 사용자의 입력값을 받는다. 사용자의 입력값을 전달하고 ViewModel의 데이터를 관찰한다. (LiveData, Flow 등을 활용) ViewModel은 Model 로 데이터를 조작 한다.
화면에 값을 출력하게 될때도 Model로 부터 받은 데이터를 ViewModel이 가공해 View에게 알려 제공할 수 있다.
단 ViewModel이 UI를 조작하지 않는다. View에서는 ViewModel에 데이터를 관찰하여 UI를 업데이트 시킨다.
LiveData를 이용해서 View를 Subscribe 하고, 데이터가 바뀔 때 마다 반응하도록 메서드를 수행하게 한다. 이 때 Android jetpack 구성요소인 DataBinding이 활용된다.
앞서 말한 비즈니스 로직과 프레젠테이션 로직에 빗대어 말하면 Model에서 데이터를 처리하는 부분을 비즈니스 로직, ViewModel이 Model로부터 받은 데이터를 가공해 View에게 제공하는 부분을 프레젠테이션 로직이라 할 수 있다.
즉 ViewModel은 Model과 View 사이에서 데이터를 관리와 제공을 해주는 것이다.
따라서 View와 ViewModel 사이에서 View만 ViewModel에 의존성을 갖게 하고, ViewModel은 View에 대한 의존성을 제거해 MVVM 패턴의 목표를 달성할 수 있다.
MVVM 패턴의 장점
유지보수에 용이하다.
유지보수의 핵심은 각 부분이 독립적이 되어 한 부분의 변경이 다른 부분에 영향을 미치지 않도록 하는 것이다.
MVVM은 ViewModel 단에서 View와 Model 사이의 의존관계를 끊어주었고, ViewModel 또한 View에 의존성을 갖지않는다. 각 부분이 독립적으로 존재해 다른 부분에 영향을 미치는 것을 줄였다.
MVVM의 ViewModel vs AAC ViewModel
과거에 MVVM을 얕게나마 공부하고 프로젝트에 적용해야지! 라는 마음을 먹고 연습 프로젝트를 만든적이 있다.
예시로 LoginActivity, LoginViewModel, LoginRepository를 만들었고, 나는 ViewModel을 이용해 MVVM 패턴을 적용했다는 착각을 한 적이 있다.
결론부터 말하면 MVVM의 ViewModel은 안드로이드에서 제공하는 AAC ViewModel과 다르다.
MVVM ViewModel :
MVVM 의 ViewModel은 View에 연결할 데이터를 제공하고 상태 변화가 생기면 View에게 상태 변화를 알려준다.
View는 ViewModel의 상태 변화를 감지하고 UI를 업데이트 할 수 있다. 옵저버 패턴을 사용하기 때문에 View는 ViewModel을 알지만, ViewModel은 View를 모른다.
ViewModel과 View는 보통 1:n의 관계이다. 그러므로 하나의 ViewModel에 여러개의 View 사용이 가능하다.
-> 이 부분에 대한 다른 블로그 의견
바로 위에서 “뷰모델은 그 액티비티에서 딱 하나만 존재하게 된다”라고 설명했는데 오해의 소지가 있을수 있을 것 같아서 추가적으로 언급하자면, 이 말이 뷰 한개에 뷰모델 유형이 딱 한개 존재해야 한다는 것은 아니다. 예를 들어 SignUpActivity 뷰가 있다고 했을 때, 그에 대응하는 SignUpViewModel 딱 하나만이 존재해야 한다는 것은 아니다. MVVM패턴에서는 뷰와 뷰모델은 1:n 관계이기 때문이다. 개발자는 필요에 따라서 얼마든지 UserPersonalDataViewModel, UserAccountViewModel 등등 여러가지 뷰모델로 나눠서 사용이 가능하다. 다만 UserPersonalDataViewModel을 한번 생성하면, 그 액티비티에서 UserPersonalDataViewModel을 여러번 생성해도 그것은 싱글톤이기 때문에 하나의 객체만 계속 사용된다는 것이다. 그리고 한 가지더, 구글은 하나의 뷰에 하나의 뷰모델만 두고 사용하는 것을 권장한다. SignUpActivity가 있다면, SignUpViewModel 하나만 놔두고, 그 안에 여러 Model과 LiveData를 사용하는 것을 권장하고 있다. 이것은 원래의 MVVM 원칙과 맞지 않는 내용이다. 더 좋고 더 나쁜 방식은 없다. 구글이 추천하는 방식과 원래 MVVM의 원칙 중 어떤것이 더 자신의 프로젝트에 맞는지는 개발자가 판단할 몫이다.
AAC ViewModel은 ViewModelProviders를 사용해서 ViewModel을 만드는데, 이렇게 만들어진 뷰모델은 그 액티비티에서 딱 하나만 존재하게 된다. 액티비티 한 개 내에서만 유효한 싱글톤인 셈이다.
따라서 ViewModel은 View와 Model 사이에서 데이터를 관리 및 바인딩 해주는 요소인 것이다.
AAC ViewModel :
AAC ViewModel은 Android 생명주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었다. 기존 UI컨트롤러(Activity / Fragment) 가 파괴되고 재생성될 때 데이터 관리가 어려웠던 부분들을 해결해준다.
Activity에서 onCreate() 메서드를 처음 호출할 때 ViewModel을 요청한다. 그리고 시스템은 활동 기간 내내(예: 기기 화면이 회전될 때) onCreate() 메서드를 여러 번 호출할 수 있다. Activity가 처음 요청되었을 때부터 활동이 끝나고 폐기될 때까지 ViewModel은 존재한다.
그러므로 화면회전과 같은 View가 파괴되고 새로 생성되는 경우에도 데이터를 유지할 수 있다.
MVVM의 ViewModel과 AAC ViewModel이 헷갈리는 이유는 MVVM 패턴을 구현하기 위해 AAC ViewModel을 사용하기 때문이다.
MVVM 의 ViewModel은 View에 데이터를 바인딩 해주는 역할을 한다. AAC ViewModel 내에 옵저브 필드나 LiveData를 사용해 데이터 바인딩을 해준다면 MVVM 패턴을 구현할 수 있다.
따라서 MVVM의 ViewModel과 AAC ViewModel이 동일하다고 볼 수 없다.
ViewModel 객체는 뷰 또는 LifecycleOwners의 특정 인스턴스화보다 오래 지속되도록 설계되어있다.
이런 설계는 ViewModel이 생명주기와 뷰 객체에 대해 모르게 때문에 ViewModel에서 Application Context를 사용해야한다.
그렇다면 AAC ViewModel을 어떻게 사용할 수 있을까?
AAC ViewModel 사용 예제
AAC ViewModel 인스턴스 제공 예제
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.CreationExtras
class MyViewModel(
private val myRepository: MyRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// ViewModel logic
// ...
// Define ViewModel factory in a companion object
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
// Get the Application object from extras
val application = checkNotNull(extras[APPLICATION_KEY])
// Create a SavedStateHandle for this ViewModel from extras
val savedStateHandle = extras.createSavedStateHandle()
return MyViewModel(
(application as MyApplication).myRepository,
savedStateHandle
) as T
}
}
}
}
ViewModel의 인스턴스 생성 예제
import androidx.activity.viewModels
class MyActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels { MyViewModel.Factory }
// Rest of Activity code
}
이렇게 ViewModel을 사용할 수 있다.
Clean Architecture
안드로이드 앱은 크기가 커지기 때문에 앱을 확장하고 앱의 유지보수 및 테스트를 위해 아키텍처를 정의하는 것이 중요하다. 앱 아키텍처는 앱의 부분과 그 부분에 필요한 기능간의 경계를 정의한다. 따라서 가장 중요한 원칙은 관심사 분리 이다.
내가 과거 프로젝트에서 그러했듯이 Activity/Fragment 에 모든 코드를 작성하는 일은 흔히 일어나는 일이다. Activity/ Fragment 와 같은 UI 기반 클래스는 UI 및 운영체제 상호작용을 처리하는 로직만을 포함해야한다.
Clean Architecture 는 변경에 용이와 의존성을 줄이기 위해 Presentation Layer / Domain Layer / Data Layer로 관심사를 분리한다.
그림2 원에서 각 레이어의 의존성은 안쪽으로만 향해야한다. 따라 가장 안에 있는 엔티티는 비즈니스 로직들의 집합이라고 볼 수 있다.
Clean Architecture in Android
안드로이드에서 클린아키텍처를 위 그림과 같이 사용한다. Presentation -> Domain, Data -> Domain 으로 의존성을 갖고 있다.
Presentation Layer
사용자 인터페이스 및 입력에 대한 처리 등 UI와 관련된 부분을 담당한다.
presentation layer는 domain 계층에 대해 의존성을 갖는다.
- View → Activity, Fragment
- ViewModel
- 사용자 요청에 따른 데이터를 불러오는 메서드를 호출한다.
- 비즈니스 로직이 포함되어서는 안된다.
- 인터페이스의 메서드를 호출하기만 한다.
- 메서드의 구현체는 Data Layer에 있음.
Domain Layer
domain layer는 비즈니스 로직을 담당해 어플리케이션 비즈니스 로직에서 필요한 UseCase 와 Model을 포함한다.
안드로이드의 의존성을 갖지 않고 순수 Kotlin 코드로 구성되며 다른 어플리케이션에도 사용할 수 있다.
- Repository Interface
- UseCase
Data Layer
domain layer에 repository, data source의 구현체를 포함하고 있다. 서버와의 통신도 data layer에서 이루어진다.
mapper를 통해 data 계층의 모델을 domain 계층의 모델로 변환해주는 역할도 한다.
- Repository Impl
- Data Source
- Mapper
- DTO
- Server API Interface
여기까지 왔을 때 UseCase가 뭐지? 그냥 Repository에서 ViewModel로 가면되는거 아닌가? 라는 생각이 든다.
UseCase 란 ?
서비스를 사용하고 있는 사용자(User)가 해당 서비스를 통해 하고자 하는 것을 의미한다.
블로그라는 서비스가 있다면, 사용자는 블로그에서 게시글 '검색', '댓글'남기기, '공유'버튼 누르기 등 여러 행동을 수행할 수 있다. 이런 사용자가 서비스에서 수행하는 것들을 UseCase라 할 수 있다.
그렇다면
UseCase 를 사용하는 이유는 무엇일까 ?
1. ViewModel 역할 쉽게 파악 가능
해당 ViewModel 이 어떤 것을 하고자 하는지 직관적으로 파악 가능하다.
Screaming Architecture = 구조만 보고도 소프트웨어가 무엇인지 알 수 있도록 명확해야 한다는 것이다.
UseCase의 이름을 직관적으로 지어 ViewModel에서는 해당 UseCase를 파라미터로 전달받을 수 있다. 이렇게 전달받게되면 ViewModel에서 어떤 일을 하는지 파라미터만으로 확인이 가능하다.
이렇게 구조를 만들면 유지보수 측면을 넘어 협업에서도 더욱이 좋은 효과를 얻을 수 있다.
2. 의존성 줄이기 가능
ViewModel은 UseCase를 파라미터로 전달받아 사용할 수 있다.
만약 UseCase를 사용하지 않으면 ViewModel에서 Repository를 전달받아 사용하게 되고, 파라미터의 Repository가 수정된다면 ViewModel 역시 수정이 필요할 경우가 높다.
하지만 UseCase를 사용하면 영향이 있는 UseCase의 부분만 수정하면되기 때문에 의존성이 줄어든다.
그렇다면 이렇게 Repository의 메서드만 호출하고 아무것도 하지 않는 UseCase도 생성해야하나?
class CommentUseCase( private val repository: CommentRepository ) {
suspend operator fun invoke(): List<Comment> =
repository.getCommentList()
}
일관성과 미래에 있을 변경에 대한 코드 보호를 위해서 생성하는게 좋다.
클린 아키텍처에서 중요한 점은 요구사항 변경에 따른 수정을 최소화 하는 것이므로 Repository 변경에 있어 UseCase를 사용하면 수정을 최소화 할 수 있다.
위 예시 코드에서 suspend fun을 사용할 수 있지만 invoke 함수를 사용하면 ViewModel에서 클래스의 이름을 함수처럼 사용할 수 있다. 이는 가독성 측면에서 좋아 어떤 역할을 하는지 바로 파악할 수 있다.
fun getCommentList() = viewModelScope.launch {
commentUseCase(). //...
}
정리하자면 UseCase를 사용하면 코드 파악과 의존성을 낮춰 유지보수에 용이하기 때문에 사용하는게 좋다!
마치며
유지보수에 용이한 코드를 작성하기 위해 MVVM 과 Clean Architecture에 대해서 공부해보았다. 새로 알게된 점이 크게 4가지인데 첫번째로 자세한 이유를 모르고 나눴던 Presentation layer/ Domain layer/ Data layer 의 존재이유와 가 각 계층에 있는 요소들의 역할에 대해 제대로 파악할 수 있었다.
두번째로 AAC ViewModel을 공부하면서 앱잼때 그냥 사용했던 ViewModel Factory의 존재 이유도 알게되었다. 아마 ViewModel 인스턴스를 제공하는데 사용할 때마다 ViewModel class에 위 예제 코드처럼 companion object로 넣을 수 없으니 ViewModel Factory라는 클래스에 if문으로 여러 ViewModel을 넣어 인스턴스를 제공한 것같다.
세번째로는 아키텍처에서 유지보수를 위해 의존성이라는 개념이 중요함을 깨달았다. 다음 포스팅에서 Dependency Injection 에 대한 공부를 위해 의존성에 대해서 자세히 다룰거지만, 이유를 몰랐던 Repository interface와 Impl 의 분리도 의존성과 관련되어 있음을 새롭게 알게되었다.
네번째로는 UseCase 를 제대로 이해할 수 있었다. ViewModel -> Repository로의 구현을 계속 해왔는데, 여러 장점들을 바탕으로 UseCase를 사용하면 좋을 것 같다.
이건 공부 내용과 별개로 나는 원래 새로운 지식을 마주하면 한번에 깊게 끝까지 보기보다는 필요한 내용 또는 사용법만 보는 편이였다. 그렇게 공부하는건 초반에는 좋을지라도 점차 지식에 대한 한계를 느끼고, 나 스스로 발전이 없는 느낌이 들었다. 이제는 이제껏 경험을 바탕으로 모르는것을 파고파서 깊이있게 공부하려고한다.
참고 레퍼런스
'Android > 공부' 카테고리의 다른 글
[Android] UI State란 무엇일까? (0) | 2023.08.16 |
---|---|
[Android] Dependency Injection이란? + Hilt (1) | 2023.08.07 |
[Android] 알림 커스텀과 안드로이드 13 알림 권한 설정 (0) | 2023.07.21 |
[Android] Intent 정리하기 (0) | 2023.04.26 |
[Android] Retrofit2 싱글톤패턴 적용하기 (0) | 2022.11.26 |