[Android] Clean Architecture in Android

강승구·2023년 10월 11일

Clean Architecture

클린 아키텍처는 소프트웨어 시스템의 구조를 설계할 때 지켜야 할 원칙과 방법을 정의한 개념으로로버트 C. 마틴(Robert C. Martin)에 의해 소개되었다. 클린 아키텍쳐는 복잡한 소프트웨어 시스템을 보다 관리 가능하고 유지보수 가능한 형태로 구축하기 위한 지침을 제공하고 소프트웨어의 유지보수성, 테스트 용이성 및 모듈 간의 분리를 강조하여 소프트웨어를 더 구조화된 방식으로 개발하기에 용이하다.


Clean Architechture in Android

즉, 클린 아키텍쳐는 계층을 나누어서 관심사를 분리하고, 각 분리된 클래스가 한가지 역할만 할 수 있도록 구현하는 방식이라고 정의할 수 있다.

안드로이드 프로젝트에서 클린 아키텍쳐를 적용하면 다음과 같은 장점들이 있다.

  • 유지보수 용이성
    각 계층이 분리되어 있기 때문에 한 계층을 변경해도 다른 계층에 영향을 미치지 않아 유지보수가 쉽다.
  • 테스트 용이성
    의존성을 주입하여 유닛 테스트 및 통합 테스트를 수행하기 용이하다.
  • 모듈 간의 분리
    각 계층이 자체 역할을 가지며, 이로 인해 코드의 재사용성이 높아진다.
  • 데이터베이스나 UI 프레임워크 변경 용이성
    중요한 비즈니스 로직은 외부 프레임워크와 분리되어 있어 해당 프레임워크를 변경하더라도 기존에 작성한 전체 코드를 다시 작성할 필요가 없다.

안드로이드에서의 클린아키텍쳐의 핵심 개념은 Presentation, Domain, Data 3계층 레이어를 기반으로 하고, 이들은 단방향 데이터 흐름(UpStream + DownStream)으로 통신한다는 것이다.


1. 단방향 데이터 흐름

단방향 흐름이란 데이터가 오로지 한 방향으로만 흐르는 것을 의미한다. 그리고 이러한 흐름에는 'Up Stream방식', 'Down Stream' 방식이 있다.

  • Up Stream : 사용자가 클릭 등의 이벤트를 발생시킴으로써 UI -> Domain -> Data로 전달함으로써 상위 레이어로 전달하는 방식
  • Down Stream : Remote or Local Server로부터 받은 데이터를 Data -> Domain -> UI로 전달함으로써 하위 레이어로 전달하는 방식

2. 3계층 레이어

2-1. Presentation Layer (UI Layer)

Data 계층과 Domain 계층에 의존성을 가지고 있는 레이어로 화면과 입력에 대한 처리 등 UI와 직접적으로 관련된 모든 것들이 UI 레이어에 포함된다.
UI 레이어에 포함되는 구성요소는 다음과 같다.

우선 UI elements란 Activity, Fragment, Dialog, TextView, Composable 함수 등 UI 요소 그 자체를 의미한다.

State Holder란 UI의 상태를 관리하고 UI에 바인딩되는 데이터를 보유하는 역할을 담당한다.
MVC 패턴에서의 Controller, MVP 패턴에서의 Presenter, MVVM 패턴에서의 ViewModel이 해당 역할을 수행하고 있다.

{
    shops:[
        {
            shopName:"모자이크된 이름", 
            shopThumbnail:"https://~~"
            score:"4.9", 
            reviewCount:"121", 
            couponType:"normal"
            merits:"[맛집검증+신속배달]+왕벌떡볶이세트", 
            minimumOrderPrice:14000, 
			deleveryType:"fix", 
            deleveryTip:2500,
            deleveryMinimumMinute:40,
            deleveryMaximumMinute:55,
        },
        {
            shopName:"모자이크된 이름", 
            shopThumbnail:"https://~~"
            score:"4.9", 
            reviewCount:"221", 
            couponType:"normal"
            merits:"꿀떡 시그니처, 가밀떡볶이", 
            minimumOrderPrice:8000,  
			deleveryType:"range",
            deleveryMinimumTip:1000,
            deleveryMaximumTip:4000,
            deleveryMinimumMinute:39,
            deleveryMaximumMinute:54,
        },
        {
            shopName:"모자이크된 이름", 
            shopThumbnail:"https://~~"
            score:"4.9", 
            reviewCount:"121", 
            couponType:"fast"
            merits:"옆집떡볶이, 2인떡볶이+치즈+비엔나+...", 
            minimumOrderPrice:9000, 
			deleveryType:"range",
            deleveryMinimumTip:0,
            deleveryMaximumTip:1500,
            deleveryMinimumMinute:33,
            deleveryMaximumMinute:48,
        },
        ...
    ]
}

예를 들어 어떤 화면에 표시되는 데이터가 다음과 같다고 하자.
안드로이드 앱에서는 위와 같은 데이터 구조들이 UI에 바인딩 됨으로써 화면을 보여주게 된다.

그리고 위의 데이터는 안드로이드에서 권장하고 있는 상태관리 홀더 클래스인 LiveData와 StateFlow로 다음과 같이 관리된다.

class ShopListViewModel: viewModel{
    private val _shopItem = MutableLiveData<List<ShopItem>>()
    val shopItem : LiveData<List<ShopItem>>
        get() = _shopItem
        
    fun ...() {}
    
}
class ShopListViewModel: viewModel{
    private val _shopItem = MutableStateFlow<List<ShopItem>>(emptyList())
    val shopItem = _shopItem.asStateFlow()
        
    fun ...() {}
    
}

위 예시에서는 ViewModel(State Holder)이 상태 홀더 클래스 (LiveData or StateFlow)를 통해 UI에 바인딩 해야하는 데이터를 보유하고 있다.

또한 State Holder는 UpStream과 DownStream의 관점에서 두 가지 역할을 더 책임지고 있다.

UpStream - 사용자의 이벤트를 받아 비즈니스 로직을 시작시킨다.

이벤트 수신이란 사용자가 UI와 상호작용할 떄 발생하는 이벤트를 말한다. 예를 들어 사용자가 로그인 버튼을 클릭하면 ViewModel은 해당 이벤트를 감지하게 된다.

이벤트를 수신한 ViewModel은 비즈니스 로직을 시작한다. 예를 들어, 로그인 버튼 클릭 이벤트를 수신하게 되면 ViewModel은 사용자 인증을 위한 API를 호출하거나 DataStore나 Room과 같은 DB에서 사용자 정보를 가져오는 작업을 수행할 수 있다.

DownStream - 상위 레이어로부터 전달 받은 데이터를 UI에 바인딩시킨다.

StateHolder는 비즈니스 로직이 완료된 후, 그 결과를 UI element에 전달하여 화면을 업데이트한다.

예를 들어, 로그인 버튼을 클릭 후 ViewModel에서는 해당 이벤트를 수신 받아 "사용자 인증 API를 호출" 하였다. 그리고 API 호출에 대한 결과로 사용자 인증 성공이나 실패에 대한 데이터를 전달 받는다.

그리고 전달받은 데이터를 UI에 반영하기 위해 ViewModel은 UI의 상태를 업데이트한다. 이때 LiveData나 StateFlow와 같은 상태 홀더 클래스를 사용해 UI가 새로운 상태를 구독하고 자동으로 변경된 사항을 반영하도록 할 수 있다.


2-2. Domain Layer

Domain 계층은 독립적이며 의존성을 가지고 있지 않는 계층으로 비즈니스 로직에 필요한 UseCase가 포함되어있는 계층이다. 의존성이 없어야하기 때문에 다른 계층의 변경이 Domain Layer에 영향을 끼쳐서는 안된다.

UseCase
Domain Layer의 핵심은 UseCase이다. UseCase는 애플리케이션의 비즈니스 규칙을 처리하는 책임을 갖는다. 예를 들어, 사용자가 로그인할 때 발생하는 모든 검증 과정이나 인증 절차가 UseCase에 포함될 수 있다.

class LoginUseCase(private val userRepository: UserRepository) {
    operator fun invoke(username: String, password: String): Boolean {
        val user = userRepository.getUser(username)
        return user?.password == password
    }
}

즉, UseCase의 역할은 다음과 같이 정리할 수 있다.

  • 비즈니스 규칙 실행: 입력된 데이터를 기반으로 비즈니스 규칙을 처리하고 결과를 반환한다.
  • 데이터를 요청하고 저장하는 로직 관리: 외부 계층(데이터 계층, 프레젠테이션 계층 등)과 상호작용하지 않으며, 해당 계층이 도메인 계층의 유스케이스에 필요한 데이터를 제공하는 방식으로 작동한다.

UseCase는 네이밍 컨벤션이 존재한다. 이는 Android DomainLayer에서 권장하고 있는 네이밍 컨벤에 따라 정해진 부분으로 현재 시제의 동사 + 명사/대상(선택사항) + UseCase 의 규칙을 따른다.

그렇다면 UseCase를 사용하는 이유는 무엇일까?
이를 이해하기 위해선 먼저 ViewModel의 역할에 대해 정리가 필요하다.

[ViewModel의 책임]

  • UpStream관점에서 사용자의 이벤트를 받아 비즈니스 로직을 시작시키는 책임
  • DownStream관점에서 상위 레이어로부터 받은 데이터를 UI에 바인딩해주는 책임
  • UI에 바인딩해준 데이터를 상태 홀더 클래스(LiveData or StateFlow)를 통해 보유한다.

따라서 위 책임에 관련되지 않는 부분(비즈니스 로직)을 UseCase패턴으로 분리한다면
1. ViewModel의 역할을 단순화 시킬 수 있어 유지보수가 용이한 구조가 된다.
2. 비즈니스 로직이 독립적으로 유지되므로 코드의 유연성과 확장성이 향상되고 테스트가 용이해진다.
예들 들어, 새로운 데이터 소스나 API를 추가할 때도 UseCase의 비즈니스 로직은 변하지 않고, 오직 데이터 레이어만 수정하면 된다.
3. UseCase는 여러 ViewModel에서도 동일하게 사용할 수 있어 재사용성이 향상된다.
예를 들어, 로그인 기능을 여러 화면에서 사용할 경우, 해당 화면의 ViewModel들은 모두 같은 LoginUseCase를 재사용할 수 있다.

Repository Interface
도메인 계층은 독립적이어야하기 때문에 외부 데이터 소스에 직접 접근하지 않는다. 대신 리포지토리 인터페이스를 사용하여 데이터 소스에 접근합니다. 리포지토리 인터페이스는 데이터베이스, 네트워크, 캐시 등 다양한 데이터 소스에서 데이터를 가져오는 역할을 추상화한다.

Repository Interface는 도메인 계층과 데이터 계층 사이의 연결 고리 역할을 하며, 도메인 계층은 해당 인터페이스를 통해 데이터를 요청한다. 데이터 계층은 이 인터페이스를 구현하여 실제 데이터 소스와 상호작용한다.

interface UserRepository {
    fun getUser(username: String): User?
}

2-3. Data Layer

Domain 계층에 의존성을 가지고 있는 계층으로 Data들을 control 하는 계층(CRUD)이다.

데이터 계층에서는 API 통신에 필요한 DTO, Repository 인터페이스의 구현체, 내부 DB를 사용한다면 Room이나 DataStore 등이 포함될 수 있다.

DataStore
로컬 DB 또는 REST API 통신과 관련된 내용
Entity
로컬 DB의 테이블을 만들기 위한 Entity와 서버 통신을 위한 Dto가 포함된다.
Repository(구현체)
Domain Layer의 Repository Interface의 실제 구현을 담당한다.
CRUD(DataSource)를 사용하여 실제 데이터를 가져온다.
Mapper
Entity -> Model, Dto -> Entity, Dto -> Model 등과 같이 데이터들의 형식을 변환한다.
Data 계층 데이터(받아오는 데이터 형태)와 Domain 계층 데이터(실제로 사용하는 데이터)로 변환해주는 클래스

profile
강승구

0개의 댓글