Photo by Alex wong on Unsplash

교체 가능한 안드로이드 아키텍처

kimdohun0104
8 min readFeb 6, 2020

소프트웨어를 개발하다 보면 갑작스러운 변화는 항상 찾아오기 마련입니다. 비밀번호의 유효성 검증 방식이 달라지는 작은 변화부터, Room을 사용하던 로컬 데이터베이스를 Realm으로 변경해야 하는 중대한 사항까지. 그 내용도, 규모도 다양합니다. 저는 이전 프로젝트의 경험으로부터 아키텍처를 꾸준히 발전 시켜 왔습니다. 하지만 큰 변화는 항상 저에게 두려운 존재였습니다. 이 글에선 로버트 C. 마틴의 클린 아키텍처를 읽으며 얻게 된 아이디어와 새로운 프로젝트인 '시키라'에 적용된 교체 가능한 구조에 대해 살펴보겠습니다.

아키텍처는 정답이 있다?

저는 과거에 아키텍처는 답이 정해져 있다고 생각했습니다. 저는 답을 찾기 위해서 검증된 아키텍처 가이드라인이 필요했고 많은 시간을 소요했습니다. 미리 말씀드리자면 최고의 아키텍처는 존재하지 않습니다. 저희는 각자 요구사항과 배경이 다른 환경에서 프로그램을 개발합니다. 서버 통신이 필요 없는 간단한 TODO앱과 수많은 기능이 내장된 카카오톡의 아키텍처가 같다면 잘 설계한 것일까요? 오히려 오버 엔지니어링으로 인해 복잡도가 올라갈 가능성이 높습니다. 아키텍처는 상황에 따라, 기능에 따라, 요구사항에 따라 유연하게 변화할 수 있어야 합니다. 이 글에서 설명하는 내용은 팀에서 진행하고 있는 '시키라' 프로젝트에 적합하게 설계된 구조입니다. 제가 설계한 구조를 통해서 자신에게 맞는 아이디어를 얻으셨으면 좋겠습니다.

세부사항?

소프트웨어를 개발하는 데 있어서 가장 중요한 부분 중 하나는 요구사항을 만족하는 기능입니다. 사용자는 데이터가 안정적으로 저장되고 출력되는 것만 확인한다면 문제 될 것이 없습니다. 우리가 서버 통신에 Retrofit을 사용하거나 Room, Realm을 통해 로컬 데이터베이스를 구축하는 것은 중요하지 않은 세부사항입니다. 물론 서비스가 안정적이고 원활하게 동작하려면 꼭 필요한 부분이지만, 제 말은 잠시 뒤로 미루어 두어도 괜찮다는 것입니다. 저희가 개발하면서 집중해야 하는 부분은 업무 규칙(Business Rules)을 해결하기 위한 로직이니까요. 세부사항에서 발생하는 예상치 못한 이슈나 제약 사항은 정작 우리가 해결해야 하는 문제를 뒤로 늦춥니다. 그렇기 때문에 우리는 세부사항과 업무 로직(Business Logic)을 분리해야 합니다.

교체 가능한 아키텍처?

채워진 화살표는 사용 관계를 뜻하며 채워지지 않은 화살표는 구현 또는 확장의 개념입니다. 정사각형은 인터페이스를 의미합니다

위 그림은 '시키라'의 일부분을 다이어그램으로 표현한 모습입니다. Repository에선 DataSource interface를 참조하여 DAO와 Retrofit을 직접적으로 알지 못합니다. 이를 통해서 업무 규칙과 세부사항의 분리를 이루어낸 모습을 확인할 수 있습니다.

그리고 흥미로운 사실을 한 가지 더 발견할 수 있는데 DataSourceImpl에선 화살표가 나오기만 하고 들어오는 모습을 볼 수 없습니다. 이는 DataSourceImpl의 존재를 무엇도 알지 못하며 언제든지 교체 가능하다는 사실을 발견할 수 있습니다. 그래서 세부사항이 크게 변경되는 상황에서도 유연하게 대처할 수 있습니다. 아래의 사례를 통해서 한 번 확인해보도록 하겠습니다.

영업팀에서는 '시키라'가 로컬 데이터베이스를 사용하는 것이 탐탁지 않았습니다. 그래서 개발팀에게 마법의 주문을 외우기 시작했습니다.

1주일 내로 Room을 파일 시스템으로 대체해주세요. 이유는 묻지 마시고요.

이를 어쩐담, 하지만 걱정하지 마세요. 저희는 교체 가능한 구조를 가지고 있습니다.

저희는 DataSource를 구현하는 FileDataSourceImpl를 Repository로 주입해주는 것 만으로 기존의 Room을 사용하는 방식을 유지하면서도 파일 시스템을 성공적으로 도입했습니다. 변덕이 심한 영업팀에서 다시 Room으로 복구하길 원한다면 그저 의존성 주입 코드만 약간 수정해주면 정상적으로 구동하는 모습을 확인할 수 있습니다. 이러한 방식으로 우리는 Retrofit, Room과 같은 세부사항을 개발 마지막까지 미룰 수 있고 심지어 Repository를 테스팅하는데 활용할 수 있습니다. 이제 '교체 가능한 아키텍처'가 무엇인지 감이 잡히셨나요?

의존성의 흐름

위 그림은 '시키라'의 의존성 흐름을 나타내는 간단한 다이어그램입니다. 위 그림에선 의존성 방향에 대한 한 가지 특징을 발견할 수 있습니다. 그렇습니다. 의존성은 모두 한쪽으로 향하고 있다는 것이죠. ViewModel에서 나온 화살표는 다시 ViewModel로 돌아올 수 없다는 것을 의미합니다. 이런 단방향 의존성이 어떤 역할을 할까요?

왼쪽의 그림에서 Component A에 변경사항이 발생한다면 어떻게 될까요? A에 의존성이 있던 C가 영향을 받을 것이고, B 까지도 영향을 끼치게 됩니다. 이런 순환 의존성은 프로그램의 복잡도를 높이고 변화에 대처하기 어렵게 만듭니다. 그래서 의존성은 항상 단방향을 유지하고 순환은 배제하도록 노력해야합니다. 잠시 익숙한 MVP(Model-View-Presenter) 구조에 대해 살펴보도록 하겠습니다.

MVP 구조는 다이어그램과 같이 양방향 관계를 맺고 있습니다. 어떻게 해결해야 할까요?

사실은 이미 많은 분들이 Contract패턴을 통해서 멋지게 해결하고 있습니다. 다이어그램을 통해 확인할 수 있듯이, 구현체에 비해서 변동성이 낮은 Interface에 의존함으로서 변화에 안정적으로 대처할 수 있도록 설계되었습니다. 자연스럽게 Presenter, View는 위에서 말한 교체 가능한 모듈의 형태를 띄게됩니다.

관심사 분리

아키텍처에 대한 관심은 관심사를 분리하기 위해서 시작되었습니다. 오류가 발생했을 때 어디서부터 디버깅을 시작해야 하는지 알 수 없었기 때문입니다. 좋은 아키텍처는 오류가 발생한 모듈을 한 번에 파악할 수 있어야 합니다.

ViewModel: 입력 값 유효성 검증과 같은 애플리케이션 단위의 업무 규칙을 작성하고, UseCase를 통해 API를 호출합니다. 결과는 DataBinding을 통해서 View로 전달됩니다.

UseCase: 하나 또는 그 이상의 레포지토리에서 흘러온 데이터를 ViewModel로 전달하기 전 UseCase에 맞게 데이터를 가공합니다. 예를 들어서, getRestaurantInfo, isInterestRestaurant의 결과를 하나의 도메인 모델에 묶어 ViewModel로 전달합니다. UseCase를 사용하지 않는다면 이러한 작업들은 ViewModel에서 처리해주어야 하는데, ViewModel은 앱에서 빠르게 변화하는 중요한 부분 중 하나이기 때문에 앱의 성장에 방해가 될 수 있습니다.

Repository: 직접 세부사항과 상호작용 하는 DataSource를 통해서 로컬 혹은 서버, 어디서 데이터를 불러오고 저장할지 선택하는 부분입니다. '시키라' 같은 경우에는 Room을 활용한 캐싱을 통해 자원 소모를 줄이려고 노력하고 있습니다. 이 같은 로직을 Repository에서 담당해줍니다. 만약 로컬 자원을 사용하지 않거나, 혹은 서버를 사용하지 않는다면, Repository 패턴은 고려해볼 대상일 것입니다.

이렇게 관심을 적절하게 분리했다면 더욱 간결하고 명확한 테스트 코드를 작성할 수 있습니다. 캐싱, 도메인 로직, ViewModel 모두 따로따로 테스팅할 수 있어 테스트 코드를 작성하는 데 비용이 더 적습니다.

맺음말

이번 아키텍처에서 핵심은 '교체 가능함'에 있습니다. 교체가 쉽다는 것은 유연하고, 테스트 가능하고, 관심의 분리가 철저하다는 것이 아닐까요? 다시 한번 강조하는 부분이지만, 상황에 따라 아키텍처는 다르게 작용할 수 있습니다. 아키텍처는 처음부터 완벽하게 설계할 수 없다고 생각합니다. 구현 상황에서 어떤 이슈가 발생할지 모두 예측할 수 없기 때문이죠. 하지만 점진적으로 위에서 말했던 내용을 생각하면서 프로젝트에 맞게 설계한다면, 최선의 아키텍처에 도달할 수 있을 것입니다.

--

--