본문 바로가기
안드로이드

[안드로이드] 애플리케이션 아키텍처

by algosketch 2022. 1. 23.

안드로이드 시스템같은 저수준 아키텍처가 아닌 앱 개발 수준의 아키텍처에 관한 글이다.

 

안드로이드를 시작하는 사람의 프로젝트 구조

안드로이드는 처음 배우고 애플리케이션을 만들면 많은 사람들이 Activity 와 layout 을 담은 xml 파일만을 이용해 코드를 작성한다. 물론 다른 개발 경험이 많다면 그러지 않을 수도 있다. 어쨋든 이렇게 코드를 작성하다 보면 Activity 코드가 걷잡을 수 없이 커진다.

 

ViewModel 의 분리

애플리케이션에서 중요한 것은 무엇일까? 바로 비즈니스 로직이다. 비즈니스 로직은 UI 와는 전혀 관련이 없다. Todo list 애플리케이션으로 예를 들어보자. "할 일을 저장한다." 와 같은 비즈니스 로직에 UI 와 관련된 언급이 있는가? 전혀 없다. View(GUI) 는 무엇일까? 사실 View 는 보잘 것 없다. View 는 단순히 애플리케이션이 갖고 있는 데이터를 사용자가 보기 편한 방식으로 화면에 뿌려줄 뿐이다. 여기서는 안드로이드를 통해 보여주지만, 단순히 텍스트로 보여주거나 웹으로 보여줄 수도 있다. 또한 변동성이 높다. 그에 비해 비즈니스 로직은 상대적으로 안정적이다. 웹으로 보여주더라도 Todo list 의 비즈니스 로직은 변하지 않는다. 중요한 건 데이터와 이 데이터를 관리하는 비즈니스 로직이다.

여기서 우리는 View 를 기준으로 경계를 만들 수 있다. UI 와 관련된 코드는 Activity 에 그대로 두고 나머지는 ViewModel 로 옮기는 것이다. 여기서 변경되기 쉬운 View(Activity) 가 더 저수준이다. 의존 관계는 항상 저수준이 고수준을 의존해야 한다. 따라서 View 에서 ViewModel 을 의존한다. 만약 반대가 될 경우 View 가 바뀔 때마다 ViewModel 을 신경써 주어야 하는데, View 는 자주 바뀌고 전체 코드도 자주 바뀌게 된다. 특히나 안드로이드에서는 메모리 누수까지 발생한다. (A 가 B 를 의존한다는 것은 B 의 변화에 A 가 영향을 받는다는 뜻이다. 필드로 갖고 있거나 상속 받는 관계를 말한다.)

결과적으로 ViewModel 에서는 데이터와 이 데이터를 변경할 수 있는 코드를 갖는다. xml 에서는 데이터 바인딩을 통해 ViewModel 에 있는 데이터를 구독한다. ViewModel 에 있는 데이터가 변경되면 화면에 출력되는 내용도 바뀐다. 이렇게 코드를 분리하는 시도를 처음 해봤다면 변수 스코프가 달라져 당황한다. Activity 에서는 이용 가능했던 변수와 메소드를 사용할 수 없게 되어 어려움을 겪는다. 이 어려움을 해결하는 방법은 이 글을 참고하면 된다.

 

Data Layer 분리

그 다음으로 분리해야하는 것은 Data 레이어이다. 어떤 데이터를 다룰 것인가? 어디서 데이터를 가져올 것인가? 무엇을 가져올 것인가? 와 같은 처리를 담당하는 계층이다. 프로젝트에서 흔히 Repository, DataSource, model 과 같은 이름으로 존재한다. 여기서 model 은 data model 로 Retrofit 의 Entity 와 동일하다.

Repository 는 어떤 데이터를 가져올지 결정하고, DataSource 는 어디서 데이터를 가져올지 결정한다. Repository 에서 getTodoList 와 같은 메소드가 있다면 DataSource 는 LocalDataSource 와 RemoteDataSource 등이 있다. Repository 에서는 상황에 맞게 적절한 DataSource 를 선택하여 사용한다. 캐싱 등을 이용할 경우 하나의 Repository 에서 두 개이상의 DataSource 를 이용하는 것도 흔한 일이다.

이렇게 두 가지 계층에 대해 알아보았고 가장 기본적인 계층이다. 여기서 말한 게 Data 계층이고 먼저 말했던 게 UI 계층이다. 더 정확히는 UI + domain 계층이라 불러야 할 것이다. 다시 말하면 도메인 계층을 분리할 수 있다.

 

Domain Layout 분리

아키텍처 공식 가이드 문서에서는 이 계층은 선택(optional)이라고 한다. 필요하지 않다면 굳이 넣지 않아도 된다. 이렇게 계층으로 나누는 것도 비용이 들어가기 때문이다. 만약 이 계층이 없다면 여기에 들어갈 코드는 ViewModel 에 들어가게 된다.

도메인 레이어는 비즈니스 로직을 담당하는 중요한 계층이다. 안드로이드에서 비즈니스 로직은 UseCase 라고 부른다. 작명 규칙은 동사+명사+UseCase 이다. GetMemoUseCase 와 같은 이름을 사용할 수 있고, invoke operator 를 이용하여 구현하면 ViewModel 에서 함수처럼 사용할 수 있다. UseCase 는 주로 Entity 를 반환한다.

UseCase 는 비즈니스 로직을 담당하기 때문에 신입 개발자가 오더라도 UseCase 만 보면 이 앱이 어떤 앱인지 파악할 수 있다.

일반적으로 위와 같이 세 가지 계층을 갖는다. 일반적으로 세 개일 뿐이지 반드시 세 개일 필요는 없다.

위 아키텍처는 흔히 볼 수 있는 구조이다. 그런데 이 아키텍처에 대해서 몇 가지 의문점 혹은 고민해 봐야 하는 점이 있다. 아키텍처적로는 도메인 계층이 더 고수준이어야 한다. 데이터베이스나 서버와 통신하는 부분은 외부이다. 실제로 외부와 통신하기도 하고 언제 바뀌어도 이상하지 않다. 따라서 고수준인 도메인 계층은 독립적이어야 아키텍처상 더 좋다는 게 내 의견이다. 위 그림에서는 데이터 레이어로 가는 화살표가 아래가 아닌 위로 가야 더 적절하다. 의존성은 interface 를 통해 쉽게 역전시킬 수 있다. Repository 의 interface 를 도메인 계층에서 만들고 데이터 레이어에서 구현하면 된다.

여기서 한 가지 문제가 있는데, 데이터 계층에서 사용하는 데이터가 Entity 라면 이것은 도메인 계층보다 고수준이라는 점이다. 가장 고수준인 Entity 와 저수준인 데이터베이스가 같은 계층에 존재하는 것이다. 그래서 아키텍처적으로 봤을 때는 Entity 는 도메인 계층에서 정의하고 데이터 계층에서 사용하는 데이터 모델은 단순히 데이터를 운반하는 용도로만 사용하는 편이 좋을 것 같다.

하지만 이렇게까지 분리시키는 것에는 간단한 애플리케이션에도 너무 많은 코드를 생산하게 된다. 오버엔지니어링이 되지 않도로 해야하고 그 기준은 시간이 지남에 따라 바뀔 수도 있다.

 

DI (의존성 주입)

각각의 계층은 Dagger, Hilt, Koin 과 같은 DI 프레임워크를 통해 의존관계를 주입시킬 수 있다. 물론 간단한 의존성 주입은 수동으로도 쉽게 할 수 있다.

 

이 아키텍처의 문제점

프로젝트에 관계 없이 모든 애플리케이션의 패키지 구조가 같다. 패키지 구조만 보고는 어떤 앱인지 파악할 수 없다. (스크리밍 아키텍처가 아니다.) 이 문제는 도메인 계층을 통해 쉽게 확인할 수 있으므로 큰 문제가 될 것 같지 않다.

각각의 계층은 동일한 패키지 레벨을 갖는다. 그래서 이런 아키텍처를 수평적 구조를 갖는다고 표현한다. 수평적 아키텍처는 언어의 캡슐화 기능을 포기한다. ViewModel 에서는 반드시 UseCase 를 통해서 데이터를 가져와야 하는데, ViewModel 에서 직접 Repository 를 사용해도 막을 수 있는 방법이 없다. 또한 DI 프레임워크를 이용하려면 private 을 사용할 수 없다.

추가로 클린 아키텍처에서는 자바의 리플렉션을 이용하는 DI 프레임워크와 같은 것을 꼼수라 표현하며 지양해야 할 것이라 한다. 프레임워크는 저수준인 세부사항인데, 리플렉션을 이용하면 애플리케이션이 저수준인 프레임워크에 의존하게 되기 때문이다. 하지만 많은 안드로이드 프로젝트에서 이 구조를 사용하고 있다. 클린 아키텍처라는 책이 절대적인 책은 아니기 때문에 이 문제에 대해 더 깊은 고민이 필요할 것 같다.

수직적인 아키텍처도 물론 있다. 올해 안으로 도메인 주도 설계 책을 읽고, 안드로이드에서 수직적 구조로 만들어진 오픈 소스를 찾아볼 예정이다.