Architecture: Live Q&A — MAD Skills[번역]

DwEnn
10 min readMay 24, 2022

--

📝 NOTE : 이 글은 영상의 스크립트를 번역한 것이며 오역이 있을 수 있습니다. 😅

벌써 Architecture 의 마지막 영상을 보고 있습니다. 해당 세션은 Live Q&A 로 구성되어있고 전체적으로 앱 아키텍처 구성 혹은 개발함에 있어서 명확한 가이드를 원하는 질문들이 많았습니다. (ex. 프로젝트를 확장할 때 domain, data 레이어를 모듈로 분리하는 것이 좋을지, Compose 사용 시 MVVM/MVI 어느 것이 효과적일지 등등)
어디에서든 다 같은 의문을 가지고 개발을 하는 것 같습니다. 하지만 동영상에서 세션들이 얘기하는 것처럼 정답은 없습니다. 어디선가 읽었던 책의 말을 인용하자면, 실버블렛은 존재하지 않습니다. 때문에 동작 원리를 파악하는 것이 중요하겠죠. 개발하는 앱의 특징을 파악하고 아키텍처 레이어의 특성에 맞게 앱을 조합해야 할 것입니다. 그 과정에서 팀의 룰을 고려하고, 만약 기존과 변경되야하는 부분이 생긴다면 그에 맞는 논리를 가지고 설득할 수 있어야 되겠죠. 저 또한 그렇게 성장하는 개발자가 되는 것이 목표입니다. 🙌

이 글에서는 라이브 Q&A 의 모든 내용을 다루지 않고 제가 선정한 Q&A 만을 다룰 예정입니다.

Q: ViewModel에서 Context의 참조 없이 어떻게 string, color 와 같은 리소스에 접근할 수 있을까요?

ViewModel 에서 리소스에 접근해서는 안됩니다. 리소스 접근은 UI 계층(View 또는 Compose)에서만 수행되어야 합니다. 또는 리소스 ID 를 ViewModel 에서 노출하는 것도 방법입니다. ViewModel 에서 리소스를 접근하면 안되는 아래와 같은 이유가 존재합니다.

리소스가 종종 Configuration에 따라 변경되기 때문입니다.

Configuration 은 UI 에서만 알 수 있습니다. 현재 Dark Mode 인지 Light Mode 인지에 대해서는 UI 밖에서 신경쓸 일이 아닙니다.

Memory Leak

Context은 ViewModel과 다른 라이프사이클을 가지고 있습니다. 때문에 ViewModel에 Context를 전달하면 유출이될 위험이 있습니다.

Q: MVVM에서 ViewModel은 무엇인가요? 단지 StateHolder 인가요? 실제로 어떤 클래스에 해당하나요?

네, MVVM에서 ViewModel 은 StateHolder 입니다. 중요한 것은 매핑하는 클래스는 상황에 따라 달라진다는 것입니다. Android View 시스템, Compose, 컴포넌트, Navigation 이라면 Android Achitecture ViewModel 클래스를 사용하고 싶을 것입니다.
재사용 가능한 UI 는 다릅니다. 예를 들어, 조각들로 이루어진 그룹이 있는 경우를 보겠습니다. 몇 개의 선택 가능한 조각들이나 검색 표시줄과 같은 것들이 재사용 가능한 UI 에 해당합니다. 이 재사용 가능한 UI의 StateHolder 는 일반 클래스일 수 있습니다.
다시 말씀드리면, 해당 UI의 범위, navigation 범위 또는 여러 다른 context 에서 재사용 가능한 작은 UI 인지에 따라 달라집니다.
MVVM에서 ViewModel 은 StateHolder 이며, ViewModel 또는 일반 클래스일 수 있습니다.
추가로, 보통 ViewModel 을 화면 레벨의 특별한 StateHolder 유형으로 추천합니다.(Screen, Navigation ..) 하지만, UI를 재사용하고 싶다면 ViewModel 로 만들면 안됩니다. 결국 그 범위 내에서 제대로 동작되지 않을 것입니다.

Q: Flow를 DataSources, Repositories, Interactors, UseCases 어디에 사용해야 하나요?

가장 적합한 곳에 Flow 를 사용해야 합니다.
데이터를 생성하는 DataSource 에서 살펴보겠습니다. 만약 데이터가 네트워크 통신을 기반으로 하고, 단발성 네트워크 응답이라면 suspend function 이 맞을 수 있습니다. 만약 데이터가 Room 과 같은 데이터베이스에 기반한다면 아마 Flow, PagingSource 를 사용하고 싶을 것입니다.
Repository의 경우, Architecture 문서에서 언급한 대로 Repository 는 데이터의 충돌을 처리합니다. 데이터가 데이터베이스 또는 네트워크에서 전달되면 병합하고 처리합니다. 시간이 지남에 따라 이러한 값이 변경될 수 있으므로 Flow 로 이 값들을 사용할 수도 있습니다.
Flow는 시간에 따라 변화하는 sequence value 입니다. 따라서 시간이 지남에 따라 반영하는 내용이 변경될 수 있는 경우, 가장 적합한 곳에서 Flow를 사용해야 합니다.

Q: 언제 UseCase를 사용해야 하나요?

팀에서 아래의 문제를 해결하기 위해 UseCase 를 정의하였습니다.

ViewModel 에 있는 비즈니스 로직을 줄이는 것 — ViewModel 복잡성 줄이기

여러 화면에서 사용하고자 하는 비즈니스 로직이 있다고 가정해보겠습니다. 이 경우 모든 ViewModel 에서 동일한 로직을 반복하는 대신, UseCase를 생성하고 Domain Layer를 도입하여 실제로 해당 로직을 수행할 수 있습니다.
그리고 하나의 ViewModel 클래스에서 복잡성을 줄이고 싶을 때 이점이 있습니다. 여러 Repository 가 결합되어 있는 큰 ViewModel 클래스가 있다고 가정하겠습니다. 기능의 복잡한 부분을 UseCase 로 추출하여 쉽게 테스트 가능하고 ViewModel 의 가독성을 높일 수 있습니다.

ViewModel 가독성

ViewModel 이 Repository 대신 UseCase 에 의존하는 경우, ViewModel 의 선언 및 생성자 파라미터만 보아도 ViewModel 수행할 작업에 대해 알기 쉽습니다.

Q: ViewModel 이 UI 에 다른 Fragment/Screen 으로 이동하도록 지시하는 것에 권장되는 패턴은 무엇인가요?

ViewModel 에서 이동하도록 지시하는 경우, 가이드한 내용 중 UI Event 라는 것이 있습니다. 그러나 중요한 것은 UI Event 던지,실제로 UI 를 구축하는데 사용하는 것이던지, 모든 것을 상태 객체에 보관하는 것이 좋다는 것입니다. 그렇기 때문에 ViewModel 에서 화면으로 이동하려면 먼저, 상태 객체에 유지해야 합니다. 이후 UI 가 해당 이벤트를 수신하면 상태 객체에서 해당 이벤트를 제거합니다.
상태 객체 모델링하는 몇 가지 방법이 있습니다. 이 작업을 Boolean 으로 하거나 UI 에서 사용할 수 있는 메시지 목록 또는 navigation event 로 수행할 수 있습니다. UI 는 이벤트를 수신하고 ViewModel 에 이벤트를 수신했음을 전달합니다. 그러면 ViewModel 이 해당 이벤트를 제거합니다.
ViewModel 의 상태는 앱의 상태를 나타내는 것입니다. UI 에 특정 작업을 지시하는 것이 아닙니다. 그렇기 때문에 앱의 현재 상태 또는 시간이 지나면서 어떻게 상태가 변화하는지가 중요합니다.

Q: MVVM/CleanArchitecture 에서 WorkManager 와 Service 는 어디에 배치되어야 하나요?

보통 WorkManager, Service 같은 것들은 비즈니스 계층보다 낮은 계층에 존재하거나 비즈니스 계층의 일부로 사용됩니다. WorkManager 는 Android 에 특화되어있습니다. Wi-Fi 연결, 배터리 절감 효과를 갖춘 환경에서 실행된다고 가정할 때 Android 내장 로직이 많기 때문입니다. 그렇기 때문에 Android Framework API 들을 사용할 수 있는 환경에 있어야 합니다.
궁극적으로 WorkManager 가 하는 일은 자신의 코드로 다시 호출하는 것 뿐입니다. 다시 호출하는 작업의 종류는 지속적인 작업읹, 반복적인 작업인지, 지연된 작업인지에 따라 달라집니다. 그리고 Android Architecture 어디에나 들어갈 수 있습니다.

Q: 오류를 다른 계층에서 Presentation Layer 로 리턴하는 가장 좋은 방법은 무엇인가요?

예외를 Presentation Layer 로 전파하는 방법으로 Coroutine Exception Handling 메커니즘만을 사용할 것을 권장합니다. Result 클래스도 유용하지만 문제는 모든 예외를 포착하지 못합니다. Result 클래스로 결과를 받을 경우 모든 계층에 이 호출이 존재하여 결과를 에러로 전환할 수 있게 해야하기 때문입니다. 만약 코틀린 코투린을 사용하고 있다면 이미 오류를 전송하는 exception 메커니즘이 있습니다. 그리고 아래 계층에서도 이 예외를 catch 할 수 있습니다.
모든 것에 예외를 잡고 result 로 변환하고 리턴하는 것은, 도움이 되지 않습니다. 더 높은 레벨에서도 수행할 수 있는 내용이기 때문입니다.

Q: Domain Layer 모델과 API/DB 모델을 매핑하는 Mapper 를 어느 계층에 배치해야 하나요?

가장 적합한 곳에 배치해야 합니다.
앱 전체에서 사용하는 모델이 있다고 가정해보겠습니다. 그리고 앱의 핵심 모듈이라고 부르겠습니다. 또한 Data Layer 도 존재하고 그 안에는 Repository, Network 모듈도 있습니다. UI 가 네트워크 모델을 처리할 가능성은 거의 없습니다. 그러니 네트워크 모델은 아닙니다.
Repository 모듈에서는, 앱은 Repository 를 통해 Data Layer 를 다소 소비하게 됩니다. 앱이 보여줘야할 외부 모델을 Network 모델과 매핑하는 로직이 Repository 모듈에 있어야 합니다. 왜냐하면 사용되고 정의하는 유일한 장소이기 때문입니다. 그리고 매핑한 외부 모델을 외부적으로 소비하게 됩니다.
공통 모듈에 모든 것을 포함시키고 싶지 않은 이유는 공통 모듈이 핫패스에 있기 때문입니다. 만약 공통 모듈을 편집하고 앱을 다시 만들어야 한다면 이미 많은 것들이 무효화된 상태일 것입니다. 그리고 그 시점에 이미 모듈화의 모든 이점을 잃게 됩니다.
그렇다면 API 에서 DB 모듈로 매핑하는 Domain 모듈을 어디에 두어야 할까요 ?
물론 매핑하는 곳에 배치해야 합니다. 왜냐하면 그곳이 유일하게 호출되는 곳이기 때문입니다. 만약 다른곳에 위치한다면 앱 모듈화를 위해 한 모든 아름다운 작업이 무효화될 수 있습니다.

Q: 다중 Activity? Single Activity Multiple Fragments?

다중 Activity 를 구현하는 상황에 대해 생각해 보겠습니다. 앱에 대한 여러 진입점이 있으면 다중 Activity 를 사용할 수도 있습니다. 하지만 만약 앱이 화면간 이동만 하고 있다면 Activity 를 사용하지 마십시오. 다른 Activity 를 실행하면 운영체제에게 이 intent 를 실행시켜달라고 부탁하게 됩니다. 그리고 운영체제는 intent 를 처리할 수 있는 Activity 를 가지고 있는지 검색하고 Activity 를 실행할 수 있게 해줍니다. 다음 화면으로 이동하기 위해 운영체제를 거쳐야 하는 것입니다. 정말 이상한 방법입니다.
Jetpack Navigation 으로 Fragment 를 사용하는 것이 훨씬 쉬워졌습니다. 더 나아가 Compose 에 도달했다면 Fragment 마저 필요하지 않을 것입니다. Compose Navigation 을 사용하면 이러한 진입점에 대한 복잡한 작업을 수행할 필요가 없습니다.
진입점을 보면 운영체제의 다른 앱이 여러분의 앱을 실행할 수 있을 것입니다. 여러분이 깨끗하게 다루기를 원하는 다른 Activity 들이겠죠. 이것을 피할 수도 있지만 그것은 더 복잡해질 것입니다. 그래서 Activity를 분리하는 것이 타당합니다. Activity 를 운영체제에 대한 API 라고 생각할 수도 있습니다. 런처가 앱을 시작하는데 사용하기 때문에 런처 Activity 는 필요합니다. 그 밖에 필요한 것이 있다면 설정해도 괜찮습니다. 하지만 그렇지 않은 상황이라면 앱에서 다른 Activity는 필요하지 않습니다.

--

--