[안드로이드스튜디오_문화][나만의 운동루틴 MVVM , 클린아키텍처 리팩토링 후기]

기말 지하기포·2024년 6월 10일
1

MVVM 패턴

MVVM 패턴은 Model View ViewModel의 앞글자를 딴채로 흔히 불려지는 단어이다.

MVVM 패턴이 뭔지 모르고 저가 몰라도 개발 쌉가능인데 왜 유난떨지라고 생각하던 시절에 만들었던 앱을 시간이 지나고 보니 왜 MVVM이 개발자 세상에 등장한지 알 수 있었음.

"MVVM 패턴 ~ 클린 아키텍처 ~ 멀티모듈" 이 세가지에 대한 개념을 이해하고 실제 프로젝트에 적용 할 수 있게 학습하는 시간들이 너무 끔찍하고 괴로운 시간들이였습니다.

Philip Lackner 아저씨 , 여러 기술블로그 , 패스트 캠퍼스 강의 , 공식문서 하나 하나 안 본 것이 없지만 재능의 한계인지 저에게는 너무 힘든 3개월의 시간이 였습니다.

아 이거 알겠다 하면 Model과 Entity의 차이..여러 오픈소스마다 각기다른 표현과 아키텍처들 속에서 너무 괴로운 시간들이였기 때문에 학습을 진행하며 포기하고 싶을 만큼 힘들 때 저를 이끌어준 생각은 "내가 이거 완벽하게 학습해서 내 시간 뒤에 따라오는 안드로이드 입문자들에게는 절대 나와 같은 시련을 겪게 하지 않겠다는" 생각이었습니다.

우선 안드로이드란 무엇일까요 ?

제가 생각하는 안드로이드는 사용자에게 데이터 단의 흐름을 제어하는 간접적인 권한을 주고 , 데이터단의 현재 상태를 사용자에게 보여주는 것이라고 생각합니다.

즉 , 사용자의 손가락이 데이터 단의 흐름을 컨트를하고 , 사용자에 의해 제어된 데이터단의 흐름의 현재 상태를 사용자에게 보여주는 역할이라고 생각합니다.

그렇다면 "데이터 단"이라는 단어에서 데이터라는 것은 어떠한 의미를 나타내는 것일까요?

제가 생각하는 데이터란 , 서버단의 데이터와 ViewModel단의 데이터라고 생각합니다. 그리고 서버단의 데이터와 ViewModel단의 데이터는 서로 다른 클래스로 각기다른 레이어에 위치하게 됩니다. 일단 해당 포스트에서는 클린 아키텍처 개념은 배제한 채로 MVVM 이라는 것에 대한 이해를 돕기 위한 글이니 생략하겠습니다.

MVVM ~ 클린아키텍처의 연결된 개념은 따로 포스팅 할 예정입니다.

그럼 다시 MVVM 패턴에서 "데이터 단"에서 데이터의 의미에 대해서 설명드리겠습니다.

데이터란 쉽게 말해 ViewModel에서 View로 전달되는 데이터 타입이라고 생각하시면 됩니다.

ViewModel에서 View로 String을 전달 할 수도 , List<>를 전달 할 수 도 있다는 의미입니다. 이 때 전달되는 데이터를 "LiveData를 통해 전달 할 것이냐" 혹은 "StateFlow를 통해 전달 할 것이냐"를 기준으로 데이터를 각기 다르게 분류 할 수 도 있습니다.

이에 대한 내용 또한 MVVM에서 LiveData Vs StateFlow를 사용 할 것이냐에 대한 별도의 포스팅글에서 설명드리겠습니다.

이제 본격적으로 MVVM 패턴에 대해서 설명 드리도록 하겠습니다.

MVVM 패턴을 쉽게 이해하기 위한 선행 조건은 2가지 단계가 있다고 생각합니다.

첫번째 단계 : MVMV 패턴으로 변형해서 생각하기
두번째 단계 : MVMVU 패턴으로 변형해서 생각하기
Model : ViewModel : View : User 단순히 알파벳의 위치를 변경하고 , 사용자라는 단어를 하나 붙혔을 뿐이지만 저는 이렇게 생각하는것이 MVVM패턴을 이해하는데 훨씬 효과적이라고 생각합니다.

이유는 다음과 같습니다.

위의 4가지 구성요소에서 제일 쉬운것부터 각각의 관계에 대해서 설명드리겠습니다. 각각의 구성요소들을 독립적인 객체라고 생각하시는 것이 더 쉬울 것입니다.

View : User
첫번째로 View와 User의 관계성입니다.
User(사용자)는 View(화면)에 있는 것을 곧이 곧대로 받아들입니다.

예를 들어 여러분이 카톡을 사용하실 때 화면을 터치하면 화면이 변경됩니다. 물론 해당 과정에서 절대적인 권한은 여러분께 있습니다.

메시지 전송 , 메시지 삭제 , 화면간의 이동 .. 등등 여러분이 의도하는대로 화면은 움직입니다.

아 뭐야 별거 없네요 맞습니다 View와 User의 힘의 균형은 User에게 있습니다.

ViewModel : View
다음은 ViewModel과 View의 관계성입니다.

예를 들어 여러분이 카카오톡에 메시지를 작성하는 중입니다.
"안녕 MVMVU"라고 작성을 하고 이제 전송버튼을 눌렀습니다.
해당 메시지는 메시지 작성칸에서 사라지고 채팅창에 띄용하고 나타납니다.

그렇다면 "메시지 작성 -> 전송"이라는 하나의 액션에서 ViewModel과 View의 역할은 무엇이었을 까요 ?

ViewModel은 View에 작성된 메시지에 대한 절대적인 권한을 가지고 있습니다.

(참고)
ViewModel : 화면이 로테이션 되어도 데이터를 유지 시킬 수 있음.
왜냐하면 생명주기 자체가 Activity는 여러 단계로 나누어져 있지만 ViewModel의 생명주기는 생성(ViewModel을 사용하는 화면이 사용 될 때)과 onCleared(해당 ViewModle을 호출한 화면이 속한 Activity가 파괴 될 떄 : onDestoryed 되고 난 이후에 호출)
이렇게 두가지의 단계로 이루어지기 때문이다.

즉 , ViewModel을 호출한 Composable이 속한 Activity가 파괴 되지 않는 한 ViewModel은 데이터를 기억하고 소유한다.
(참고)

그러나 중요한 점은 ViewModel이 메시지라는 "데이터"를 StateFlow 또는 LiveData를 통해 직접적으로 View에 전달하는 것은 아니라는 점입니다.

그렇다면 View는 어떡해 ViewModel에서 방출되는 값을 받아 올 수 있을까요?

이때 View가 ViewModel을 구독하고 있다는 표현이 흔히 사용됩니다.

무슨 의미이냐면 ViewModel에는 크게 3가지의 덩어리가 존재합니다.
오픈 변수와 프라이빗 변수 그리고 함수입니다.

즉 , ViewModel에는 아래와 같은 3가지 덩어리가 존재한다는 의미입니다. 아래 코드를 기반으로 설명 드리겠습니다.

private val _allExcercises = MutableLiveData<List<ExcerciseModel>>()

val allExcercises: LiveData<List<ExcerciseModel>> = _allExcercises

fun GetAllExcercise() = viewModelScope.launch (Dispatchers.IO) {
        val excercises = excerciseGetAllUseCase()
        _allExcercises.postValue(excercises)
    }

View에서는 ViewModel의 객체를 생성한 후 오픈변수를 다음과 같이 불러옵니다.
(LiveData를 사용하는 경우입니다.)

private val excerciseViewModel: ExcerciseViewModel by viewModels()


excerciseViewModel.latestWord.observe(this, Observer { excercise ->
            excercise?.let {
                excerciseAdapter.list.add(0, it)
                excerciseAdapter.notifyItemInserted(0)
            }
        })

(StateFlow를 사용하는 경우입니다.)

val addressViewModel : AddressViewModel = hiltViewModel()
val addressList = addressViewModel.addressList.collectAsState().value

위의 코드를 보면 View는 ViewModel의 존재를 명확히 인식한채로 , ViewModel의 변수 및 함수들을 사용하게 됩니다. 그러나 ViewModel의 경우에는 단순히 LiveData 또는 StateFlow를 사용하여 데이터를 방출 할 뿐입니다.

즉 , 절대적인 힘의 권한이 View < ViewModel에 있다고 생각해볼 수 있다는 의미입니다. ViewModel은 View의 존재 자체를 모르고 View는 ViewModel의 존재를 알고 ViewModel에서 작성된 로직들을 사용 할 수 있습니다.

이렇게 되면 View에서는 단순히 UI 작업만을 진행 할 수 있고 , ViewModle에 작성된 함수의 로직은 완전히 배제한 채로 ViewModle의 함수의 로직의 호출 위치를 Listener를 통하여 UI에서 개발자가 선택하게 할 수 있는 장점이 생기게 됩니다.

결론적으로 View와 ViewModel의 관계는 View가 ViewModel을 의존한다는 것인데 의존성의 개념이 이해되지 않으면 힘의 크기라고 생각하셔도 됩니다.

쉽게 말하면 지구는 그냥 비를 내릴 뿐인데 인간은 기상예보를 통해서 우산을 가져가야 할지 말지 결정하는 것처럼 , ViewModel은 그저 방출 할 뿐이고 , View는 LiveData일때는 observe , StateFlow일 때는 collect(kbs 일기예보냐 , sbs 일기예보냐에 따라서 채널이 변경되는 것처럼) 각기 다른 방식으로 ViewModel의 움직임에 따라서 행동을 취하게 된다고 생각하시면 됩니다.

이로써 개발자인 저희는 컴포넌트 별로 각기 다른 데이터의 렌더링과 함수를 선언한 채로 대다수의 로직을 ViewModel에서 작성함으로서 관심사의 분리 즉, 내가 코드를 작성할 때 UI를 수정하고 싶거나 , 특정 로직의 호출 위치를 수정하고 싶을때에는 View를 보면 되는 것이고 , 특정 로직을 변경하고 싶다면 ViewModel을 보면 된다는 것입니다.

Model : ViewModel

그렇다면 Model과 ViewModel은 서로 무슨 관계일까?

일단 Model이라는 것을 어렵게 생각하면 어렵고 쉽게 생각하면 아주 쉽다고 생각합니다.

제가 생각하는 MVVM에서 Model을 가장 쉽게 이해하는 방법은 Model이라는 것은 비즈니스 로직 이런 말 말고 안드로이드 스튜디오의 백엔드라는 개념으로 접근하는 것이 좋을 것 같습니다.

백엔드라는 개념자체가 프론트 엔드와 완전히 반대되는 말로 사용자가 백엔드를 아예 모르는 것처럼 Model 또한 User , View , ViewModel 또한 모르는 것을 Model이라고 생각하시면 될 것 같습니다.

하지만 실제로 코드상에서는 ViewModel에서 UseCase 또는 Repository를 통해서 Impl라는 구현체를 호출해야 되기 때문에 의존성이 있습니다.

그러나 저희도 카카오톡을 사용 할 때 "아 내가 전송버튼을 누르면 뭔가 일이 발생하기는 하는구나"라는 생각이 드는 것처럼 Model을 바라보는 ViewModel 또한 "아 ViewModel인 내가 나의 함수를 init(ViewModel의 객체가 생성될 때 호출 되는 함수)에서 호출하거나 viewModelScope(CoroutineScope를 상속받은 Scope로서 비동기 작업을 ViewModel에서 실행 할 수 있게 도와준다.)에서 호출하면 UseCase 또는 Repository를 타고 들어간 곳에서의 무언가가 실행되면서 작업이 이루어지는 구나"라고 생각한다고 이해하시면 될 것 같습니다. 즉 힘의 크기는 ViewModel < UseCase인 것입니다.

최종적으로 힘의 크기를 의존성 개념을 활용해서 정리하자면 MVVM패턴에서 각각의 친구들의 의존성은 다음과 같습니다.


사진 설명 추가
1. usecase or repository를 작성한 이유는 ViewModel의 파라미터로 usecase 또는 repository가 올 수 있기 때문입니다.
자세한 내용은 클린 아키텍처에서 MVVM과 연동해서 설명하는 글을 보시면 됩니다.

다음은 위의 내용에서 어디에 넣어야 할 지 몰라서 하하.. 아래에 작성한 MVVM 관련 추가내용입니다.

  1. "그런데 MVVM에서 굳이 ViewModel에 있는 오픈 변수를 사용 안 하고 TextField 나 OutlinedTextField 사용하니까 잘 만 렌더링 되는데요 ?"

답변 : 그렇다면 화면회전을 해보시면 여러분께서 작성한 데이터는 삭제 된 채로 남아있을 것입니다. 화면이 회전된경우에는 액티비티가 onDestroy되고 다시 onCreate가 됩니다. 어? 그러면 VieWModel도 onCleared되야 되는거아니에요? 라는 의문점이 드는 것은 당연합니다. 하지만 좀 더 깊게 들어가면 ViewModel의 onCleared는 ViewModelStore가 파괴 될 때 호출됩니다. 즉 , ViewModel은 Activity에서 관리되는 것이 아니라 ViewModelStore라는 곳에서 관리되며 onCreated되면서 새로운 Activity가 생성될 때 동일한 ViewModel 객체를 제공하기 때문입니다.

또한 , TextField와 OutlinedTextField의 경우 컴포저블 함수이기 때문에 상태를 기반으로 렌더링이 되는데 이때 value의 값이 변경될때 onValueChange가 호출되면서 상태가 업데이트 되면 리컴포지션이 발생하면서 화면에 최신의 텍스트가 렌더링 되는 것입니다.

리팩토링 후기

하.. 진짜.. 너무 하기 싫었다..하지만..해야지...코드가 누적되고 누적되면 더욱 더 하기 싫으니까..

일단 진정하고 코드를 분석해보니 크게 두가지 기능이 있었습니다.

  1. 기본적인 운동 목록을 정의해 놓은 채로 운동루틴 추가

  2. 추가하고 싶은 운동 루틴이 있을 경우 별도로 사용자가 작성한 채로 추가

1단계

1단계로는 폴더의 구조화 부터 시작하였다..
MVVM으로 나누되 클린아키텍처도 생각해가면서 폴더의 구조를
data , domain , presentation으로 만들어주고 그 안에 세부적으로 model과 entity등등의 클린 아키텍처 구조에서 사용되는 폴더들을 생성하였다.

2단계

2단계로는 각각의 로직별로 로직 플로우대로 화면에 파일을 띄우고 해당 로직이 실제로 구현되는 구현체 단을 impl로 옮긴후 1단계에서 생성한 폴더에 파일을 작성한 후에 모든 로직을 분해하였음

3단계

이제 각각의 로직에서 mapper를 추가해줌으로서 최종완성...

뒤돌아보니까 코드의 양이 많지 않아서 다행이라고 느꼈음.. XML도 거의 5개월? 정도 만에 만져보는데 익숙지 않아서 힘들었음.. 일단 1차적으로 "나만의 운동루틴"앱은 MVVM 클린아키텍처 완성하였지만 이제는 "AR친구랑" 앱이 제일 걱정된다.. 하하

profile
포기하지 말기

0개의 댓글