작년 10월즈음, 과제에서 처음 MVVM 패턴을 적용했을 때
MVVM ViewModel과 AAC ViewModel의 개념이 너무 헷갈렸던 경험이 있다.
블로그를 돌아다녀보면 AAC ViewModel을 쓰는 것만으로도 MVVM이라고 말하는 사람도 있고,
AAC ViewModel != MVVM ViewModel이라고 말하는 사람도 있고...(후반이 좀 더 설득력 있긴 했다)
결국 혼자 마음대로 결론내리고(!!!) 구현한 적이 있는데
그 때 뻘짓한 경험을 적어보려고 한다.
(이미 다 아는 사람이라면 엥? 할 정도의 내용일 수도 있어서
부끄럽기도 합니다ㅠㅠㅠㅠㅠ 열람 주의..)
어쩌다가 AAC ViewModel 없이 MVVM 패턴 적용했나
위키백과에서 가져온 MVVM 패턴 그림이다.
여기서 ViewModel은 모델에 있는 데이터의 상태로, MVP의 Presenter와 달리 View를 참조하지 않는다.
대신 바인딩을 통해 ViewModel의 업데이트를 주고 받는데,
이 때 활용하는 대표적 예시가 Databinding.
반면 공식 문서에서 있는 AAC ViewModel은 lifecycle을 고려해서 만들어진 개념이라고 나온다.
ViewModel의 생명주기가 Activity의 생명주기보다 길기 때문.
여기서 단순히 AAC ViewModel != MVVM ViewModel 라고 이해하고, (여기까진 맞음)
AAC ViewModel은 아에 다른 패턴이구나.. 둘이 접목시킬 순 없구나..라고 잘못 이해해버림
좀 더 정확히 말하면 AAC ViewModel처럼 사용하면 MVVM 패턴이 아니구나...라고 이해해버림..ㅠㅠ
때문에 AAC ViewModel의 장점을 누리지 않고(!) MVVM 패턴을 구현했다.
그 때 적은 코드를 필요한 부분만 추려서 보여주면 다음과 같다.
private const val ID_KEY = "id"
private const val PASSWORD_KEY = "password"
private const val RECONFIRM_PASSWORD_KEY = "reconfirm_password"
private const val NAME_KEY = "name"
class RegisterActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterBinding
private lateinit var viewModel: RegisterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignUpBinding.inflate(layoutInflater)
setContentView(binding.root)
val appContainer = (application as MyApplication).appContainer // 수동 di
viewModel = appContainer.registerViewModelFactory.create() // viewModel 초기화
savedInstanceState?.let {
viewModel.setId(it.getString(ID_KEY) ?: "")
viewModel.setPassword(it.getString(PASSWORD_KEY) ?: "")
viewModel.setReConfirmPassword(it.getString(RECONFIRM_PASSWORD_KEY) ?: "")
viewModel.setName(it.getString(NAME_KEY) ?: "")
} // saveInstanceState로 저장된 정보 가져오기
binding.viewmodel = viewModel // 데이터바인딩
}
// ...
override fun onSaveInstanceState(outState: Bundle) {
outState.putString(ID_KEY, binding.etId.text.toString())
outState.putString(PASSWORD_KEY, binding.etPassword.text.toString())
outState.putString(RECONFIRM_PASSWORD_KEY, binding.etReconfirmPassword.text.toString())
outState.putString(NAME_KEY, binding.etName.text.toString())
super.onSaveInstanceState(outState)
}
}
해당 부분은 단순한 회원가입 페이지로,
사용자로부터 아이디와 비번, 확인용 비번, 이름 총 4개를 받는 Activity 였다.
각 입력받는 ui는 EditText로 구현된 상태이며, 데이터 바인딩으로 구현하였다.
이 때는 수동 DI 밖에 몰랐던 때라,
onCreate()에서 AppContainer를 사용해서 Factory로부터 ViewModel을 생성했다.(당시 참고한 공식 문서)
하지만 여기서 정말 정말 마음에 안 드는 부분이 발생한다...
바로 Configuration 대응 때문이다.
화면 회전, 다크 모드 전환와 같은 상황에서 Activity는 재생성된다.
즉, onDestory() 후에 onCreate()를 호출한다.
결국 화면 회전이 일어나거나 다크 모드가 전환되는 순간
ui의 데이터를 알고 있던 ViewModel이 재생성되서
네 개의 EditText에 입력된 데이터가 싸그리 날라가게 되는데
그런 앱이 세상에 어디있겠는가??
그래서 따로 onSaveInstanceState() 콜백을 써서
원래 같으면 날아갈 정보를 저장해주고,
onCreate()에서 해당 정보를 받아와 ViewModel의 변수에 넣어주는 작업을 수행하게 된 것이다.
MVVM ViewModel이 AAC ViewModel 상속받도록 하기
onSaveIsnstanceState()를 사용한 코드가 당시에는 정말 마음에 안 들었다.
"각각의 KEY도 들고 있어야 하는데 그걸 일일히 넣어주는데...
근데 이거 AAC ViewModel 쓰면 한 방인데.. MVVM을 써야하는가?"
여기서 내가 정말 착각하고 있던건,
AAC ViewModel을 사용한다고 해서 MVVM 패턴을 사용하지 않는 게 아니라는 것이다.
AAC ViewModel은 MVVM ViewModel과 다른 개념인 것은 맞으나,
또 다른 패턴, 또 다른 ViewModel인 게 아닌 Android에서 데이터 지속성을 위해 제공한 클래스다.
따라서 MVVM ViewModel에 AAC ViewModel을 상속받아 사용하는 것이 가능한 것. (그리고 이게 보통이라고 한다.)
참고로 위 코드에서 AAC ViewModel을 적용하게 되면 다음처럼 코드가 확 줄어든다.
// private const val ID_KEY = "id"
// private const val PASSWORD_KEY = "password"
// private const val RECONFIRM_PASSWORD_KEY = "reconfirm_password"
// private const val NAME_KEY = "name"
class RegisterActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterBinding
private lateinit var viewModel: RegisterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySignUpBinding.inflate(layoutInflater)
setContentView(binding.root)
// viewModel = appContainer.registerViewModelFactory.create() // viewModel 초기화
viewModel = ViewModelProvider(this).get(RegisterViewModel::class.java)
// AAC ViewModel 가져오기
/*
savedInstanceState?.let {
viewModel.setId(it.getString(ID_KEY) ?: "")
viewModel.setPassword(it.getString(PASSWORD_KEY) ?: "")
viewModel.setReConfirmPassword(it.getString(RECONFIRM_PASSWORD_KEY) ?: "")
viewModel.setName(it.getString(NAME_KEY) ?: "")
} // saveInstanceState로 저장된 정보 가져오기
*/
binding.viewmodel = viewModel // 데이터바인딩
}
// ...
/*
override fun onSaveInstanceState(outState: Bundle) {
outState.putString(ID_KEY, binding.etId.text.toString())
outState.putString(PASSWORD_KEY, binding.etPassword.text.toString())
outState.putString(RECONFIRM_PASSWORD_KEY, binding.etReconfirmPassword.text.toString())
outState.putString(NAME_KEY, binding.etName.text.toString())
super.onSaveInstanceState(outState)
}
*/
}
잘못 이해했을 때는 몰랐던..
왜 ViewModel로 이름 지었냐....라고 원망하던 블로그 글을 이해해따..
댓글을 읽어보면서 과거에 잘못 이해해서 구현하는 바람에
본의아니게 ViewModel 이름 탓(?)으로 한 느낌이 없지 않아 있는 것 같다...
다른 것은 맞지만, AAC ViewModel을 설명하던 공식 문서 글을 보면,
실제 MVVM 패턴과 흡사한 것은 맞기에 (애초에 많이 달랐으면 이름이 같아도 헷갈리지 않았을 것 같긴 함)
완전히 분리해서 말하기에는 어렵다고 말할 수 있을 것 같다.
(댓글로 조언 주신 MangBaam 님 감사합니다..!)
'Android' 카테고리의 다른 글
[TIL/개념] Android Notification + PendingIntent (0) | 2023.07.11 |
---|---|
[적용기] Clean Architecture는 정말 좋을까? (2) | 2023.05.13 |
[TIL/개념] Android의 Context와 ApplicationContext (0) | 2023.04.30 |
[TIL/개념] Activity와 Fragment (2) - Fragment와 Fragment의 lifecycle (0) | 2023.03.21 |
[TIL/개념] Activity와 Fragment (1) - Activity와 Activity의 lifecycle (0) | 2023.03.21 |