Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android MVVM 패턴의 접근법 - 2023 드로이드 나이츠

TaeHwan
September 12, 2023

Android MVVM 패턴의 접근법 - 2023 드로이드 나이츠

TaeHwan

September 12, 2023
Tweet

More Decks by TaeHwan

Other Decks in Programming

Transcript

  1. Android MVVM 패턴의 접근법 권태환 / 레몬트리(퍼핀) Android MVVM 패턴의

    접근법이 다양할 수 있는데, 다양한 MVVM 형태를 알아보고, 하나의 시나리오를 통해 사용 중인 접근 방법에 대해 알아보려 합니다.
  2. 발표 진행하기 전에 ❏ MVVM을 써보셨거나, 컴포즈를 활용 중이시라면 도움

    될 수 있음 ❏ MVVM을 기반한 다양한 형태 ❏ 사용하고 있는 MVVM 패턴 소개 ❏ MVVM 패턴을 사용하다 보니 설계에 대한 고민 포함 본 발표는 MVVM을 이미 사용해 보셨거나, 새로운 접근법의 고민을 해보았다면 도움 될 수 있습니다.
  3. 구글 아키텍처 가이드 ❏ 구글의 아키텍처 가이드? - MVVM +

    UiState + Compose/XML ❏ https://developer.android.com/topic/architecture 구글 아키텍처 가이드를 최근 가장 많이 사용할 걸로 생각됩니다. MVVM 기반에 UiState를 활용하고 Compose에서 이를 받아 처리하는 형태입니다. 구글 아키텍처 가이드가 나오기 전엔 MVI에서 주로 사용하는 방식 중 하나입니다.
  4. ❏ Ui State를 활용하는 Airbnb mavericks ❏ https://airbnb.io/mavericks/#/ MVI? Airbnb에서

    제공하는 Mavericks인데, 구글 아키텍처 가이드를 보고 이 코드를 읽어본다면 쉽게 이해할 수 있는 부분입니다. 위 가이드 코드만 보면 구글 아키텍처 가이드와 큰 차이는 없어 보입니다.
  5. MVVM 모듈 분리 - 드로이드나이츠 ❏ 드로이드 나이츠 모듈 분리(Data,

    Domain, Feature) ❏ https://github.com/droidknights/DroidKnights2023_App 드로이 드나이츠 코드도 모듈화되어 작업되어 있습니다. app, feature, core로 분리해 작업했습니다. Now in android 보다 잘 정리되어 있으니 참고하시면 좋습니다.
  6. ❏ 드로이드 나이츠 모듈 분리에 api(인터페이스 분리) 모듈 추가 ❏

    https://github.com/droidknights/DroidKnights2023_App/tree/MVVM_Split_API_Module MVVM 모듈 분리 - 라우팅용 API 추가 앞선 모듈 중 인터페이스 분리 원칙을 따르는 모듈을 추가한 형태인데, 정승욱 님이 작업해 주셨고, 제가 사용하는 형태도 이와 같습니다. 생각보다 많은 부분이 분리되어 있어 이렇게까지 가능 하구 나로 확인해 보시면 좋습니다. 결국 덩치가 커지면 구현체를 모든 다른 모듈에서 알 필요 없이 인터페이스를 통해서 접근할 수 있도록 분리한 형태입니다.
  7. Composable Architecture ❏ Composable Architecture? ❏ Slack Circuit(https://github.com/slackhq/circuit) ❏ Chrisbanes/tivi(https://github.com/chrisbanes/tivi)

    컴포저블 아키텍처란 이름으로 제목을 달았는데, 리엑트 개념을 포함한 형태입니다. 개인적으론 MVI 형태가 아닌 이런 형태로 흘러가는 것이 더 적절하다고 생각됩니다.
  8. 테스트와 모듈화 ❏ 테스트 ❏ MVP/MVVM 패턴의 변화는 UnitTest 가능성을

    만들기 위함 ❏ 모듈화 ❏ 모듈화를 통해 불필요한 디펜던시 줄이기 ❏ 모듈단위 빌드로 추가 작업 시 빌드 시간이 줄어든다 MVVM 패턴을 활용하면 테스트, 모듈화(사용하지 않더라도 모듈화는 과거부터 가능했지만)까지 가능합니다. 비즈니스 로직에 대한 UnitTest 가능성을 만들어주기 위해 UnitTest를 적용할 수 있고, 모듈화까지 지원하면 빌드 시간의 단축까지 이어질 수 있습니다. 모듈화를 했을때의 가장 큰 장점은 불필요한 의존성을 만들지 않을 수 있다는 점입니다. 기존 패키지 단위로 작업한다면 이름이 유사하더라도 data 레이어에서 ui 관련 코드를 가져다 사용하는 것도 가능했지만 모듈화로 잘 분리해두면 이런 문제를 해결할 수 있습니다.
  9. 데이터 흐름의 변화 ❏ UI State를 통한 화면의 변화 UiState

    이전에는 각각의 상태를 따로 관리했었지만 지금은 UiState를 통해 화면의 변화를 하나의 data class로 관리할 수 있습니다.
  10. 데이터 흐름의 변화 ❏ Domain/Data 레이어 분리 Domain과 Data 레이어의

    분리도 전통적으로 많이 하던 부분입니다. 결국 uiState까지 이어지는 데이터의 흐름의 연결점이라고 생각하시면 좋을 것 같습니다.
  11. 시나리오 ❏ 사용자가 메모를 삭제한다. 이때 삭제 여부와 핀 코드를

    확인한다. ❏ 사용자 메모 삭제 이벤트 발생 ❏ 삭제 Alert을 통해 사용자의 액션에 따라 핀 코드 추가 확인 i. Yes > 핀 코드 확인 ii. No > 취소 ❏ 핀 코드 입력 후 성공하면 데이터 삭제, 실패하면 취소 하나의 시나리오를 적어보았습니다. 이 시나리오를 기반하여 일부 코드와 상태를 분리해 볼 수 있는데, 메모장 앱의 삭제 이벤트를 받고, 이를 삭제할 것인지 정하고 핀 코드 입력 후 삭제하는 방식의 화면이 있다고 가정합니다.
  12. 이런 경우 어떻게 개발할까 일반적으론 이 그림과 같이 View와 ViewModel

    간의 상태를 핑퐁 하는 형태로 구현하게 됩니다.
  13. 이런 경우 어떻게 개발할까 1 1 단계로 User의 액션이 일어나면

    ViewModel에서 액션을 처리합니다. alert을 노출해달라고 View에 요청합니다.
  14. 이런 경우 어떻게 개발할까 2 2 단계로 이벤트 상태를 ViewModel로

    다시 전달합니다. 이를 받아 ViewModel은 Pin 화면을 노출 여부를 결정합니다. Pin에서의 이벤트가 다시 ViewModel로 전달됩니다.
  15. fun showAlert() { if (isConfirm) { _showActivity.value = true //

    2 단계 } else { if (isEdit) { _showAlert.value = true // 1 단계 } } } fun actionDeleteDone() { // 3 단계 viewModelScope.launch { deleteMemo(id) } } UiState를 적용하기 전의 일부 코드입니다. 액션을 받아, 새로운 화면을 노출할지 alert을 노출할지를 한 번의 이벤트를 처리하는 방식으로 SingleLiveData를 이용해 view에 전달하고, 뷰는 이 이벤트를 통해 원하는 형태를 처리합니다.
  16. fun showAlert() { if (isConfirm) { _showActivity.value = true //

    2 단계 } else { if (isEdit) { _showAlert.value = true // 1 단계 } } } fun actionDeleteDone() { // 3 단계 viewModelScope.launch { deleteMemo(id) } } 3 단계에서 deleteDone 액션이 발생하면 데이터를 삭제하고, UI를 갱신하게 됩니다.
  17. sealed interface MemoDetailUiState { data object Loading : MemoDetailUiState data

    class Success( val memo: Boolean = false, ) : MemoDetailUiState } sealed interface MemoDetailEffect { data object Idle : MemoDetailEffect data object ShowAlert : MemoDetailEffect data object ShowNewView : MemoDetailEffect data object Delete : MemoDetailEffect } UiState를 적용한다면 로딩의 상태와 데이터 상태를 모두 가지게 됩니다. 그리고 Effect를 통해 다음 액션을 정의할 수 있습니다. 앞에서 보았던 각각의 상태 관리보다 좀 더 쉽게 상태를 알 수 있습니다.
  18. private val _memoUiState = MutableStateFlow<MemoDetailUiState>(MemoDetailUiState.Loading) private val _memoDetailEvent = MutableStateFlow<MemoDetailEffect>(MemoDetailEffect.Idle)

    fun actionNext() { if (_memoUiState.value !is MemoDetailUiState.Success) { return } val newEffect = when (_memoDetailEvent.value) { is MemoDetailEffect.Idle -> MemoDetailEffect.ShowAlert is MemoDetailEffect.ShowAlert -> MemoDetailEffect.ShowNewView is MemoDetailEffect.ShowNewView -> MemoDetailEffect.Delete else -> MemoDetailEffect.Idle } if (newEffect is MemoDetailEffect.Delete) { // delete memo } else { _memoDetailEvent.tryEmit(newEffect) } } Ui의 상태와 event 처리를 위한 2개의 StateFlow를 정의합니다. UI의 상태가 Success가 아니라면 액션을 진행하지 못하므로 1차적으로 막아줍니다. 기존엔 flag를 통해 이를 처리하기도 하였습니다. 새로운 효과(이벤트)는 현재 상태를 보고 다음의 상태로 매핑하는 형태로도 코드 작성이 가능합니다. 훨씬 간결해졌고, 마지막 delete 상태일 때 메모를 삭제하고 UiState를 수정할 수 있습니다.
  19. sealed interface MemoDetailUiState { data object Loading : MemoDetailUiState data

    class Success( val memo: Boolean = false, val event: MemoDetailEvent, <<< Event 를 State가 가지고 있다 ) : MemoDetailUiState } sealed interface MemoDetailEvent { data object ShowAlert : MemoDetailEvent data object ShowNewView : MemoDetailEvent data object Delete : MemoDetailEvent } 컴포저블 아키텍처(리액트)를 사용하면 Event를 직접 들고 있도록 수정합니다. 결국 event 대신 UiState만 알면 현재 화면의 상태도 다음의 이벤트도 알 수 있습니다. 한 단계 줄어든 것처럼 보일 수 있지만 이 샘플도 하나의 상태만을 이해하기 위해 적었으니 참고만 해주세요.
  20. private val _memoUiState = MutableStateFlow<MemoDetailUiState>(MemoDetailUiState.Loading) fun actionNext(event: MemoDetailEvent) { if

    (_memoUiState.value !is MemoDetailUiState.Success) { return } if (event is MemoDetailEvent.Delete) { // delete memo } else { // Action } } UiState에 event 상태도 포함이니 좀 더 코드가 간결해질 수 있습니다.
  21. 사용 중인 Architecture 여기까지 일반적인 아키텍처들에서 많이 사용하는 방식입니다. 제가

    사용하고 있는 아키텍처를 소개하기 위해 몇 가지 형태를 살펴보았습니다.
  22. 한 번쯤 ❏ View와 ViewModel을 매번 핑퐁 하면서 써야 할까?

    ❏ View에서 다 처리하고 로직만 분리할 수 없을까? ❏ ViewModel에서 로직적인 걸 좀 더 쉽게 접근할 수 없을까? ❏ 부분 부분 테스트는 가능한데 전체는 못할까? ❏ UI 테스트해야 하나? MVVM 사용하다 보면 그리고 제가 앞에서 그렸던 그림이나 코드들을 생각해보면 View와 ViewModel 간의 핑퐁이 너무 많다는 생각이 듭니다. 이를 조금 더 줄이고, 테스트 검증도 좀 더 쉽게 할 수 있는 방법이 없을까란 생각을 해본 적이 있습니다.
  23. 더 좋은 방법 없을까? ❏ 그냥 지금처럼 사용한다. ❏ View

    관련을 미리 다 처리하고, ViewModel에 상태를 던져준다. ❏ 이벤트를 바로 받을 수 없을까? ❏ callbackFlow를 활용하는 것처럼 ❏ SharedFlow를 활용하는 것처럼 이런 부분을 해결할 수 있는 방법이 딱히 떠오르지 않아서 그냥 원래 사용하는 방법으로 그대로 접근하거나, View에서 뷰와 관련한 부분을 모두 처리하고, ViewModel에 필요한 상태만을 전달해 주기도 했습니다. 좀 더 좋은 방법으론 Callback Flow를 활용하는 형태로도 가능할 것 같습니다. 결국 이벤트를 받아서 다음 스트림으로 이어서 처리하는 형태를 고민해 볼 수 있는데, 이는 리액트 개념을 이해하고 있어야 합니다.
  24. https://youtu.be/lkrEfYO54xU?si=XmKf5-LSFdgJ7s5y 제가 사용하고 있는 아키텍쳐는 정승욱 님의 발표 내용에서 언급하고

    있는 부분을 사용하고 있습니다. 이 발표에서는 조금 더 구체적인 코드 사용법을 살펴보려고 합니다.
  25. 서비스 적용 사례 마치 스스로 판단하는 것 같지만 그렇지는 않고,

    모두 미리 만들어둔 코드들이 동작하는 부분입니다. 당연히 각각은 View와 ViewModel의 통신 방식을 차용하고 있습니다. 그걸 미리 만들어두고 가져다 쓰는 형태로 접근합니다. 이렇게만 하더라도 단순히 View와 ViewModel 간의 핑퐁이 줄어들지만 전체적인 그림은 앞서본 그림과 동일합니다.
  26. val interactionTrigger = LocalComposeEventTriggerOwner.current Button( onClick = interactionTrigger.onClick(TriggerEvent.BTN_SHOW_MAIN), ) {

    Text(text = "홈으로 이동") } 컴포즈에서 ViewModel로 이벤트를 전달합니다. triggerOwner를 사용하여 이벤트만 전달합니다. 그 이벤트는 뒷단의 SharedFlow를 통해 브로드 캐스트 하며, filter를 통해 원하는 이벤트를 캐치하는 형태입니다.
  27. val interactionTrigger = LocalComposeEventTriggerOwner.current Button( onClick = interactionTrigger.onClick(TriggerEvent.BTN_DELETE_MEMO), ) {

    Text(text = "메모 삭제") } onClick에 뷰 모델의 함수를 직접 호출하는 대신 이와 같이 onClick 이벤트를 발생시킵니다.
  28. @CreatedToDestroy fun flowDeleteMemo( flowViewInteractionStream: FlowViewInteractionStream, router: Router, alertDialogBuilderFactory: AlertDialogBuilderFactory, ):

    Flow<ResultData?> = flowViewInteractionStream.onClick(TriggerEvent.BTN_DELETE_MEMO) .showAwaitDialog(alertDialogBuilderFactory, "메모를 삭제할까요?", "확인") .filter { it == DialogButtonState.BtnNegative } .visitForResult(router, PinCodeActivityJourneyGuidance::class) .filter { it?.getBoolean("KEY_SUCCESS", false) == true } .flowOn(dispatcherProvider.main()) .onEach { /* delete action */ } viewModel에서는 flow를 통해 모든 처리를 하는데, 한 줄씩 살펴보겠습니다.
  29. @CreatedToDestroy fun flowDeleteMemo( flowViewInteractionStream: FlowViewInteractionStream, router: Router, alertDialogBuilderFactory: AlertDialogBuilderFactory, ):

    Flow<ResultData?> = flowViewInteractionStream.onClick(TriggerEvent.BTN_DELETE_MEMO) .showAwaitDialog(alertDialogBuilderFactory, "메모를 삭제할까요?", "확인") .filter { it == DialogButtonState.BtnNegative } .visitForResult(router, PinCodeActivityJourneyGuidance::class) .filter { it?.getBoolean("KEY_SUCCESS", false) == true } .flowOn(dispatcherProvider.main()) .onEach { /* delete action */ } 1 먼저 뷰를 통해 이벤트를 전달받는 stream을 사용합니다. btn_delete_memo를 필터 해서 onClick 이벤트를 캐치합니다.
  30. @CreatedToDestroy fun flowDeleteMemo( flowViewInteractionStream: FlowViewInteractionStream, router: Router, alertDialogBuilderFactory: AlertDialogBuilderFactory, ):

    Flow<ResultData?> = flowViewInteractionStream.onClick(TriggerEvent.BTN_DELETE_MEMO) .showAwaitDialog(alertDialogBuilderFactory, "메모를 삭제할까요?", "확인") .filter { it == DialogButtonState.BtnNegative } .visitForResult(router, PinCodeActivityJourneyGuidance::class) .filter { it?.getBoolean("KEY_SUCCESS", false) == true } .flowOn(dispatcherProvider.main()) .onEach { /* delete action */ } 2 Dialog를 한번 노출하는데, 메시지와 버튼에 대한 상태를 정의합니다. 그리고 응답으로 돌아온 값에 대한 filter를 처리합니다. 확인이 아닌 취소인 경우 다음 진행을 할 필요가 없으므로, filter로 무시합니다.
  31. @CreatedToDestroy fun flowDeleteMemo( flowViewInteractionStream: FlowViewInteractionStream, router: Router, alertDialogBuilderFactory: AlertDialogBuilderFactory, ):

    Flow<ResultData?> = flowViewInteractionStream.onClick(TriggerEvent.BTN_DELETE_MEMO) .showAwaitDialog(alertDialogBuilderFactory, "메모를 삭제할까요?", "확인") .filter { it == DialogButtonState.BtnNegative } .visitForResult(router, PinCodeActivityJourneyGuidance::class) .filter { it?.getBoolean("KEY_SUCCESS", false) == true } .flowOn(dispatcherProvider.main()) .onEach { /* delete action */ } 3 Router를 통해 새로운 화면을 노출합니다. 액티비티를 열 때 startActivityResult 형태로 응답을 받고, 이때 들어오는 응답을 캐치합니다. 당연히 실패한다면 다음을 넘어가지 않습니다.
  32. @CreatedToDestroy fun flowDeleteMemo( flowViewInteractionStream: FlowViewInteractionStream, router: Router, alertDialogBuilderFactory: AlertDialogBuilderFactory, ):

    Flow<ResultData?> = flowViewInteractionStream.onClick(TriggerEvent.BTN_DELETE_MEMO) .showAwaitDialog(alertDialogBuilderFactory, "메모를 삭제할까요?", "확인") .filter { it == DialogButtonState.BtnNegative } .visitForResult(router, PinCodeActivityJourneyGuidance::class) .filter { it?.getBoolean("KEY_SUCCESS", false) == true } .flowOn(dispatcherProvider.main()) .onEach { /* delete action */ } 4 응답에 대한 처리가 모두 끝났다면 저장되어 있던 메모를 지우고, UiState를 갱신하거나, 화면을 종료합니다.
  33. @Test fun `test flowDeleteMemo`() { val flowViewInteractionController = MockFlowViewInteractionController() val

    alertDialogBuilderFactory = MockAlertDialogBuilderFactory() val router = MockRouter.create() val activityJourneyBuilder = MockActivityJourneyBuilder(MockBundleData.create("key-result" to true)) router.putJourneyGuidance(PinCodeActivityJourneyGuidance::class, object : PinCodeActivityJourneyGuidance(activityJourneyBuilder) {}) viewModel.flowDeleteMemo(flowViewInteractionController.asStream(), router, alertDialogBuilderFactory) .turbine { turbine -> turbine.expectNoEvents() // 시작 flowViewInteractionController.find(TriggerEvent.BTN_DELETE_MEMO).click() // Dialog 데이터 확인 alertDialogBuilderFactory.check("메모를 삭제할까요?", "확인") // Router 확인 Assertions.assertEquals(1, router.findCount[PinCodeActivityJourneyGuidance::class]) Assertions.assertEquals(1, activityJourneyBuilder.buildCount) Assertions.assertEquals(1, activityJourneyBuilder.lastJourney!!.visitCountForResult) turbine.cancelAndConsumeRemainingEvents() } 성공 케이스만 정리하였고, 한 줄씩 살펴보겠습니다.
  34. @Test fun `test flowDeleteMemo`() { val flowViewInteractionController = MockFlowViewInteractionController() val

    alertDialogBuilderFactory = MockAlertDialogBuilderFactory() val router = MockRouter.create() val activityJourneyBuilder = MockActivityJourneyBuilder(MockBundleData.create("key-result" to true)) router.putJourneyGuidance(PinCodeActivityJourneyGuidance::class, object : PinCodeActivityJourneyGuidance(activityJourneyBuilder) {}) viewModel.flowDeleteMemo(flowViewInteractionController.asStream(), router, alertDialogBuilderFactory) .turbine { turbine -> turbine.expectNoEvents() // 시작 flowViewInteractionController.find(TriggerEvent.BTN_DELETE_MEMO).click() // Dialog 데이터 확인 alertDialogBuilderFactory.check("메모를 삭제할까요?", "확인") // Router 확인 Assertions.assertEquals(1, router.findCount[PinCodeActivityJourneyGuidance::class]) Assertions.assertEquals(1, activityJourneyBuilder.buildCount) Assertions.assertEquals(1, activityJourneyBuilder.lastJourney!!.visitCountForResult) turbine.cancelAndConsumeRemainingEvents() } 1 버튼에 대한 액션을 처리합니다. SharedFlow를 활용하고 있기 때문에 처음 시작은 이벤트가 없습니다. 이벤트를 찾고, 이에 대한 click을 발생시킵니다. 참고로 jakewharton의 turbine을 사용합니다.
  35. @Test fun `test flowDeleteMemo`() { val flowViewInteractionController = MockFlowViewInteractionController() val

    alertDialogBuilderFactory = MockAlertDialogBuilderFactory() val router = MockRouter.create() val activityJourneyBuilder = MockActivityJourneyBuilder(MockBundleData.create("key-result" to true)) router.putJourneyGuidance(PinCodeActivityJourneyGuidance::class, object : PinCodeActivityJourneyGuidance(activityJourneyBuilder) {}) viewModel.flowDeleteMemo(flowViewInteractionController.asStream(), router, alertDialogBuilderFactory) .turbine { turbine -> turbine.expectNoEvents() // 시작 flowViewInteractionController.find(TriggerEvent.BTN_DELETE_MEMO).click() // Dialog 데이터 확인 alertDialogBuilderFactory.check("메모를 삭제할까요?", "확인") // Router 확인 Assertions.assertEquals(1, router.findCount[PinCodeActivityJourneyGuidance::class]) Assertions.assertEquals(1, activityJourneyBuilder.buildCount) Assertions.assertEquals(1, activityJourneyBuilder.lastJourney!!.visitCountForResult) turbine.cancelAndConsumeRemainingEvents() } 2 앞에서 확인한 다이얼로그의 설정 정보와 view가 잘 노출하도록 호출되었는지(기존 showEvent flag)를 확인하는 절차입니다. 실제 Ui가 연결되었는지는 UI 테스트를 해야 하지만 전 이 부분을 생략하고 있습니다. 버튼까지 확인하고 다음 이벤트를 전달합니다. dialog에서 주는 이벤트에 따라 취소/성공 모두 가능합니다.
  36. @Test fun `test flowDeleteMemo`() { val flowViewInteractionController = MockFlowViewInteractionController() val

    alertDialogBuilderFactory = MockAlertDialogBuilderFactory() val router = MockRouter.create() val activityJourneyBuilder = MockActivityJourneyBuilder(MockBundleData.create("key-result" to true)) router.putJourneyGuidance(PinCodeActivityJourneyGuidance::class, object : PinCodeActivityJourneyGuidance(activityJourneyBuilder) {}) viewModel.flowDeleteMemo(flowViewInteractionController.asStream(), router, alertDialogBuilderFactory) .turbine { turbine -> turbine.expectNoEvents() // 시작 flowViewInteractionController.find(TriggerEvent.BTN_DELETE_MEMO).click() // Dialog 데이터 확인 alertDialogBuilderFactory.check("메모를 삭제할까요?", "확인") // Router 확인 Assertions.assertEquals(1, router.findCount[PinCodeActivityJourneyGuidance::class]) Assertions.assertEquals(1, activityJourneyBuilder.buildCount) Assertions.assertEquals(1, activityJourneyBuilder.lastJourney!!.visitCountForResult) turbine.cancelAndConsumeRemainingEvents() } 3 마지막으로 라우팅이 원하는 형태로 잘 되었는지 확인합니다. 그리고 이에 대한 응답은 위쪽 코드의 MockBundleData 부분에서 제공합니다. 이 값에 따라 다음 진행이 불가능 형태도 만들 수 있습니다.
  37. 아키텍처 선택 기준 여기까지가 제가 사용한 아키텍처의 코드 부분입니다. 전체적인

    코드를 다 들어내지는 못하고, 추후 블로그를 통해 조금 더 쉬운 접근 방법을 소개하려고 하니 추후 블로그를 확인해 주세요.
  38. 사용 중인 아키텍처 장점 ❏ AAC-ViewModel을 사용하지 않지만 라이프 사이클에

    따른 함수 자동 호출 ❏ UI에 집중한 개발 가능, 로직은 Flow를 통해 작업 ❏ 함수를 직접 호출하지 않을 뿐 Flow 구독을 통해 자동 필터 후 동작 ❏ Compose Navigation 활용에 DisposablEeffect 사용 ❏ 싱글 액티비티 활용 가능성 ❏ UnitTest 활용성 높음(약 1,630개의 UnitTest) ❏ 개발 편의성을 위해 KSP 도입 ❏ Dagger 모듈 자동화, DataStore 코드 생성 자동화 등 아키텍처의 장점은 명확합니다. 빠른 UI 개발이 가능합니다.
  39. 사용 중인 아키텍처 단점 ❏ 숨겨진 뒷단의 코드가 매우 많음(이에

    대한 모듈 약 79개) ❏ Event 처리를 위한 로직, 자동화(KSP 활용) ❏ AAC-ViewModel 활용하려면 구조 변경 필요 ❏ 만든 사람만 아는 유지 보수 가능 ❏ KSP 코드 부분 다만 숨겨진 코드가 매우 많은데, 이를 유지 보수하는데 들어가는 비용이 필요합니다. 그래서 보통 구글에서 제공하는 라이브러리의 활용을 하지만 분명 모든 부분이 만족스럽지 않을 겁니다. 그런 부분을 수정했다라고 이해하시면 좋을 것 같습니다. 특히 제가 작성한 KSP 코드 부분은 KSP를 다뤄보지 않았다면 수정이 어려운 단점이 있습니다.
  40. 요약하면 ❏ 개발 편의성 vs 익숙한 개발 방법론 ❏ 혼자

    개발함에 있어 유연한 대응이 가능한가? ❏ 최소한의 테스트 코드를 통해 사이드 발생을 줄일 수 있는가? ❏ 모듈 간 디펜던시를 최대한 끊어낼 수 있는가? ❏ 어떤 아키텍처를 선택하든 새로운 사람은 학습이 필요 ❏ 이 학습을 줄일 수 있는 방안은?
  41. 꼭 필요한 고민 ❏ 아키텍처 선택 ❏ 익숙함과는 별개로 오버

    엔지니어링은 아닐까? ❏ 코드 컨벤션을 따르고 빠르게 코드 파악이 가능한가? ❏ 모든 팀원이 같은 형태의 코드를 고민 없이 작성하는가? ❏ 데이터처리 ❏ 큰 고민 없이도 바로 데이터와 로직을 분리하고 작업할 수 있을까? ❏ 굳이 도메인이 필요할까?(구글 가이드 참고)
  42. Android Architecture guide ❏ 관심사 분리 ❏ 데이터 모델에서 UI

    도출하기 ❏ 단일 소스 저장소 ❏ 단방향 데이터 흐름