Android Architecture: Part2. MVVM 적용하기

박성철
How we build MyRealTrip
11 min readMay 18, 2022

지난 포스트에서는 MVVM 패턴을 통한 관심사의 분리와 다양한 소프트웨어 아키텍처에 대해서 알아보았고, 이번에는 마이리얼트립에서 어떻게 프로젝트 아키텍처를 적용하는지에 대해 공유해 보려고 합니다.

추상적인 개발방법론을 구체화하고 다수의 팀 구성원들과 함께 동일한 기준에 맞춰 개발하는 것은 쉽지 않은 일입니다. 엔지니어들 간의 의견이 서로 다를 수 있고, 결론까지 도달하기 위해서 많은 시간이 소요됩니다. 이러한 현상은 여러 회사에서 흔히 발생하며, AngularJS에서는 MVW(Model-View-Whatever)라는 패턴을 만들어 불필요한 논쟁 시간을 줄이고자 하는 사례도 있습니다.

마이리얼트립에서는 MVVM 패턴의 Model, View, ViewModel로부터 무엇을 기대하는지, 어떠한 역할을 위임해야 하는지를 서비스 관점에서 실제 사례를 바탕으로 논의했습니다. 또한 지속적으로 발전하는 Android Platform에 부합할 수 있는 모습인지도 함께 고려했습니다.

Model

Model refers either to a domain model, which represents real state content (an object-oriented approach), or to the data access layer, which represents content (a data-centric approach).

모델은 실제 상태 콘텐츠(객체 지향 접근 방식)를 나타내는 도메인 모델 또는 콘텐츠(데이터 중심 접근 방식)를 나타내는 데이터 액세스 계층을 가리킵니다.

마이리얼트립 서비스는 검색, 후기, 인증 등의 공통적인 플랫폼을 기반으로 항공, 숙소, 투어/티켓 등의 다양한 서비스를 제공하며, Data 기반으로 의사결정을 하기 위해 많은 데이터를 수집, 운영, 관리하고 있습니다.

서비스에서 기본적인 데이터 처리(CRUD)는 내부/외부저장소를 활용합니다. 특정한 경우 이미지 파일을 업로드하기도 하고, GPS 정보를 활용해 서비스를 제공하기도 합니다. 또한 목적에 따라 네이티브 코드와 웹뷰를 제공하며 사용자의 경험을 개선하기 위해 많은 데이터를 참조하고 있습니다.

폭넓은 도메인, 복잡한 비즈니스 속에서 Model에 기대하는 것이 무엇인지 고민해 보았습니다.

  • 내부저장소(ex. DB)에서 데이터를 생성, 갱신할 수 있는가?
  • 외부저장소(ex. Server API)에서 데이터를 생성, 갱신할 수 있는가?
  • 생성된 이미지 등의 콘텐츠들을 CDN 등과 외부저장소에 저장, 삭제할 수 있는가?
  • AB Test를 포함해 사용자의 화면 갱신 없이 데이터 분석을 위한 로그를 전송할 수 있는가?
  • 사용자의 GPS의 정보를 읽어올 수 있는가?
  • 외부저장소에서 읽어온 데이터와 View에 보여주는 모델이 상이하다면, 데이터 구조를 리모델링할 수 있는가?
  • 내부/외부저장소에서 제공되는 데이터들은 Immutable 해야 하는가?
  • WebView에서 발생하는 이벤트(ex. shouldoverrideurlloading)를 ViewModel을 통해 Model에서 처리할 수 있는가?

기본적인 데이터 처리뿐만 아니라 이미지 업로드, 로그 발송, GPS 정보획득 등을 단일한 Data Access Component로 규정했습니다. 그리고 이러한 Component들을 비즈니스 로직에 맞게 조합할 수 있는 구조가 마이리얼트립에 적합하다고 생각했습니다.

이는 우리가 흔히 알고 있는 UseCase와 Repository의 관계에서 크게 벗어나지 않았습니다. UseCase의 재활용을 통해 ViewModel이 한결 더 단순해졌고, View의 이벤트와 사용자 UseCase를 결합하는 역할을 기대할 수 있었습니다.

마이리얼트립에서는 UseCase의 자유도를 높여 합리적으로 각 서비스를 관리하도록 권고하고 있습니다. 예를 들어, 후기라는 도메인이 있다면 크게 2가지 형태의 UseCase 생성을 고민해 볼 수 있습니다.

  • 1) 도메인 당 단일 UseCase 생성 (ReviewUseCase) : 후기와 관련된 모든 UseCase를 한곳에 모아서 관리
  • 2) 도메인 내 다수 UseCase 생성 (ReviewListUseCase, ReviewFormUseCase 등) : 후기와 관련된 UseCase를 특정 기능 단위로 분리해 관리

그뿐만 아니라, 엔지니어는 도메인의 특성에 따라 UseCase 생성을 선택하고, 때로는 UseCase에서 다른 UseCase를 참조함으로써 비즈니스 로직을 보다 효율적으로 관리하고 있습니다.

마이리얼트립에서는 구글에서 권장하는 Layered Architecture 기반으로 MVVM을 적용해 보았습니다. 구글에서 Domain Layer 사용을 필수로 권고하지는 않지만, 마이리얼트립 서비스에서는 Domain Layer가 복잡한 비즈니스 로직을 담당하게 했습니다. 그리고 Data Layer를 통하여 단일 기능을 처리하는 Data Access Component들을 관리하고 있습니다.

우리가 흔히 생각하는 MVVM과 동일하지만, Model에 기대하는 역할을 정의함으로써 엔지니어들 간 생각의 차이를 줄일 수 있었습니다.

View

As in the model–view–controller (MVC) and model–view–presenter (MVP) patterns, the view is the structure, layout, and appearance of what a user sees on the screen. It displays a representation of the model and receives the user’s interaction with the view (mouse clicks, keyboard input, screen tap gestures, etc.), and it forwards the handling of these to the view model via the data binding (properties, event callbacks, etc.) that is defined to link the view and view model.

모델-뷰-컨트롤러(MVC)와 모델-뷰-프레젠터(MVP) 패턴에서와 같이, 뷰는 사용자가 화면에서 보는 것의 구조, 레이아웃, 외관을 말합니다. 모델의 표현을 표시하여 사용자가 뷰와 상호 작용(마우스 클릭, 키보드 입력, 화면 탭 제스처 등)을 수신하고 뷰와 뷰 모델을 연결하도록 정의된 데이터 바인딩(속성, 이벤트 콜백 등)을 통해 뷰 모델로 처리를 전달합니다.

폭넓은 도메인과 복잡한 비즈니스 로직들의 고민은 Model의 역할을 규정함으로써 많은 부분이 해소되었습니다. 마이리얼트립에서는 View의 재사용을 극대화하고, 신뢰성 높은 안정적인 시스템을 구축하기 위해서 View의 역할을 최소화하기 위한 방안을 고민했습니다.

  • 직접적으로 Entity에 접근해 필요한 데이터를 가져와서 화면을 구성해야 하는가?
  • Model에 요청이 필요없는 이벤트로 UI를 업데이트할 때 ViewModel로 이벤트를 전달하지 않아도 되는가?
  • 클릭 이벤트 발생 시 로그 데이터를 발송해야 하는가?
  • 라이프사이클 혹은 이벤트 발생 시 loadData과 같은 Action에 대해 ViewModel로 요청해야 하는가?
  • 스크롤링과 같은 이벤트(1px 이동할 때마다 발생하는 이벤트)를 모든 ViewModel로 요청해야 하는가?
  • AB Test 값을 직접 비교하여 다양한 UI/UX를 화면에 구성해야 하는가?
  • MVVM과 같은 프로젝트 아키텍처의 뷰 초기화 작업을 어디에서 해야 하는가?

View는 데이터의 변경을 감지해 사용자 화면에 업데이트합니다. AAC(Android Architecture Components)의 LiveData를 통해 화면이 활성화 되어 있을 때만 동작하도록 하여 메모리 릭을 줄이고, 중지된 컴포넌트로 인해Crash 발생이 방지되었습니다. 데이터 관리도 ViewModel에 이관함으로써 라이프사이클도 간결하게 관리할 수 있었습니다.

그리고 바인딩 된 데이터를 화면에 표현하고, 발생하는 이벤트를 선별하고, 선별한 이벤트만을 ViewModel에 전달할 수 있도록 집중했습니다. 예를 들어, 스크롤 이벤트를 비롯해 View 관점에서 업데이트하는 경우에는 내부적으로 이를 처리하고, 필요에 따라 EventTrigger를 통해 ViewModel을 호출하는 것을 권장하고 있습니다.

Clean Architecture 그리고 선택

View의 재활용성을 높이기 위해서는 View에 부합하는 Entity가 필요합니다. 예를 들어, 사용자에게 노출되는 Layout이 동일하지만, 도메인이 다른 API로부터 상이한 DataModel 응답이 있을 경우, 우리는 DataModel에 인터페이스를 활용하거나 컨버팅하여 대응할 수 있습니다. 그리고 사용자의 콘텐츠를 수정해야 하는 경우, 불변 객체(Value Object)인 DataModel을 복사하여 업데이트하기도 합니다.

우리는 View의 재활용과 Mutable한 Object를 위해서 Mapper를 활용할 수 있습니다. 하지만, 마이리얼트립 서비스의 많은 부분은 Repository의 응답을 그대로 활용하는 경우가 많습니다. 이로 인해 불필요한 Mapper가 생성될 수 있어 클린 아키텍처 도입에 대해 부정적인 의견도 있었습니다. 그래서 우리는 DataModel과 Entity가 일치하는 경우, 클래스 간 변환을 쉽게 하기 위한 라이브러리를 적용하여 이를 보완하기로 했습니다. 그리고 실제 클린 아키텍처를 도입하고 이후에 회고를 통해 타당성을 검토해 보기로 했습니다.

Testable

우리는 단위테스트를 통해서 소스 코드의 특정 모듈이 의도대로 정확하게 동작하는지 검증하며 신뢰성을 확보합니다. 그리고 이러한 테스트는 신속하고 반복적으로 이루어져야 합니다. 또한 UI 자동화 테스트 혹은 무작위 테스트를 통해서 실제 사용자의 다양한 액션을 테스트할 수 있습니다.

마이리얼트립에서는 구조화된 코드를 통해서 코드 품질 향상과 유지보수의 편의성을 보장하고, 단일화된 프로젝트 아키텍처를 통해 가독성을 높일 수 있었습니다. 그리고 관심사의 분리를 통해 Android Framework의 의존성을 제거함으로써 편리하게 테스트를 진행할 수 있는 환경을 마련했습니다.

MVVM 패턴은 Android Framework에 의존성을 가지고 있는 UI, 비즈니스 로직을 담당하고 있는 ViewModel, 그리고 Model을 명확히 구분하고 있습니다. 이로 인해 ViewModel과 Model은 단위테스트가 용이해졌고, 정적분석을 비롯한 테스트 커버리지를 확보하고자 합니다. 그리고 엔지니어들이 레거시 시스템을 개편할 때 이를 활용하여 안정성 있는 시스템을 만들도록 권장하고 있습니다.

우리는 UI 테스트를 통해서 사용자의 시나리오를 테스트할 수 있습니다. 하지만, 마이리얼트립은 서비스의 성장에 발맞춰 빠르고 잦게 UI/UX를 업데이트 하기 때문에 AB 테스트와 UI 테스트에 큰 유지보수 비용이 발생할 것입니다. 이에 따라 마이리얼트립에서는 신뢰성 있는 데이터 기반의 서비스를 위해, 사용자 시나리오의 로그 데이터를 검증하는 것이 장기적인 목표입니다.

오늘 포스트는 마이리얼트립에서 MVVM 패턴을 어떻게 적용하고 있는지 정리해 보았습니다. 비즈니스의 요구사항을 개발하면서 기존의 레거시를 개선하는 것은 쉬운 일이 아니며, 많은 엔지니어들의 시간과 노력이 필요하기 때문에 점진적으로 문제를 해결해나가고 있습니다. 마이리얼트립에서는 서비스 매트릭을 관리하여 빠르게 문제를 인지하고 해결할 수 있도록 끊임없이 발전하고 있습니다. 다음 포스트는 마이리얼트립에서 적용하고 있는 Server-driven UI에 대해 이야기해 보고자 합니다.

마이리얼트립에서는 더 나은 모바일 애플리케이션을 개발하고 고객에게 최상의 서비스를 제공하고자 노력하고 있습니다. 함께 서비스와 팀을 성장시킬 분들의 많은 지원 부탁드립니다.

- 마이리얼트립은 지금 채용중! https://career.myrealtrip.com/

--

--