[안드로이드 공식문서 파헤치기] 클린 아키텍쳐의 모든 것!

dada·2022년 7월 22일
5
post-thumbnail

✅공부배경

  • 제가 지금까지 했던 모든 프로젝트 아키텍쳐 패턴은 MVVM 패턴으로 설계되어있습니다. 하지만 정작 Android developer에서 제공하는 앱 아키텍쳐 가이드를 명확히 숙지하고 있나?라는 의문이 들었습니다. UI Layer / Data Layer / Domain Layer 의 역할을 알고 패키징을 할 수 있으며 ViewModel과 Databinding 등 Android Jetpack에서 제공하는 Architecture 컴포넌트 라이브러리를 사용했지만 정작 '안드로이드에서 제공하는 앱 아키텍쳐 가이드에 대해 설명해봐~'라고 하면 자신없게 대답하는 저는 발견했습니다😥

  • 그래서 해당 포스팅을 통해 깊게 공부하고자 하는 내용은,

      1. mvvm 패턴에 따라 패키징하는 법 등 같이 실질적인 구현방법이 아닌 정확한 '정의'와 '단어'에 집중할 것입니다(특히 사용되는 단어를 잘 정리하려 합니다 ex. UI State 등)
      1. UI Layer / Data Layer / Domain Layer 의 역할과 구성요소 '개념'을 자세히 파고들 것입니다
  • 본 포스팅을 보고 공부하고자 하는 분이라면, 아예 MVVM 패턴 사용해본 적 없는 분보다 사용한 경험은 있지만 저처럼 개념을 깊이 파고들어보고 싶은 분이 읽으시면 더 도움될 것 같습니다!

✅용어 정리

  • 혼동되는 용어부터 정리해보겠습니다!

  • MVVM 아키텍처 인가? 패턴인가?

    • 아키텍처란?
      시스템 구성과 동작 원리 등 최상의 소프트웨어를 구성하는 설계도

    • 디자인 패턴이란?
      좋은 코드를 설계하기 위한 방법론

  • 결론적으로 MVVM은 "아키텍처 패턴" 이 정확한 표현이라고 할 수 있습니다!

  • 그리고 안드로이드에서 자주 사용되는 '아키텍쳐 패턴'에는 MVC패턴,MVP패턴,MVVM패턴이 있습니다!

  • 안드로이드 공식문서에서 설명하는 '권장하는 아키텍쳐란' MVVM과 같은 아키텍쳐 패턴을 사용하면서, 프로젝트에서 계층을 나눠 관심사를 분리시킨 '설계구조'라고 할 수 있습니다.

  • 위 사진은 안드로이드권장 아키텍쳐+MVVM패턴을 함께 사용한 모습이라고 봐도 좋습니다.

✅안드로이드가 권장하는 아키텍쳐를 구현하기 위한 사전지식

  • 안드로이드 아키텍쳐 공식문서를 살펴보기 전, Android Jetpack 이야기를 잠깐 해보겠습니다
  • 안드로이드에서 제공하는 라이브러리 모음집인 Android Jetpack을 이루는 4가지 컴포넌트 중 Architecture 컴포넌트로 분류되어있는 라이브러리는 아래와 같습니다

    Data Binding: xml파일에 Data를 연결해서 사용할 수 있게 도와준다
    Lifecycles: 안드로이드 activity 생명주기 관련 유틸리티
    LiveData: 데이터가 변경될때 실시간으로 view에 알려준다
    Navigation: activity, fragment간 이동을 쉽게 도와준다
    Paging: 대량의 데이트를 관리해주는 유틸리티
    Room: Database 보다 쉽게 사용할 수 있게 도와준다
    WorkManager: 백그라운드 작업을 보다 쉽게 도와준다

  • 이런 Architecture 컴포넌트 라이브러리들을 사용하면 구글이 권장하는 안드로이드 아키틱쳐를 자신의 앱 프로젝트에 적절히 적용할 수 있습니다

✅모바일 앱 사용자 환경?

  • Android Developer 도큐먼트 - 앱 아키텍처 가이드에서는 아키텍쳐를 설명하는 가장 첫문단으로 '모바일 앱 사용자 환경'이라는 개념에 대해 소개하고 있습니다

  • 일반적인 안드로이드 앱은 Activity/Fragment, Service, Content Provider, Broadcast Receiver 와같은 4대 컴포넌트로 구성되어 있습니다. 개발자는 이러한 앱 구성요소 대부분을 Manifest파일에 선언하고 Android OS가 이 manifest 파일을 사용하여 기기를 사용하는 사용자의 전반적인 작업 환경을 망가뜨리지 않으면서 앱을 이 환경에 자연스럽게 통합하는 방법을 결정합니다

  • 여기서 말하는 사용자의 전반적인 작업 환경(모바일 앱 사용자 환경) 이란 무엇을 의미하는 것일까요?

  • 안드로이드 앱을 사용하는 유저는 짧은 시간 내에 여러 앱을 실행하는 경우가 많습니다 따라서 앱이 사용자 중심의 다양한 workflow에 맞게 조정될 수 있어야 합니다.

  • 예를 들어 사용자가 SNS앱에서 사진을 업로드 하는 과정을 생각해보면 SNS앱은 가장 먼저 카메라 인텐트를 트리거 할 것입니다. 그러면 Android OS에서 요청에 따라 카메라 앱을 실행시킵니다. 이 순간 사용자는 카메라 앱으로 이동하게 되면서 SNS 앱에서는 나간 상황이 되지만 사용자의 작업 환경은 끊김없이 연결되어 있는 상태라고 볼 수 있습니다. 그 다음, 카메라 앱에서 앨범을 보기 위해 앨범 인턴트를 트리거할 수 있습니다. 이 후에는 사용자가 다시 SNS앱으로 돌아가서 앨범에서 선택한 사진이나 카메라 앱으로 직접 찍은 사진을 업로드 할 것입니다. 심지어 이때 전화나 알림에 의해 사용 환경의 흐름이 끊어질 수도 있습니다

  • 즉 사용자는 이러한 흐름 중단과 상관없이 다시 SNS앱으로 돌아가 하던 작업을 계속 하길 원할겁니다

  • 이처럼 사용자는 모바일 환경에서 다양한 앱을 시도때도 없이 바꾸기도 하고 전화나 알림 등의 작업도 동시에 하기 때문에 앱에서 이런 사용자 흐름이 중단되지 않고 자연스럽게 흘러가도록 올바르게 처리해야 합니다

  • 이 뿐만 아니라 모바일 기기는 하드웨어 자원(메모리 등)이 제한되어 있으므로 메모리 부족으로 인한 사용자 흐름 중단이 발생할 수도 있습니다

  • 이에 대비해 안드로이드 OS는 메모리 공간이 부족할 경우 '일시정지됨'상태에 있는 앱의 프로세스를 강제 종료해서 사용자 작업 흐름이 끊기지 않게 해야합니다

  • 이러한 모바일 환경 조건을 고려해볼때 App Component(앱 구성요소)는 개별적이고 비순차적으로 실행될 수 있으며 Android OS나 사용자가 언제든지 앱 구성요소를 제거할 수 있다는 것을 알 수 있습니다

  • 하지만 개발자는 이러한 이벤트(앱 구성요소가 개별적, 비순차적으로 실행 / OS에 의한 앱 구성요소 제거)를 직접 제어할 수 없습니다😂

  • 따라서 이러한 이벤트가 발생할 것에 대비해 앱 구성요소에 앱 데이터나 상태를 저장해서는 안되며 앱 구성요소가 서로 종속되도록 개발해선 안됩니다

  • 앱 구성요소에 앱 데이터나 상태를 저장할 경우 앱의 강제종료나 예상치 못한 전화, 알림 등이 왔을 경우 앱 구성요소가 저장하고 있는 데이터는 유실될 수 있기 때문입니다

  • 또한 앱 구성요소가 서로 종속된다면 포그라운드에서 동작하는 Activity를 종료했을 경우 백그라운드에서 동작하는 Service도 함께 종료되므로 앱 구성요소는 서로 독립적으로 작동해야합니다

✅Android Clean Architecture 두 가지 원칙

  • 위 내용을 통해 '앱 구성요소에는 앱 데이터와 상태를 저장해선 안된다'라는 걸 알 수 있었습니다

  • 그렇다면 앱 데이터와 상태는 어디에 저장해야 앱을 잘 설계(클린 아키텍쳐 구현)했다고 소문이 날까요?🤔

  • 일단 앱을 설계하는데 필수적인 원칙들을 살펴 본후 위 질문에 대한 답을 알아봅시다


✔ 앱 설계 필수원칙1- "UI 기반 클래스를 가볍게 하라"

  • 이 원칙은 안드로이드 앱 아키텍쳐 원칙 중 가장 중요한 원칙입니다.

  • 안드로이드 초기 개발자들은 Activity나 Fragment에 모든 코드를 작성하는 '실수'를 범하기도 했습니다

  • Activity나 Fragment 클래스를 UI 기반 클래스 라고 하는데 원칙적으로 UI 기반 클래스는 UI를 핸들링하거나 안드로이드 OS와 앱의 상호작용을 처리하는 로직만 포함해야 합니다

  • UI 기반 클래스를 최대한 가볍게 유지해야 많은 수명 주기 관련 문제를 피할 수 있을 것입니다

  • Activity와 Fragment 클래스는 Android 운영체제와 Application(앱) 사이의 계약을 나타내도록 이어주는 클래스일 뿐입니다.

  • 즉, 여러 수명 주기를 제어함으로써 사용자가 어떤 동작을 하더라도 그 흐름이 끊어지지 않도록 해주는 것이 UI 기반 클래스 라고 생각하면 됩니다

  • 따라서 UI 기반 클래스에서는 수명 주기에 따라 발생할 수 있는 상황에 대처하는 코드만 작성하는 것이 좋습니다


✔ 앱 설계 필수원칙2- "UI와 Model(모델)을 분리하라"

  • 안드로이드 앱 아키텍처 원칙 중 두 번째로 중요한 원칙은 모델과 UI를 분리해야 한다는 것입니다

  • 이 말의 의미는 UI 기반 클래스에 데이터나 상태를 저장하지 말라 는 말과 같습니다

  • 모델은 앱의 데이터를 담당하는 구성요소 로, 앱의 View 객체 및 앱 구성요소와 독립되어 있는 존재여야 합니다

  • 즉, 모델을 UI와 분리하여 앱의 수명 주기나 여러 사용자 환경 흐름에 영향을 받지 않도록 해야한다는 것입니다

✅권장하는 Android Clean Architecture

  • 앱 설계 필수원칙 2개를 잘 지키면서 + 앱 구성요소가 아닌 다른 곳에 앱 데이터와 상태를 저장하면서 개발자는 어떻게 앱을 구조화 해야 할까요? 에 대답으로 구글에서는 아래와 같은 앱 아키텍쳐 다이어그램을 제공합니다
  • 해당 그림에서 Layer라고 되어있는 용어에 집중해 설명해 보자면 아래와 같으며 안드로이드에서 권장하는 앱 아키텍쳐는 최소 2개 이상의 Layer로 구성되어야 합니다
    • UI Layer: 화면에 애플리케이션 데이터를 표시하는 UI 레이어
    • Data Layer: 앱의 비즈니스 로직을 포함하고 애플리케이션 데이터를 노출
    • Domain Layer: UI와 데이터 레이어 간의 상호작용을 간소화하고 재사용하기 위함
  • 각 레이어는 화살표 방향대로 UI Layer->Domain Layer->Data Layer 순대로 의존성이 높습니다

1. UI Layer

  • 본격적으로 Android Developer 도큐먼트-UI Layer부터 분석해보겠습니다

  • 해당 문서에 나오는 용어들을 살짝 정리하고 가겠습니다

    UI : UI element(버튼) + UI State를 결합한 것 ex)로그인 버튼 활성화 되어있는 화면
    UI 컨트롤러(Fragment/Activity) : UI에 View를 그리거나 사용자 event를 trigger하는 등 UI관련 동작을 캡쳐함
    UI 데이터 : 사용자 눈에 보여야 하는 데이터
    stateHolders(ViewModel) : UI State를 생성하는 역할을 하며 생성 작업에 필요한 로직을 포함하는 클래스
    UI element : 버튼같은 컴포넌트


(사진 1)

(사진 2)

  • UI레이어에는 UI관련 상태 및 UI로직이 포함됩니다
  • UI Layer는 사진1에서 보이는 것처럼 각 레이어중 가장 다른 레이어에 대한 의존도가 높은 레이어입니다.
  • UI Layer는 사진 2에서 보이는 것처럼 ViewModel + UI element로 구성되어 있습니다
  • 공식 문서에서 권장하는 UI element 구성요소는 Jetpack Compose입니다. Compose는 선언형 함수를 사용하여 간단한 UI element를 빌드할 수 있기 때문입니다.

2. Data Layer

  • Data Layer에는 애플리케이션 데이터 및 비즈니스 로직이 포함됩니다. (UI레이어에는 UI관련 상태 및 UI로직이 포함됩니다)
  • 비즈니스 로직은 앱에 가치를 부여하는 요소로, 애플리케이션의 데이터 생성, 저장, 변경 방식을 결정하는 실제 비즈니스 규칙으로 구성됩니다
  • 이렇게 관심사를 분리하면 데이터 영역을 여러 화면에서 사용하고 앱의 여러 부분 간에 정보를 공유하고 단위 테스트를 위해 UI외부에서 비즈니스 로직을 재현할 수 있습니다.
class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) {

    val data: Flow<Example> = ...

    //데이터를 가공하는 비즈니스 로직이 ViewModel에 있는게 아니었구나!
    suspend fun modifyData(example: Example) { ... }
}
  • Data Layer는 여러개의 DataSource로 추상화되어있는 Repository로 구성됩니다. 앱에서 처리하는 다양한 유형의 데이터 별로 ~Repository라는 이름의 클래스를 만들어야 합니다

    • 예를 들어 영화 관련 데이터에는 MovieRepository 라는 클래스를 만들 수 있습니다
  • Repository 클래스에서 담당하는 작업은 아래와 같습니다

    • 앱의 나머지 부분에 데이터 노출
    • 데이터 변경사항을 한 곳에 집중
    • 여러 데이터 소스 간의 충돌 해결
    • 앱의 나머지 부분에서 데이터 소스 추상화
    • 비즈니스 로직 포함
  • Domain Layer나 UI Layer는 데이터 소스에 직접 액세스 하면 안됩니다. 데이터 영역의 진입점은 항상 Repository클래스여야 합니다. UI Layer의 StateHolder(ViewModel) 같은 곳에 데이터 소스가 직접 종속 항목으로 있어서는 안됩니다(data class 등으로 만든 model 객체 생성X)

  • Repository 클래스를 데이터의 진입점으로 사용하면 아키텍쳐의 다양한 레이어를 독립적으로 확장할 수 이씁니다.

  • Data Layer의 Repository는 Repository클래스가 담당하는 데이터의 이름+Repository 라고 이름지어야 합니다

    • ex)NewsRepository, PaymentRepository
  • 위 사진처럼 Repository클래스(저장소)에 더 복잡한 비즈니스 요구사항이 포함된 경우엔 저장소가 다른 저장소에 종속될 수 있습니다. 관련된 데이터가 여러 데이터 소스의 집계이거나 책임이 다른 저장소 클래스에 캡슐화되어야 하기 때문입니다.

    • 예를 들어 사용자 인증 데이터를 처리하는 저장소인 UserRepository는 요구사항을 충족하기 위해 LoginRepository 및 RegistrationRepository와 같은 다른 저장소에 종속될 수 있습니다.
  • 비즈니스 모델

    • 데이터 영역에서 노출하려는 데이터 모델은 다양한 데이터 소스에서 가져오는 정보의 하위 집합일 수 있습니다. 네트워크 및 로컬의 다양한 데이터 소스가 애플리케이션에 필요한 정보만 반환하는 것이 좋으나 실제 이런 경우는 많지 않습니다.

    • 예를 들어 기사 정보뿐만 아니라 수정 기록, 사용자 댓글, 일부 메타데이터도 반환하는 News API 서버가 있다고 합시다.

data class ArticleApiModel(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val modifications: Array<ArticleApiModel>,
    val comments: Array<CommentApiModel>,
    val lastModificationDate: Date,
    val authorId: Long,
    val authorName: String,
    val authorDateOfBirth: Date,
    val readTimeMin: Int
)
  • 화면에 기사 콘텐츠와 작성자에 관한 기본 정보만 표시하므로 앱은 기사에 관한 많은 정보를 필요로 하지 않습니다. 모델 클래스를 분리하고 저장소에서 계층 구조의 다른 레이어에 필요한 데이터만 노출하도록 하는 것이 좋습니다. 예를 들어 다음은 Article 모델 클래스를 도메인 및 UI 레이어에 노출하기 위해 네트워크에서 ArticleApiModel을 다듬는 방법입니다.
data class Article(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val authorName: String,
    val readTimeMin: Int
)
  • 모델 클래스를 분리하면 다음과 같은 이점이 있습니다.
    • 필요한 수준으로 데이터를 줄여 앱 메모리를 절약합니다.
  • 앱에서 사용하는 데이터 유형에 맞게 외부 데이터 유형을 조정합니다. 예를 들어 앱은 날짜를 나타내는 데 다른 데이터 유형을 사용할 수 있습니다.
  • 이를 통해 관심사를 더 잘 분리할 수 있습니다. 예를 들어 모델 클래스가 미리 정의된 경우 대규모 팀원이 기능의 네트워크 레이어와 UI 레이어에서 개별적으로 작업할 수 있습니다.
  • 이 방식을 확장하고 앱 아키텍처의 다른 부분(예: 데이터 소스 클래스 및 ViewModel)에서도 별도의 모델 클래스를 정의할 수 있습니다. 그러나 이를 위해서는 적절하게 문서화하고 테스트해야 하는 추가 클래스 및 로직을 정의해야 합니다. 최소한 데이터 소스가 앱의 나머지 부분에서 예상하는 데이터와 일치하지 않는 데이터를 수신하는 경우에는 새 모델을 만드는 것이 좋습니다.

3. Domain Layer

class FormatDateUseCase(userRepository: UserRepository) {
    //복잡한 비즈니스 로직 처리
    private val formatter = SimpleDateFormat(
        userRepository.getPreferredDateFormat(),
        userRepository.getPreferredLocale()
    )

    operator fun invoke(date: Date): String {
        return formatter.format(date)
    }
}
  • Domain Layer는 UI 레이어와 데이터 레이어 사이에 있는 선택적 레이어입니다.
  • 도메인 레이어는 복잡한 비즈니스 로직이나 여러 ViewModel에서 재사용되는 간단한 비즈니스 로직의 캡슐화를 담당합니다. 모든 앱에 이러한 요구사항이 있는 것은 아니므로 이 레이어는 선택사항입니다. 따라서 복잡성을 처리하거나 재사용성을 선호하는 등 필요한 경우에만 도메인 레이어를 사용해야 합니다.
  • 도메인 레이어의 이점
    • 코드 중복을 방지합니다.
    • 도메인 레이어 클래스를 사용하는 클래스의 가독성을 개선합니다.
    • 앱의 테스트 가능성을 높입니다.
    • 책임을 분할하여 대형 클래스를 방지합니다.
  • 이 가이드의 이름 지정 규칙 현재 시제의 동사 + 명사/대상(선택사항) + UseCase
    • 예를 들면 FormatDateUseCase, LogOutUserUseCase, GetLatestNewsWithAuthorsUseCase, MakeLoginRequestUseCase가 있습니다.(Data Layer의 Repository 이름 규칙은 Repository클래스가 담당하는 데이터의 이름+Repository 이었습니다!)
class GetLatestNewsWithAuthorsUseCase(
  private val newsRepository: NewsRepository,
  private val authorsRepository: AuthorsRepository
) { /* ... */ }
  • 일반적인 앱 아키텍처에서 사용 사례 클래스는 UI 레이어의 ViewModel과 데이터 레이어의 저장소 사이에 적합합니다. 즉, 사용 사례 클래스는 일반적으로 저장소 클래스(Repository 클래스)에 종속되며, 저장소와 동일한 방법으로 콜백(자바의 경우) 또는 코루틴(Kotlin의 경우)을 사용하여 UI 레이어와 통신합니다.
profile
'왜?'라는 물음을 해결하며 마지막 개념까지 공부합니다✍

0개의 댓글