Android Pattern 에 대한 이야기는 정말 많이 나오는 주제입니다.
저 역시, Pattern 없이 업무를 진행한 적도 있고, 패턴을 하나씩 적용해본 경험도 있습니다.
패턴을 써보니 확실히 좋아서 패턴이 우리 곁에 자리잡게 되었을거에요.
사실 어떤 패턴을 적용하든, 적용하지 않든 결과물은 만들 수 있습니다.
우리가 흔히 클린 아키텍처, 패턴, 클린코드를 중요하게 생각하는 이유는 바로 생산성 때문이죠.
패턴은 결국 생산성을 높이기 위한 고민의 결과라고 볼 수 있어요.
생산성이 높다는 것은 많은 의미를 내포합니다.
유지보수가 수월하고, 신규 기능 추가도 편하고, 가독성도 좋겠죠.
패턴은 의존성에 대해 고민하며, 우리가 실수할 가능성을 줄여주는 방향으로 진화해왔습니다. 의존성이 복잡하면, 우리는 개발을 하면서 우리 뇌 속에 많은 것들을 올려두고 생각해야합니다. 컴퓨터로 비유하면, 메모리에 이것저것 다 올려놓아야하는거죠.
의존성을 줄인다는 것은 결국 우리가 다른 부분들에 정신을 흩트리지 않고, 현재 작업하는 하나에만 집중할 수 있도록 도와준다고 볼 수 있습니다.
현재는 MVVM 이 많이 사용되고, MVI 도 괜찮은 패턴이라 인식되지만, 이 또한 또 바뀔 수 있습니다.
현재의 MVVM 과 MVI 패턴이 오기까지의 여정을 한번 살펴볼게요.
Android 를 처음 개발할 때 Activity 에 Hello world! 를 띄우며 개발을 시작했던 기억이 있습니다.
실무를 시작할 때도, Empty Activity 하나를 만들어두고, xml 파일에 레이아웃을 열심히 작성하고, Activity 에서는 findViewById 로 뷰 참조를 잔뜩 불러와서 필요한 데이터를 어떻게든 가져와서 뷰에 표시해줬죠.
안될 것은 없지만.. 우리가 실무를 할 때는 개발 기본서의 샘플 정도 수준에서 끝나지 않고, 하나의 화면에도 다양한 요구사항이 나오게됩니다.
나름대로 함수를 만들고, 클래스를 만들어가며 작업해도, 결국 Activity 는 점점 비대해지고, 새로운 크루가 왔을 때, 이 Activity 는 마치 잔뜩 어질러진 집 안을 보는 것 같을거에요.
기존 담당자는 어질러진 집 안에도 나름의 규칙과 히스토리가 있기에, 어느정도 생산성은 유지할 수 있겠지만, 새로운 크루는 '다시 만들고 싶다' 라는 생각이 들거에요. 말 그대로 생산성은 바닥을 치게 됩니다.
사실, 저는 실무에서 MVC 로 개발한 기억은 없습니다.
개념을 들어본 적은 있어서, 개발했을 수도 있지만 기억이 없는 것으로 보아 흉내만 내거나, 큰 효과를 보지 않았던게 아닐까 싶습니다.
그래도, 짧게 짚고 넘어가자면
Android Model-View-Controller 패턴은 나름대로 코드를 분리하고 의존성 관계를 적립한 첫 시도라고 볼 수도 있겠습니다.
화면에 표시하기 위해 우리가 가져와야하는 데이터와 관련된 부분들을 Model이라고 합니다.
User 클래스와 같은 어떤 하나의 객체를 의미하는 것이 아니라, Remote 또는 Local 에서 데이터를 얻어오는 등의 과정은 모두 Model 의 역할이 됩니다.
만약 Activity 에서 Remote/Local DataSource, Preference 등에서 데이터를 가져오는 과오가 있었다면, Model 이라는 아이로 분리를 시도하게 됩니다.
Model은 View와 Controller를 알지 못합니다. 순수하게 어떤 조회 요청이 오면 데이터를 던져주는 역할을 합니다. 따라서, Model 은 재사용이 가능합니다!
Model에는 아래와 같은 function이 있을겁니다.
fun getUserName(userId: String): String = remoteDataSource.fetchUserName(userId)
fun saveUserName(userId: String, userName: String): Boolean = remoteDataSource.saveUserName(userId, userName)
Android 구조상 Activity 가 가장 먼저 View 에 해당한다고 볼 수 있어요.
Fragment도 포함되고, 그 외에 모든 View Component 는 View 에 해당한다고 볼 수 있습니다.
View는 Model에서 데이터를 가져와서 화면에 그려줍니다.
fun showUserName(userId: String) {
val userName = model.getUserName(userId)
view.text = "안녕하세요, ${userName} 님!"
}
Controller는 유저와 소통하는 채널이자 View와 Model의 중개자입니다.
Android의 경우에는 Activity 즉, View에서 UserInputs를 받는 경우가 대부분입니다.
그래서, 대부분의 MVC 설명 블로그를 보면, Activity가 View와 Controller 역할을 모두 수행하여, Android에서 MVC는 적절하지 않다는 이야기도 많습니다.
아래 예시는 buttonA를 클릭하면 사용자 이름을 보여주는 기능과 buttonB를 클릭하면 사용자 이름을 저장하는 기능입니다.
전자의 경우는 UserInput(클릭)을 받아서, model에서 데이터를 가져와서 화면에 그려주게 되고,
후자의 경우는 UserInput(클릭)을 받아서, model을 통해서 유저 이름을 업데이트하게 됩니다.
Activity 안에 있지만, Controller의 역할을 수행하는 부분이라고 생각하면 될 것 같습니다.
buttonA.setOnClickListener(v -> view.showUserName(userId))
buttonB.setOnClickListener(v -> {
if (model.saveUserName(userId, name))
view.showUserName(userId)
})
MVC의 가장 큰 장점은, Model이 재사용 가능해진 점입니다.
재사용이 가능하다는 것은 View, Controller와의 의존성이 없다는 뜻이고,
그 말은 비대했졌던 Activity 안에서 일부 코드의 책임이 분리되어 Model로 이동했음을 뜻합니다!
반면, View와 Controller는 아직 조금 아쉽습니다.
Android 특성상 View와 Controller가 같이 관리되긴 하지만, 일단 그래도 분리해서 생각해볼게요.
View는 Model을 알고 있어야합니다. Model에서 가져와서 그려야하니까요.
model.getUserName()
Controller도 Model을 알고 있어야합니다. Model에게 변경사항을 알려야하니까요.
model.saveUserName()
즉, View 와 Controller 모두 Model에 의존성이 있습니다.
View와 Controller 사이의 의존성은 어떨까요?
Controller는 View를 알고 있습니다.
view.showUserName(userId)
View는 Controller를 알지 못합니다.
만약 View 영역을 책임에 맞게 여러개로 나누었다면, Controller는 여러개의 View를 알고 있어야겠죠.
MVC는 나름대로 역할과 책임을 분리하고, 의존성을 조금 정리했습니다.
가장 큰 장점은, Model 이라는 아이의 등장으로, 데이터와 티키타카를 하는 코드들을 한 켠으로 모아둘 수 있었습니다. 그리고, Model은 View 와 Controller에 관심이 없으니 재사용도 가능하죠.
단점은 View 와 Controller 가 Android 특성상 Activity 내에 공존하게 되고,
둘 다 Model 에 의존성이 있다는 것입니다.
또한, Controller 역시 View에 의존성을 갖고 있죠.
일부 책임을 나눠서 코드를 분리해서, 가독성은 조금 좋아졌지만 Activity는 여전히 조금 뚱뚱합니다.
MVC에서 View는 Model에 대한 의존성이 있고, Controller는 View와 Model에 대한 의존성이 있습니다.
가장 아쉬운 점은 View가 Model을 알고 있어야한다는 점입니다.
의존성 자체도 아쉽지만, 이렇게 되면 화면을 담당하는 View가 Model로 부터 데이터를 직접 가져와야하고, 이 과정에서 UI 로직과 비즈니스 로직이 혼재하게 됩니다.
비즈니스 로직이 서비스의 스펙에 가깝다면, UI 로직은 화면에 어떻게 표현할지에 대한 로직으로 볼 수 있습니다.
Model에서 가져온 데이터를 View에서 바로 그리게 되면, 이 부분이 모호해질 수 밖에 없습니다.
예를 들어볼게요.
User 정보를 가져와서 가입한지 1년 이상인 경우는 충성고객(왕관표시), 1년 미만인 경우는 신규고객으로 구분해서 화면에 표시해줘야한다고 해볼게요.
model.getUser(userId) 를 통해서 가져온 유저 정보에는 joinedAt 과 같은 필드도 같이 있을거에요.
충성고객과 신규고객을 1년 기준으로 구분하는 로직은 서비스의 스펙에 해당하는 비즈니스 로직이고, 이 구분을 화면에 표시하기 위해서 충성고객 옆에는 왕관을 그려준다고 하면, 이는 UI 로직이라고 볼 수 있겠어요.
MVC 에서는 위 2가지 처리가 모두 View 에서 진행이 됩니다.
비즈니스 로직과 UI 로직이 같이 혼재하게 되죠.
그래서 이어서 나온 MVP 에서는 UI 로직과 비즈니스 로직을 분리하고자합니다.
분리하기 위해서는 View와 Model의 의존성을 없애야합니다.
Model은 MVC의 Model과 같습니다.
여전히, 재사용성이 높고 의존성이 없습니다.
Activity가 View의 역할만을 하게됩니다.
사용자와 상호작용도 View에서 하게됩니다.
무엇보다, MVP에서는 View가 Model에서 직접 데이터를 가져오지 않습니다.
이제 View는 Model과의 의존성이 끊어졌습니다.
대신, 사용자 이벤트가 View를 통해서 Presenter로 전달되어야하기 때문에, View는 Presenter를 알게됩니다.
View에서 발생한 사용자 이벤트는 Presenter를 거쳐서 Model에게 전해집니다.
Model에서 제공받은 데이터는 Presenter가 View에 렌더링을 지시하게 됩니다.
즉, Presenter는 Model과 View에 의존성을 갖게됩니다.
의존성 측면에서는 MVC의 Controller와 동일합니다.
Model은 여전히 의존성이 없습니다.
View는 Presenter에 사용자 이벤트를 전달해야해서 의존성이 있습니다.
Presenter는 Model에서 데이터를 가져와서 View에 렌더링을 지시하기 때문에 Model과 View에 의존성이 있습니다.
View와 Model의 의존성은 완전히 분리되었습니다.
이제, 비즈니스 로직과 UI 로직이 혼재될 걱정은 줄었습니다.
UserInputs -> View <-> Presenter -> Model
앞선 예제로 돌아가보면,
Presenter는 model.getUser(userId) 로 유저 정보를 얻어올 것입니다.
Presenter는 Model에서 받은 유저 정보를 joinedAt 1년을 기준으로 loyalCustomer과 normalCustomer로 구분하여 View에 단순히 렌더링만 지시하게 됩니다.
View에는 loyalCustomer인 경우 왕관을 보여주는 UI 로직이 포함되어있겠죠.
MVP는 Presenter의 이름에 걸맞게, UI 로직을 잘 분리시켜주었습니다.
View는 이제 단순히 그리기만 하거나, 이벤트를 Presenter로 전파하는 일만 담당하게 됩니다.
MVP의 단점은 View와 Presenter의 상호 의존성에 있습니다.
상호 의존적이기에 1:1의 관계가 생기게 되고, View와 Presenter 모두 재사용이 어렵게 됩니다.
MVP는 View와 Presenter의 강한 결합도 아쉽지만, 실무에서 MVP를 적용하며 느꼈던 점은 Presenter의 덩치가 점점 커지는 것이었습니다.
View로부터 넘어온 이벤트를 처리하는 일, Model로부터 받은 데이터를 가공해서 View에 렌더링을 지시하는 일. Presenter는 2가지 책임을 지고 있습니다.
SOLID 원칙의 ISP(Interface Seperation Principle)에 따라
Presenter를 2개의 인터페이스로 분리하여, 이벤트 전파 관련 인터페이스, 화면에
그리기 위한 인터페이스를 사용하기도 했습니다.
2개 인터페이스의 구현체는 하나였구요.
인터페이스 분리를 통해 코드 가독성은 조금 좋아지긴 했지만, Presenter가 비대해지는 것은 막을 수 없었습니다.
의존성도 정리하면서, Presenter의 덩치를 줄일 수 있는 방법은 어떤게 있을까요?
여전히 의존성이 없어요.
MVP의 View와 동일합니다.
단, Presenter 대신 ViewModel에 대한 의존성을 갖고 있습니다.
의존성이 필요한 이유는 View에서 발생한 사용자 이벤트를 ViewModel로 전파하기 위함입니다.
ViewModel 이라는 이름을 쓴건, 아무래도 View에 대한 Model임을 더 정확히 표현하려는 시도로 보입니다.
즉, Model이 제공하는 데이터가 애플리케이션 데이터(User)라면 ViewModel은 특정 View를 위해 가공된 데이터(LoyalUser, NormalUser)라고 보면 좋겠습니다.
ViewModel은 MVP Presenter와 마찬가지로 View의 이벤트를 전달 받아 Model에서 데이터를 가져옵니다.
한가지 큰 차이점은, 더 이상 View를 알지 못합니다.
이 의존성을 끊어내기 위해 Observer Pattern을 사용하게 됩니다.
View가 ViewModel을 관찰하면서, 관찰하는 데이터에 변경이 생기면 스스로 다시 그리게 됩니다. ViewModel은 이제 View에게 렌더링을 지시하지 않아도 됩니다.
Model은 여전히 의존성이 없습니다.
View는 ViewModel에 사용자 이벤트를 전달해야해서 의존성이 있습니다.
ViewModel은 Model에서 데이터를 가져오기 때문에 의존성이 있습니다.
UserInputs -> View -> ViewModel -> Model
앞선 예제로 돌아가보면,
ViewModel은 model.getUser(userId) 로 유저 정보를 얻어올 것입니다.
ViewModel은 Model에서 받은 유저 정보를 joinedAt 1년을 기준으로 loyalCustomer과 normalCustomer로 구분하여 View가 관찰 중인 데이터 홀더(ex. LiveData)에 값을 업데이트하겠죠.
View는 관찰하던 데이터에 변경사항이 생겨서 데이터에 맞는 화면을 적절히 그려줄 겁니다.
MVVM은 의존성을 더 간결하게 정리했고, 코드 내에서 관심사의 분리, 책임의 분리도 잘 이루어졌습니다!
View 는 이벤트 전파와 변경사항 구독 후 단순히 렌더링만 하면 됩니다.
ViewModel은 Model에서 데이터를 가져와서 UI 로직만 수행한 후 데이터홀더의 값만 업데이트해주면 됩니다.
ViewModel이 View를 몰라도 되기 때문에, ViewModel은 재사용성도 높아졌습니다.
MVVM은 요즘도 많이 쓰이는 아키텍처 패턴이지만, 여전히 조금은 문제가 있습니다.
서비스가 복잡해지면, 개발자가 쉽게 실수할 수 있는 부분이 있는데요,
ViewModel이 관리하는 UI 데이터가 복잡해지면, 각 데이터들의 상태를 관리하기가 어려워지는 점입니다.
예를 들어볼게요.
어떤 화면에 진입할 때, 첫 화면에 필요한 데이터를 Model에서 가져와서 보여준다고 가정할게요.
데이터를 불러오는 중에는 아마도 화면 가운데에는 로딩바가 빙글빙글 돌고 있을거에요.
ViewModel은 Model로부터 데이터를 가져온 후 로딩바를 숨기도록 구현합니다.
몇 달 뒤, 해당 화면에서 새로운 [더보기] 버튼이 생겼습니다.
이 버튼을 누르면 화면에 새로운 정보가 추가되어야 합니다.
개발자는 더보기 버튼을 클릭하면 기존 로딩바의 값을 로딩중으로 바꾸고, 새로운 정보를 Model에서 잘 가져오면 다시 로딩바가 사라지도록 구현했습니다.
이제 로딩바의 상태를 바꾸는 부분이 ViewModel 내에 2개가 됐습니다.
현재는 별 문제 없지만, 몇 달 뒤 요구사항이 다시 바뀌게 됩니다.
더보기 버튼을 없애고, 처음부터 모든 정보를 보여주기로 스펙이 변경됐습니다.
개발자는 기존 코드를 깊게 보지 않고, '더보기' 버튼 클릭 후 하던 일을 단순히 onCreate 시점에 하도록 후다닥 수정합니다.
이제 해당 화면에 진입하면 첫 화면 데이터 요청과 더보기 요청이 동시에 발생합니다. 둘 중에 먼저 끝난 요청이 로딩바를 숨기게 되지만, 여전히 화면에는 빈공간이 보이게됩니다.
아주 간단하고 다소 성의없는 예시이긴 하지만, 이런 일은 실무에서 충분히 발생 가능합니다. 코드 리뷰 문화가 잘 발달해있고, 개발 기간이 넉넉하다면, 충분히 예방할 수는 있겠지만, 실수를 할 위험이 도사리고 있는 점은 분명합니다.
이제, 아키텍처 패턴은 "데이터의 상태, 뷰의 상태"를 더 실수 없이 관리하기 위한 방법을 고민하게 됩니다.
MVI는 단방향 아키텍처입니다.
앞선 MVVM의 예시도 단방향의 모습을 띄기도 하지만, MVVM의 경우에는 View와 ViewModel이 bi-directional 하도록 구현이 되기도해서, 단방향성에 초점을 둔 아키텍처는 아닙니다.(물론 data-binding 등을 활용하기에 결합이 굉장히 느슨하다고 볼 순 있습니다)
MVVM은 ViewModel이 View를 알지 못하도록, 의존성을 끊음으로서 확장성을 높인 점이 가장 큰 특징이라고 봅니다.
MVI는 의존성 및 관심사의 분리에 대한 패턴이라기 보다는, 데이터 흐름에 대한 패턴이라 MVC, MVP, MVVM과는 그 결이 조금 다릅니다.
제가 이해하는 MVI는 기본적으로 MVVM의 도움을 받게 됩니다.
즉, MVVM의 큰 골격 위에 데이터의 단방향성에 초점을 둔 아키텍처 패턴이라고 봐야할 것 같습니다.
MVI의 Model은 State를 의미합니다.
앞선 패턴들(MVC, MVP, MVVM)에서의 Model은 거의 동일한 역할을 수행했습니다. 의존성 없이 늘 사용되기만 하는.. DataSource 의 역할이 크다고 볼 수도 있겠습니다.
MVI에서의 Model은 ViewState라고 봅니다. 즉, 뷰의 상태를 의미합니다.
View는 ViewState를 그리는 역할을 합니다.
물론, userInputs 을 ViewModel에 전달하는 역할도 합니다.
(제가 이해하는 MVI는 MVVM의 골격 위에 있어서, ViewModel도 존재합니다)
View에서 발생하는 UserInputs는 하나의 Intent(의도)로 포장되어 전달됩니다.
이 의도에 맞는 비즈니스 로직을 수행한 후 Model(뷰 상태)을 만들, 이 뷰 상태가 화면에 렌더링 됩니다.
Android Intent와 헷갈리지 않도록 저는 Action 이라는 이름을 사용합니다.
MVI의 의존성은 큰 맥락에서 MVVM과 다르지 않습니다.
따라서 의존성 보다는 흐름에 초점을 맞추는 것이 좋겠습니다.
모듈 관점에서 ui 모듈과 presentation 모듈을 분리해두고 생각해보겠습니다.
ui 모듈에는 View가 위치합니다.
presentation 모듈에는 Action(Intent), ViewState(Model)이 위치합니다. Action 을 ViewState로 바꿔주기 위한 ViewModel도 존재합니다.
ui 모듈은 presentation 모듈에 의존성이 있습니다.
따라서 ui 모듈의 View는 사용자 이벤트를 Action으로 만들어 ViewModel로 전달하게 됩니다.
presentation 모듈에서는 Action에 따라 비즈니스 로직을 처리하고, ViewState를 만들게 됩니다.
presentation 모듈은 ui 모듈을 알지 못합니다. 마치, MVVM에서 ViewModel이 View를 모르는 것과 같습니다.
ui 모듈은 presentation 모듈의 ViewState를 관찰하고 있습니다. 변경된 뷰 상태에 맞는 화면을 그려줍니다.
최근 트렌드인 선언형 UI에 MVI가 잘 어울린다는 이야기가 많습니다.
선언형 UI는 하나의 작은 View를 마치 하나의 함수와 같은 방식으로 그린다고 생각할 수 있습니다. 즉, 선언형 UI가 input(what) 에 대한 렌더링을 정의해나가는 방법인 만큼, 이 input에 해당하는 ViewState가 툭툭 던져지는 MVI는 선언형 UI와 굉장히 잘 어울린다고 할 수 있습니다.
Android Developers 에도 권장 아키텍처를 소개하는 섹션이 있습니다.
MVI라는 표현은 따로 하지 않지만 단방향 데이터 흐름을 권장합니다. 여기서는 더 큰 애플리케이션 아키텍처 관점에서 단방향 데이터 흐름을 소개하고 있기는 하지만, 결국 애플리케이션을 큰 그림으로 보든, 조금 작은 단위로 보든, 단방향 데이터 흐름은 확실히 개발 생산성에 도움을 주는 접근 방법이라고 생각합니다.