MVP/MVVM/Clean Architecture 등 아키텍처 설계 혹은 적용 경험이 있으신 분 |
안드로이드 채용 공고를 보면 자주 등장하는 말이다.
그래서 아키텍쳐에 대해 자세하게 알아보려고 한다.
소프트웨어 아키텍쳐
소프트웨어를 구성하는 구성요소간의 관계를 관리하는 시스템의 구조이자 소프트웨어의 설계와 업그레이드를 통제하는 지침과 원칙이다.
말 그대로 소프트웨어를 개발할 때 지켜야 하는 규칙같은 것이다.
이러한 규칙을 정하고 지키는 이유는 협업을 할 때 본인이 마음대로 짠 코드를 다른 사람들이 본다면 그 사람들은 코드를 이해하기 어려워 할 것이다.
물론 협업이 아니더라도 코드의 수정이 매우 힘들거나 구조를 이해하기 힘들어진다.
이러한 이유 때문에 아키텍쳐 패턴이라는 개념이 나오고 사용되는 것이다.
실제로 아키텍처 패턴은 반복적으로 발견되고 재사용할 수 있는, 잘 정의된 속성을 가진 아키텍처 클래스를 의미한다.
안드로이드 아키텍쳐 패턴
안드로이드에서 쓰이는 아키텍쳐 패턴은 다음과 같다.
- MVC (Model - View - Controller)
- MVP (Model - View - Presenter)
- MVVM (Model - View - ViewModel)
대표적으로 MVVM이 MVC, MVP의 단점을 보완하기 위해 가장 많이 쓰이는 방법론이라고는 하지만, 어느 패턴이 더 낫다는 정답은 없다. 프로젝트의 상황에 맞게 적절한 패턴을 사용해서 개발하면 된다.
MVC (Model - View - Controller)
MVC 패턴은 가장 보편적인 아키텍쳐 패턴으로 알려져 있다.
- Model
애플리케이션에 사용되는 데이터와 처리 로직이 담겨있다.
View 또는 Controller와 묶이지 않기 때문에 재사용할 수 있다. - View (UI)
사용자에게 보이는 화면이다. - Controller
사용자로부터 입력을 받고, 모델에 의해 View 정의를 하게 된다.
웹에서 MVC 패턴의 구조는 위와 같지만, 안드로이드는 View와 Controller가 액티비티에 포함되어 있다.
MVC 패턴의 장점
단순한 패턴으로 러닝 커브가 낮아 구현이 쉽고 가장 빠르게 구현할 수 있다.
MVC 패턴의 단점
Controller와 View가 액티비티에 결합되어 있기 때문에 테스트가 어려워진다. UI 로직과 Controller 로직을 분리할 수 없기 때문이다.
마찬가지로 UI 로직이 바뀌어도 Controller 로직도 바뀔 수 있다.
View와 Model 사이의 의존성이 생긴다. 따라서 액티비티에 코드가 몰려 스파게티 코드가 될 수 있다.
안드로이드 초보자들에게는 MVC 패턴이 접근하기 매우 쉽기 때문에 학교나 서적이나 대부분 MVC 패턴으로 알려준다.
하지만 대규모 프로젝트를 하는 기업은 MVC 패턴을 잘 쓰지 않는다.
MVP (Model - View - Presenter)
MVP 패턴은 MVC 패턴의 단점을 보완하고자 등장한 패턴이다.
안드로이드에서는 컴포넌트 분리가 좀더 명확하게 이루어졌다고 할 수 있다.
- Model
MVC 모델과 동일한 개념이다.
애플리케이션에 사용되는 데이터와 처리 로직이 담겨있다.
View 또는 Controller와 묶이지 않기 때문에 재사용할 수 있다. - View (UI)
사용자에게 보이는 화면이다.
액티비티가 자연스럽게 View의 일부로 간주된다. - Presenter
Controller와 같은 역할을 하지만 Interface로 이루어져서 UnitTest에 자유롭다.
안드로이드에서 MVP 패턴은 아래와 같이 액티비티에서 Model을 호출하지 않고, Presenter를 통해 Model과 주고받는다.
MVP 패턴의 장점
Model과 View의 의존성이 없어졌기 때문에 수정에서 자유롭다.
UI와 Data가 확실하게 구분되어 있어 유지 보수면에서 간편하다.
MVP 패턴의 단점
애플리케이션이 커질수록 View와 Presenter 사이의 의존성이 강해진다.
기능이 많아지면 Presenter의 비중이 커지기 때문에 분리하기가 어렵다.
MVC와 다르게 컴포넌트가 구분되기 때문에 MVC보다 역할분리가 잘 되어있지만 Presenter와 View가 1:1 관계를 가지고 있어서 View가 늘어나는 만큼 Presenter도 늘어난다.
MVVM (Model - View - ViewModel)
MVVM 패턴은 MVC, MVP 패턴을 보완하여 나온 패턴이다.
MVC, MVP 패턴은 결국 View와 연관성이 있었지만 MVVM은 Presenter에 의존하지 않고 Observer Pattern을 이용해 객체의 변경이 일어날 때마다 UI가 갱신되도록 만들어졌다.
Observer Pattern - 상태 변화가 있을 때마다 메서드를 통하여 관찰 대상자가 직접 옵저버들에게 통지하여 상태를 동기화할 수 있도록 하는 디자인 패턴을 의미한다.
- Model
MVC 모델과 동일한 개념이다.
애플리케이션에 사용되는 데이터와 처리 로직이 담겨있다.
View 또는 Controller와 묶이지 않기 때문에 재사용할 수 있다. - View (UI)
ViewModel의 데이터들을 구독하고 있다가 객체가 변할 때 UI를 업데이트한다. - ViewModel
Model과 상호작용하며, View에 종속되지 않고 1:N 구조를 가진다.
View에 어떤 종속성도 가지지 않기 때문에 ViewModel을 다른 View에서도 활용할 수 있다.
안드로이드 권장 아키텍쳐에 따르면 MVVM 패턴을 권장하고 있다.
MVVM 패턴의 장점
Model과 View가 독립되어 있어서 유지보수가 간편하다.
ViewModel에서 View 코드가 없기 때문에 UnitTest를 쉽게 할 수 있다.
MVVM 패턴의 단점
간단한 UI에서도 ViewModel을 설계해야 해서 구현하기 매우 어렵다.
자 그럼 아키텍쳐 패턴의 종류도 알았고 MVVM이 안드로이드 권장 아키텍쳐 패턴인것도 알았는데 어떻게 뭘 해야해? 라고 궁금해 할 것이다.
물론 지금까지 설명한 패턴은 진짜 패턴 설명한거밖에 없다.
안드로이드 아키텍쳐는 이제부터 시작이다.
안드로이드 권장 아키텍쳐
안드로이드 권장 아키텍쳐에 따르면 아래 사진과 같다. 링크에 설명하는 그림과 다른거같지만 링크의 그림을 풀면 아래 그림이 된다.
각 구성요소가 한 수준 아래의 구성요소에만 종속됨을 볼 수 있다. 이러한 구조는 MVVM 패턴 구조를 따른다.
안드로이드에서는 MVVM 패턴을 구현하기 위해 AAC(Android Architecture Components)를 제공한다.
AAC를 사용하면 위와 같은 구조를 구현할 수 있다.
AAC (Android Architecture Components)
AAC란 안드로이드에서 2017년도에 발표한 라이브러리로 다음과 같이 구성되어 있다.
- Lifecycles(Easy handling lifecycles)
- LiveData(Lifecycle aware observable)
- ViewModel(Managing data in a lifecycle)
- Room(Object Mapping for SQLite)
- Paging(Gradually loading information)
- Databinding
- Navigation
- WorkManager
1. Lifecycles
생명주기 모니터링을 돕는 라이브러리이다.
Lifecycle Owner - Lifecycle 객체를 통해 다른 곳에서 해당 화면의 생명주기를 모니터링 할 수 있는데 자신의 생명주기를 담은 Lifecycle 객체가 Lifecycle Owner이다.
Lifecycle Observer - 화면 밖에서도 생명주기에 따른 동작을 정의하기 위해서는 원하는 클래스에 LifecycleObserver 인터페이스를 구현하고 넘겨받은 Lifecycle Owner 객체에 구현한 LifecycleObserver를 등록해야 한다.
2.LiveData
LiveData는 Obserable 형태로 사용하며, 안드로이드 Lifecycle에 따라 데이터를 관리한다. Activity, Fragment의 라이프 사이클을 따르기에 활동에 대한 처리를 알아서 관리해준다.
3. ViewModel
수명주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었다. ViewModel 클래스를 사용하면 화면전환과 같이 구성을 변경할 때도 데이터를 보존할 수 있다.
ViewModel과 Lifecycle, LiveData를 사용해서 MVVM 패턴을 제작하면, 다음과 같은 액티비티 생명주기를 가지게 되며 생명주기 관리가 매우 간결해진다.
4. Room
SQLite 객체 매핑 라이브러리이다. Room을 사용해서 사용구 코드를 피하고 SQLite 테이블 데이터를 자바 객체로 쉽게 변환할 수 있다. Room은 SQLite 문의 컴파일 시간 확인을 제공하며 RxJavam, Flowable, LiveData, Observable을 반환할 수 있다.
5. Paging
페이징 라이브러리를 사용하면 로컬 저장소에서나 네트워크를 통해 대규모 데이터세트의 데이터 페이지를 로드하고 표시할 수 있다. 이 방식을 사용하면 앱에서 네트워크 대역폭과 시스템 리소스를 모두 더 효율적으로 사용할 수 있다.
6. Databinding
선언형 형식으로 Data를 UI에 쉽게 Binding하기 쉽게 해주며 findViewById에 의한 객체 획득 번거로움을 제거해주는 라이브러리이다.
여담으로 공식적으로 Databinding은 Compose가 나온 뒤로 구글에서도 버리는 느낌이 든다.
Compose에 대해 추가로 설명하자면 위 사진과 같이 액티비티 내에서 UI를 구현할 수 있는 라이브러리이다.
안드로이드에서 가장 밀고있는 라이브러리이며, 점점 Compose로 전환되는 분위기이다. XML 코드가 사라지고 액티비티 내에서 동작하기 때문에 Databinding 방식이 완전히 다르다.
7. Navigation
사용자가 앱 내의 여러 콘텐츠를 탐색하고 그곳에 들어갔다 나올 수 있게 하는 상호작용을 의미한다. 또한 View의 흐름을 직관적으로 보여주기 때문에 앱의 동작흐름을 파악하는데도 도움이 된다.
8. WorkManager
WorkManager는 지속적인 작업에 권장되는 솔루션이다. 앱이 다시 시작하거나 시스템이 재부팅될 때 작업이 예약된 채로 남아있으면 그 작업은 유지된다. 대부분의 백그라운드 처리는 지속적인 작업을 통해 가장 잘 처리되므로 WorkManager는 백그라운드 처리에 권장하는 기본 API이다.
MVVM + AAC를 사용해서 애플리케이션을 구성하면 다음 예시처럼 작성하면 된다.
두개의 버튼과 텍스트뷰가 있으며 버튼을 누르면 텍스트뷰의 값이 변경되는 간단한 애플리케이션이다.
activity_main.xml
<TextView
android:id="@+id/text_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="MainActivity"
android:textSize="50dp" />
<Button
android:id="@+id/btn_minus"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
Databinding을 사용해도 되지만 구조 설명을 위해 쓰지 않았다.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private var mBinding: ActivityMainBinding? = null
private val binding get() = mBinding!!
lateinit var mainActivityViewModel: MainActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
mainActivityViewModel = ViewModelProvider(this)[MainActivityViewModel::class.java]
mainActivityViewModel.currentValue.observe(this, Observer {
binding.textResult.text = it.toString()
})
binding.btnPlus.setOnClickListener {
mainActivityViewModel.updateValue(actionType = ActionType.PLUS)
}
binding.btnMinus.setOnClickListener {
mainActivityViewModel.updateValue(actionType = ActionType.MINUS)
}
}
}
액티비티에서는 ViewModel을 연결시켜주고 Observe로 text 값을 구독하는 형태이다.
리스너 내부에 updateValue 함수를 호출해서 text 값을 변동시켜준다.
MainActivityViewModel.kt
enum class ActionType {
PLUS, MINUS
}
class MainActivityViewModel : ViewModel() {
private val _currentValue = MutableLiveData<Int>()
val currentValue: LiveData<Int>
get() = _currentValue
init {
_currentValue.value = 0
}
fun updateValue(actionType: ActionType) {
when (actionType) {
ActionType.PLUS ->
_currentValue.value = _currentValue.value?.plus(1)
ActionType.MINUS ->
_currentValue.value = _currentValue.value?.minus(1)
}
}
}
ViewModel에서는 View에 관한 코드가 하나도 없으며 오로지 data에 관한 코드만 있다. 따라서 UnitTest도 간편하다. 추가로 Model이 있다면 Model을 작성해서 연결시켜주면 된다.
기존에는 Java에 MVC 패턴을 사용해서 제작했지만, 코틀린을 배우게 되면서 MVVM 패턴을 사용해보고 싶었는데 구조가 다르면 코틀린을 이해하는데 걸림돌이 될까봐 일부러 코틀린에 기존 MVC 패턴을 사용해서 프로젝트를 진행했다.
코틀린과 코루틴, Lifecycle까지 이해한 뒤에 프로젝트를 MVVM 패턴으로 리팩토링하는 시간을 가졌다.
프로젝트 전체 코드를 다 갈아 엎는 수준의 리팩토링이였지만 결과는 만족스러웠다.
LiveData와 Observer를 이용해서 View와 ViewModel간의 종속성을 제거하고, 대부분의 코드가 모듈화되었다.
또한 패키지를 Data 계층과 Presentation 계층으로 나누어 보기 깔끔해졌다.
그리고 ViewModel에서 Retrofit 통신과 UI스레드 위에 그리던 타이머를 다음 사진처럼 처리하게 되면서 더욱 관리가 쉬워졌다.
Coroutine을 통해 LifeCycle을 알아서 관리해주고 LiveData를 통해 실시간으로 View에 그려주기 때문에 생명주기 관리에 신경을 덜 써도 됐다.
참고
https://meetup.nhncloud.com/posts/342
https://velog.io/@heetaeheo/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-AAC
'Android' 카테고리의 다른 글
[Android] Navigation Bar와 Bottom Sheet를 같이 쓸 때 문제점과 주의사항 (0) | 2023.03.24 |
---|---|
[Android] LiveData VS Kotlin Flow with Chat GPT (1) | 2023.03.11 |
[Android] 안드로이드 에러 분석 방법 및 에러 종류에 따른 설계 방법 (0) | 2023.02.19 |
[Android] REST API 예외처리 (0) | 2023.02.10 |
[Android] Retrofit Multipart로 이미지 전송하기 (0) | 2023.02.05 |