Android Architecture: Part1. 관심사의 분리와 아키텍처
지난 포스트에서는 멋진 모바일 엔지니어링 팀을 만들기 위한 전략을 포괄적으로 기재하였고, 이번 포스트에는 많은 엔지니어들이 함께 만들어가는 프로젝트 아키텍처에 대하여 이야기해 보려고 합니다.
우리는 새로운 프로젝트를 진행하게 되면 Android SDK, Convention 등을 고려하며 모바일 애플리케이션을 개발합니다. 그리고 개발방식은 해를 거듭하여 발전해 왔고 수년 전과 비교하면 많은 것들이 달라졌습니다.
마이리얼트립은 대고객 서비스를 제공하는 플랫폼으로서 최상의 고객 경험을 선사하기 위해 다양한 기능을 개발하고, 애플리케이션의 성능을 최적화하며, UX와 프로세스를 더 직관적인 형태로 개선하기 위해 많은 시간을 보내고 있습니다. 하지만 이런 과정속에서 많은 엔지니어들이 일관성, 생산성과 유지보수를 효과적으로 할 수 있는 개발 코드를 유지하는 것은 쉬운 일이 아닙니다.
안티 패턴 그리고 또 다른 문제 등장
프로젝트 초기에는 Android 엔지니어들도 많지 않았고, 요구사항 역시 복잡하지 않았습니다. 하지만 지속적으로 비즈니스는 성장해왔고 이로 인해 악명 높은 안티 패턴인 갓(God) 클래스/오브젝트/모듈(많은 일을 하고 매우 복잡해서 아무도 건드릴 엄두가 나지 않는 클래스)이 생겨났습니다.
이를 개선하기 위해 MVC/MVP/MVVM 패턴을 부분적으로 도입하였지만, 정확한 패턴을 끝까지 적용하지 않고 또 다른 MVI 패턴을 부분적으로 적용하면서 프로젝트는 더 복잡해졌습니다. 이로 인해 새로운 기능을 추가할 때마다 각 패턴에 맞춰 다른 방식으로 개발하기가 쉽지 않았고 지속적인 통합, 개선에 부족한 점들이 많았습니다.
그뿐만 아니라, 너무나 많은 패턴 속에서 팀 구성원들은 각각의 패턴을 다르게 생각하고 있었고 이러한 생각의 차이를 좁히고자 했습니다.
관심사의 분리(Separation of Concerns, SoC)
대부분의 Android 엔지니어들은 관심사의 분리에 대해 알고 있거나 MVVM 혹은 그 유사한 패턴을 적용하고 있을 것이라 생각합니다. 우리는 어떠한 패턴을 선정하는 것보다는 관심사의 분리를 통해서 무엇을 얻고자 하는지 생각해 보았습니다.
- 전반적인 코드 품질 향상
- 많은 엔지니어들이 함께 개발할 수 있도록 유지보수의 편의성을 보장
- 어떤 개발자들이 개발해도 쉽게 눈에 들어오는 코드의 가독성
- 유닛테스트를 통한 신뢰성 및 안정성 확보
먼저 개발 커뮤니티에서 범용적으로 널리 사용되고 있는 패턴(입증되고 신뢰할 수 있는 표준)을 적용하고, 프로젝트에 전체적용 이후에 추가적인 논의와 개선을 진행하기로 하였습니다.
MVVM 패턴은 Model, View, ViewModel로 구성되며, 이 패턴은 UI와 비즈니스 로직이 명확히 구분됩니다.
- Model: 비즈니스 규칙, 데이터 접근, 모델 클래스를 처리
- View: 사용자의 입력을 ViewModel에 위임
- ViewModel: View와 Model 사이의 중개자 역할
MVVM은 MVP의 단점인 콜백 지옥을 보완한 패턴으로 MVP와 비슷하게 테스트/유지보수/확장성이 뛰어납니다. 그리고 DataBinding, LiveData 등 Android Architecture Components의 활용이 쉬울 것으로 생각했으며, DataBinding(XML 파일) 디버깅이 어렵다는 점은 추후에 개선하기로 하고 MVVM 패턴을 프로젝트에 적용하기로 선정하였습니다.
또한, MVI(UDF)는 MVVM을 제대로 적용한다면 어렵지 않게 전환할 수 있을거라고 의견을 나누었습니다.
프로젝트의 아키텍처를 선정하는 것만으로는 관심사의 분리가 완전히 해결되지 않습니다. 명확한 관심사의 분리를 위해서는 소프트웨어의 구성요소들 사이에서 유기적인 관계를 표현하고 소프트웨어의 설계와 업그레이드를 통제하는 ‘소프트웨어 아키텍처’의 지침과 원칙이 필요합니다.
마이리얼트립에서는 소프트웨어 엔지니어라면, 누구나 한번쯤 들어보았을 3계층 아키텍처와 클린 아키텍처를 기반으로 실질적으로 어떻게 시스템에 적용할 수 있을지 분석해 보았습니다.
3계층 아키텍처
어떠한 플랫폼을 3계층 혹은 N계층으로 나누어 별도로 논리적/물리적인 장치를 구축 및 운영하는 형태를 3계층 아키텍처(3-Tier Architecture 혹은 Layered Architecture)라고 부릅니다.
- Presentation Layer: 사용자와 직접 마주하는 계층으로 UI를 지원
- Business Logic Layer(Application Layer): 어떠한 데이터가 필요한지 결정하는 비즈니스 로직 처리
- Data Access Layer: 데이터베이스 혹은 저장공간에 접근하여 데이터를 읽거나 쓰는 처리
흔히 웹 애플리케이션에서 많이 사용하는 3계층 아키텍처는 각 레이어들이 특정한 관심사와 관련된 개체만을 포함하도록 설계하여 시스템의 결합도를 낮추고 개발자의 인지 과부하를 방지하며 재사용성을 보이고 유지 보수성을 향상했으며, 이는 Android 개발에서도 유사하게 적용할 수 있습니다.
3계층 아키텍처의 모든 의존성은 결국 Data Access Layer로 이동하고 있고, 우리는 이러한 순차적인 의존성의 흐름에 익숙한 편입니다. 하지만 Data Access Layer 의존성 때문에 Data 기반으로 Business Logic을 처리하는 문제점이 있을 수 있습니다.
다시 말해, 우리가 원하는 비즈니스 모델(Entity)이 있을 때 비즈니스 모델을 정하고 그에 맞는 데이터를 가지고 와서 조합(Mapper) 하는 형식이어야 하지만, 3계층 아키텍처에서는 데이터를 기반으로 비즈니스 모델을 조합하는 형식으로 이루어질 가능성이 있습니다.
클린 아키텍처
클린 아키텍처는 일명 ‘엉클 밥’이라고 불리는 로버트 C. 마틴이 2021년 엔터프라이즈 아키텍처에서 논의되던 내용을 집약시킨 개념입니다. 단일 책임 원칙(Single Responsibility Principle)을 포함한 5가지의 SOLID 원칙을 중심으로, 계층들을 세부화하여 어떤 요소로 구성하고 역할을 할지를 안내하고 있습니다.
- Entities: 가장 일반적인 비즈니스 규칙을 캡슐화하고 VO(Value Object, 불변 객체)도 포함하는 전사적 비즈니스 규칙
- Use Cases: 애플리케이션의 비즈니스 규칙
- Interface Adapters(Presenters): 데이터를 Entity 및 UseCase의 편리한 형식(Format)에서 데이터베이스 및 웹에 적용할 수 있는 형식으로 변환
- Frameworks&Drivers: 웹 프레임 워크, 데이터베이스, UI, HTTP 클라이언트 등으로 구성된 가장 바깥쪽 계층
Dependency Rule
클린 아키텍처과 3계층 아키텍처는 각각의 영역 혹은 계층들이 독립적으로 관심사를 분리하여 처리하고 있습니다. 그리고 클린 아키텍처는 위 그림과 같이 외부 레이어는 내부 레이어에 의존(Blue → Green → Red → Yellow)해야합니다.
즉, 내부 레이어에서는 외부 레이어에서 어떠한 처리를 하는지 알지 못합니다. 이를 통해 클린 아키텍처는 3계층 아키텍처와는 다르게 Entities와 Use Cases가 가장 가운데 존재함으로써 데이터베이스의 의존성을 가지지 않는다는 것을 알 수 있습니다.
클린 아키텍처는 비즈니스 계층을 구조적으로 명확히 하여 Data Access Layer 에서 자칫 처리할 수 있는 비즈니스 로직을 Business Logic Layer에서 처리할 수 있도록 하고 있습니다. 다만, Database 혹은 Server AP I 등에서 단순한 데이터에 기반을 두고 이를 UI에서 그대로 사용한다면 비슷한 유형인 Mapper, Data Model, Entity 들이 생성됨으로써 불필요한 코드들이 생성될 수 있습니다.
구글의 권장 앱 아키텍처
3계층 아키텍처와 클린 아키텍처 모두 기본적인 관심사의 분리를 통해서 효율적인 개발을 할 수 있도록 도와주고 있습니다. 최근 이에 관한 내용을 구글 안드로이드 공식 사이트에서도 업데이트되었습니다. 위 아키텍처들과 용어는 다를 수 있지만, 기본적인 개념들은 거의 같다고 볼 수 있습니다.
- UI Layer(View-ViewModel)의 역할은 화면에 애플리케이션 데이터를 표시하는 것이며, 사용자의 인터렉션(예, 페이지 진입 혹은 버튼 클릭)으로 인해 발생한 이벤트를 도메인 레이어로 전달하고 데이터가 변할 때마다 변경사항을 반영하는 것을 담당합니다. 기본적으로 Data Layer에서 가져온 값과 Domain Layer에서 업데이트된 애플리케이션 상태를 시각적으로 나타냅니다.
- Data Layer(Repository)는 비즈니스 로직을 포함하고 있으며, 데이터의 생성, 저장, 변경 방식을 결정하는 다양한 유형의 데이터마다 저장소 클래스를 만듭니다. 또한 데이터 변경사항을 한 곳에 집중하거나 데이터 소스 간의 충돌 해결 등을 담당합니다. 이 레이어에서 발생한 데이터들은 변경이 불가능해야 하고, 이를 통해 여러 스레드에서 안전하게 처리할 수 있도록 유지합니다.
- Domain Layer(UseCases)는 복잡한 비즈니스 로직, 또는 ViewModel에서 재사용되는 간단한 비즈니스 로직을 캡슐화하여 중복코드(예, 대부분의 퍼널에서 사용하는 좋아요 혹은 위시 처리를 위한 공통 로직)를 제거합니다. 이 레이어는 선택적으로 복잡성을 처리하거나, 재사용이 필요한 경우 사용합니다.
구글에서 권장하는 앱 아키텍처는 모바일 환경의 특성(심플하거나 많은 로직들이 서버로 이전됨)을 이해하고 이에 따라 Domain Layer를 조금 더 간소화하여 Data Layer를 직접 처리하는 방식을 선택할 수도 있다고 생각합니다. (마이리얼트립에서는 Android Developer 가이드와 달리, UseCases를 항상 만들어서 적용하는 것을 권장하도록 하여 추후에 발생할 수 있는 고민거리들을 사전에 해결하도록 하였습니다.)
마무리
우리는 프로젝트의 아키텍처를 선정하고 여러 가지 원칙들을 리뷰하고 최대한 합의를 맞추려고 노력하지만, 이는 결코 쉬운 일이 아닙니다. 의존성의 규칙을 강제하기 위해서 시스템적으로 보완할 수 있는 장치들을 두기도 합니다. 하지만 이로 인해 의도하지 않은 영향이 발생할 수 있기 때문에 안정적으로 자리매김하기 전까지는 지속적으로 동료 엔지니어들과 함께 원칙에 대해 논의하고 지키도록 권고하고 있습니다.
특정한 아키텍처가 정답이라고 말할 수 없습니다. 마이리얼트립에서는 적합한 아키텍처를 찾아 이에 맞는 규칙들을 조금 더 세분화하고, 지속해서 동일한 기준을 바탕으로 일하기 위해 노력하고 있습니다.
관심사의 분리를 통해 전반적인 코드의 품질을 향상시킴과 동시에, 빠르게 변화하고 있는 여행시장에서 유지 보수성이 뛰어나고 코드 테스트 커버리지를 높이는 등 안정성 있는 시스템을 구축하기 위해서 함께 고민하고 있습니다.
오늘 포스트는 관심사의 분리와 소프트웨어 아키텍처의 이론에 대해 마이리얼트립에서 나누었던 이야기를 정리해 보았습니다. 다음 포스트에는 실제 어떤 의사결정을 했고, 이를 적용하게 되었는지 사례를 바탕으로 이야기해볼까 합니다.
마이리얼트립에서는 더 나은 모바일 애플리케이션을 개발하고 고객에게 최상의 서비스를 제공하고자 노력하고 있습니다. 함께 서비스와 팀을 성장시킬 분들의 많은 지원 부탁드립니다.
- 마이리얼트립은 지금 채용중! https://career.myrealtrip.com/