Android Clean architecture

강성우, 17 September 2020

그동안 안드로이드 아키텍쳐에 클린 아키텍쳐를 적용하면서 정리했어야 하는 내용들을 정리 하였다.

1. Clean architecture란 무엇인가

Clean architecture는 Robert C.Martin(Uncle Bob)의 아키텍쳐의 개념을 정리한 내용이다.

시스템 아키텍쳐에서는 많은 아이디어들을 보여왔었다. 그 중에는 Hexagonal ArchitectureOnion Architecture, Screaming Architecture등 많은 좋은 아이디어들이 알려져 있다. 이러한 아키텍처들은 세부사항이 조금씩 다르지만 목표로 하는 바는 비슷하다. 이 아키텍처들은 모두 동일한 목표인 “관심사 분리”를 갖고 있다.

이러한 아키텍처들은 소프트웨어 내부를 각 계층으로 나누어 계층간을 완전히 분리한다. 각각에는 비즈니스 로직에 대한 계층과 인터페이스에 대한 계층이 하나 이상씩 존재 하고 있다. 이들은 아래와 같은 시스템을 만들게 된다.

  1. 독립적인 프레임 워크. 아키텍처는 다른 기능이 포함 된 소프트웨어 라이브러리의 존재에 의존을 갖지 않는다. 이를 통해 제한된 제약들에 시스템을 밀어넣을 필요 없이 단순한 프레임워크를 도구로서 사용 할 수 있게 해준다.

  2. 테스트의 용이함. 비즈니스 로직, UI, 데이터베이스, 웹 서버 또는 기타 외부의 요소에 상관 없이 테스트 할 수 있다.

  3. UI 독립적. UI는 시스템의 나머지 부분을 변경하지 않고도 쉽게 변경할 수 있다. 예를 들어 비즈니스 로직을 변경하지 않고도 웹 UI를 콘솔UI로 변경할 수 있는것 과 동일하다.

  4. 데이터베이스 독립적. 데이터베이스를 다른 종류의 데이터베이스로 바꿀 수 있다. 비즈니스 로직은 데이터베이스에 바인딩 되지 않는다.

  5. 외부의 것 들과 독립적. 비즈니스로직은 외부에 대해서 전혀 알지 못한다.

아래의 다이어그램는 이러한 모든 아키텍처를 통합한 형태로 보여주고 있다.

clean-architecture img

위 그림에서 보여주고자 하는 바는 각 목적에 맞추어 정리된 냉용을 계층(Layer)로 나누어 의존을 정리 한 것 이다. 원들의 중심으로 갈 수록 높은, 바깥으로 갈 수록 낮은 수준의 컴포넌트로서 이들에 대한 계층의 분리를 통해 효율높은 설계가 가능하다는 것을 설명하고자 하는 것 이다.

추가적으로 클린 아키텍처를 이해하기 전에 앞서 SOLID(객체 지향 설계)에 대해서 자세히 알아두는게 좋다. SOLID(객체 지향 설계) 5원리는 아래와 같다. (참고로 SOLID는 각 5개원소의 영문 앞한글자씩을 나열한 것 이다)

  1. 단일 책임의 원칙 (Single Responsibility Principle - SRP)
    • 클래스 및 함수는 단 하나의 책임만 가져야 한다. 객체지향적으로 응집도를 높게, 결합도는 낮게 설계 하는 것 이좋다.
    • SRP 에 따르면 클래스, 함수에서는 맡은 책임(기능)에 대해서 많은 요구를 가질필요가 없다. 많은 응집도를 갖게 되면 결합도 또한 높아진다. 이는 유지보수에 악영향을 끼치므로 책음을 세세하게 분리하는게 좋다.
  2. 개방-폐쇄 원칙 (Open-Closed Principle - OCP)
    • 클래스는 확장에는 열려(Open)있으나 변경에는 닫혀(Closed)있어야 한다.
    • 자주 변경되는 기능에 대해선 수정하기 쉽게 설계 하고, 변경되지 않는 내용은 추후 수정될 것 으로 예상될 내용에 영향을 받지 않게 해야 한다. 그렇기 때문에 Interface를 사용 하게 된다.
  3. 리스코프 치환 원칙 (Liskov Subsitution Principle)
    • 자식 클래스는 부모클래스에서 가능한 행위를 수행할 수 있어야 한다.
    • 부모클래스와 자식클래스간 행위에 일관성이 있어야 한다는 내용이며, 부모클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용해도 문제가 없어야 한다는 이야기 이다. (IS-A 관계)
  4. 인터페이스 분리 원칙 (Interface Segregation Principle)
    • 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다. 하나의 일반적인 인터페이스 보다는, 여러개의 구체적인 인터페이스가 낫다.
    • 말그대로 사용되지 않을 기능에 대해서 영향받는 클래스, 함수를 만들면 안된다. 이럴 경우 하나의 인터페이스에 요구사항 외의 기능을 한꺼번에 추가하는 것 보다는, 인터페이스들을 잘게 분리 하여 서로에게 영향을 받지 않도록 설계 해야 한다.
  5. 의존 역전 원칙 (Dependency Inversion Principle - DIP)
    • 의존 관계를 맺을 때, 변화하기 쉬운것 보단 변화하기 어려운 것에 의존 해야 한다.
    • 변화하기 쉬운것 은 구체적인 것(구체화된 클래스)을 말하고, 변화하기 어려운 것 은 추상적인 것(인터페이스, 추상 클래스) 을 말한다. 의존성 주입(Dependency injection)에서 요구되는 원칙이다.

1.1 소프트웨어에 아키텍처를 사용하지 않는다면 어떻게 될까?

그동안 이야기한 클린 아키텍처, 아니 소프트웨어에 아키텍처가 존재 하지 않거나 잘못된 아키텍처를 사용중이라면 어떠한 일들이 발생할까? 과거 대학교때와 첫 회사에서 개발했었던 프로젝트들이 생각났다. 당시 프로젝트들은 한결같이 안드로이드 플랫폼 위에서 개발했으며 어떠한 아키텍처도 정의하지 않고 빠르게 개발해야 하는 업무들이었었다. 당시에 경험했었던 문제들을 지금 시점에서 다시 정리해보면 아래와 같다.

  • 책임이 명확하지 않은 클래스, 메소드들이 난립하므로 코드를 봐도 명확하게 책임을 알 수 없어 버그와 같은 문제의 근원을 파악하기 어렵다.
  • 복잡한 클래스들간의 의존과 떨어지는 단일 (책임을 갖는)클래스 응집력으로 인하여 보일러 플레이트 코드가 발생 한다.
  • 구분없이 섞여버린 비즈니스 로직과 뷰의 코드들로 인해 단위 테스트 코드 작성이 사실상 어렵다.

한결같이 안좋은 코드의 문제들을 말해주고 있다. 뭐, 그렇다고 아키텍처를 적용한다고 해서 위의 문제들이 단번에 완전히 해결되는것은 아니다. 아키텍처를 도입하는 것도 좋지만 그에 걸맞는 코드 컨벤션과 개발자 룰이 필요하고 그를 잘 지켜나가야 하기 때문이다.

환경적인 요인도 있다. 아무리 아키텍처를 잘 설계하고 룰을 세워 하나하나 잘 지킨다고 하더라도 외부적인 요인으로 인해 강압적인 업무 프로세스로 룰이 깨져나가고 부족한 시간에 코드를 빨리 처리 하고 응급처리 해야 하므로 코드 부채와 더불어 문제로 가득한 코드들이 발생할 수도 있다.

그렇다면 아키텍처를 적용함으로서 얻는 이점은 무엇일까? 바로 위에 적은 단점을 대부분 해결할 수 있으니 그게 바로 이점이라고 생각 한다.

2. 안드로이드에서의 클린 아키텍처

안드로이드 앱에 클린 아키텍쳐를 적용하는데에 무리가 없기는 하지만 개발 환경이나 조건에 따라 조금씩 다르게 적용하는 것 으로 알고 있다. 구글에서 안드로이드 클린 아키텍처를 검색하기만 하더라도 개발자들마다 서로 다른 의견과 철학을 가진 아키텍처들과 각 구현방식이 서로 다름을 알 수 있다.

본인이 개발했던 앱 에서는 기본적으로 각 레이어를 “module”으로 나누어 확실하게 경계를 세워 벽을 만들어 서로간의 의존을 완전히 정리하려고 하였다. 단위 테스트를 조금 더 빠르고 단순하게 동작시키기 위해서 모듈을 나눈 이유도 있지만 개발자들이 할 수도 있는 실수(접근 하면 안되는 계층에 접근하여 의존이 발생하는 것)를 방지함도 있었다. 다만, 각 모듈간 gradle파일의 관리가 복잡해지거나 그냥 app모듈 내 에서 패키지로 나뉘어도 문제는 없긴 하다.

일반적으로 알려진 안드로이드에서의 클린 아키텍처를 구성하면 아래와 같이 계층을 구분한다. (이 계층들은 개발자, 문서마다 이름이 다 다르다. 하지만 목적하고자 하는 바인 특화된 관심사를 기반으로 계층을 나눈다는 개념은 모두 비슷하며 구현또한 그에 따라 다르긴 하다)

  • Entity : Data object
  • 비즈니스 로직 : Use-case
  • 인터페이스 어뎁터 : Presenter
  • 그 외 : 네트워크, 데이터 베이스 등

몰론 위 처럼 계층을 나누어도 상관없지만 일단 계층을 크게 3개로 나뉘어 아래처럼 구분하였다.

module_dep

위 이미지에서는 본인이 개발했던 앱의 계층의 이미지 이다. 각 모듈의 역할은 아래와 같다.

  • app : 안드로이드에 의존을 갖는 작업들을 수행한다.
    • 각 도메인별 Activity, Fragment, View등의 안드로이드에 의존을 갖는 클래스들이 있다.
    • 그리고 model의 인터페이스의 구현 클래스들이 있다.
    • 일반적으로 Presentation Layer라고도 부른다.
  • model : 각 도메인별 비즈니스 로직을 수행한다.
    • ViewModel의 구현. 인터페이스들.
    • 일반적으로 Data Layer라고도 부른다.
  • common : Constants와 같은 글로벌 상수와 각종 유틸리티성 확장함수들. string 리소스 xml.

필요에 따라 POJO와 같은 entity을 보유하는 모듈이 추가 되거나 다른 환경에 따라 라이브러리 모듈이 추가 될 수도 있다.

각 모듈들간의 의존은 아래와 같다.

  • app 모듈은 common, model모듈을 알고 있다.
  • model 모듈은 common모듈만 알고 있다.
  • common모듈은 다른 모듈을 전혀 모르고 있다.

위 아키텍처에서 달성하고자 했던 목표는 아래와 같다.

  1. 비즈니스 로직과 뷰처리 코드들의 완전한 분리.
  2. 유연한 의존성 주입을 위한 재사용가능한 컴포넌트들의 추상화.
  3. 최대한 단순하고 멍청한 ViewModel.
  4. 클래스들(ViewModel, Repository 외 인터페이스들)은 쉽고 빠르게 단위 테스트 가능.
  5. 그리고 개발할때 위의 정책을 어기지 않는다.

2.1 MVC, MVP, MVVM 아키텍처

클린 아키텍처와 더불어 MVC, MVP, MVVM등의 아키택처 패턴도 존재 한다. 개인적으로 Model과 View의 의존이 약한 MVVM패턴을 최근에 많이 사용 하고 있다. 아니, 사실상 MVVM만 사용하고 있다고 봐도 무방할거 같다. 추가적으로 MVI패턴도 사용해보았고, MVI가 영향을 받았었던 Redux도 적용해보았지만 기본적으로 MVVM기반의 ViewModel이 가장 좋은 경험을 갖고 있다.

MVC, MVP, MVVM 모두 각자 자기만의 장단점을 갖고 있으며 편한 패턴을 사용해도 무방하다고 생각 한다. 하지만, 만약 내가 프로젝트를 만들게 되고 아키텍처와 패턴을 구성할 기회나 발언을 할 수 있다면 클린 아키텍쳐를 기반으로 MVVM패턴을 적용 하는 것을 추천 하고 싶다.

3. 정리

본인이 개발했던 앱에서의 구현을 정리하면 아래와 같다.

  • app layer
    • 화면의 실체 출력, 사용자 입력 처리 등 UI와 관련된 코드
    • view sub layer : 안드로이드 플랫폼에 의존을 갖는 클래스, 함수 (각 도메인 별 Activity, Fragment, View), DI, 인터페이스의 구현 클래스
    • api sub layer : retrofit2을 인터페이스, model 계층에서 사용 하는 리포지터리 인터페이스의 비즈니스 로직이 아닌 네트워크 코드만 구현.
    • MVVM 패턴을 적용한 경우 ViewModel의 구현 (ViewModel클래스는 app 혹은 model계층에 있을 수 있다)
  • model layer
    • domain sub layer : 유즈케이스 비즈니스 로직의 구현. (네트워크, 로컬 등 데이터 소스 관리)
    • Repository 인터페이스 등을 비롯한 각종 인터페이스들.
      • 안드로이드 리소스에 직접적으로 접근할 수 없으므로 app계층에 접근 하기 위해서 인터페이스를 만들어 접근 한다.
    • 비즈니스 로직에서 사용되는 Entity들.
  • common layer
    • Constants와 같은 글로벌 상수들.
    • 각종 확장함수 및 유틸리티성 공통 함수.
    • 필요에 따라 문자열 리소스 xml을 관리.

크게 3개의 계층으로 나누고 각 큰 계층마다 계층을 다시 나눈 형태이며, 작게 나뉘어진 계층은 큰 계층 내에서 의존을 최대한 줄이는 코드의 형태로 개발 하였다.

아키텍처에는 정확한 정답이 없다고 생각 한다. 하지만 클린 아키텍처는 좋은 아키텍처를 구현하기 위한 좋은 기반이 되어준다고 확신 하고 있다. 각자 다른 개발 환경과 철학 아래에서 만들어지는 아키텍처가 서로 다를수도 있음을 인정 하고 더 좋은 코드를 만들기 위해 같이 고민하는게 가장 좋은 방향이라고 생각 하고 있다.