본문 바로가기
안드로이드/기타

Android Clean Architecture 와 Android App Architecture - 1

by 나이아카 2024. 6. 9.

 회사에서 작업을 진행하다가, 뭔가 잘못 사용하고 있는 부분들이 있나 싶어서 다시 확인해봤습니다. 아키텍처를 적용하는 부분에 있어 다른 사람에게 설명하는데 뭔가 어색하거나 이해가 가지 않는 부분이 있더라구요(제가 짠 코드인데!). 새로운 것도 아니고 이제는 익숙할 때도 됐는데 아직까지 매 번 코드로 적용하는 과정에서 어색함이 느껴진다는 게 웃기기도 하지만, 어쩌겠어요. 다시 정리해야죠.

 그래서 이번 주제는 안드로이드에서 사용하는 클린 아키텍처와 안드로이드 앱 아키텍처 가이드에 대해서 소개하고 비교하는 주제로 글을 써볼까 합니다. 일단 첫 번째로 클린 아키텍처에 대해서 다시 정의해야겠어요. 하지만, 이미 많은 블로그에서 이와 같은 부분을 정의하고 있고 사실 거기만 봐도 이론적인 부분은 어느 정도 다 정리되었다고  판단할 수 있으니 조금씩 생략하면서 진행하겠습니다.


Clean Architecture

Though these architectures all vary somewhat in their details, they are very similar. They all have the same objective, which is the separation of concerns.

 대부분의 아키텍처는 세부적인 부분에서 다소 다르지만, 매우 유사한 부분이 있습니다. 아키텍처들은 관심사 분리라는 동일한 목표를 지니고 있습니다.

 클린 아키텍처를 설명하는 블로그에 들어가면 위와 같이 소개하고 있습니다. 이는 클린 아키텍처 뿐 아니라 많은 아키텍처들이 관심사를 분리하려는 방향으로 진행한다는 것을 설명하려는 부분임을 알 수 있는데요. 클린 아키텍처 역시 기본적인 아키텍처의 방향성에 맞췄다고 이야기하네요.

관심사 분리(Separation of Concerns)

A program that embodies SoC well is called a modular program

SoC를 잘 구현한 프로그램을 모듈러 프로그램이라고 합니다.

Layered designs in information systems are another embodiment of separation of concerns (e.g., presentation layer, business logic layer, data access layer, persistence layer).

 information systems에서의 계층형 설계는 SoC의 또 다른 구현 방식입니다.

 관심사 분리란 크게 각 섹션을 계층화 혹은 캡슐화 시켜 다른 섹션과 공유하지 않은 상태를 유지하는 것을 의미합니다. 이는 각 계층이 분리되어 서로가 서로의 계층을 침범하지 않은 상태로 합의된 input - output을 공유하는 것으로 정의됩니다.

 

안드로이드 Clean Architecture

안드로이드 클린 아키텍처

 클린 아키텍처를 표현하는 여러 구조도가 있지만, 위 구조도가 저에게 제일 이해하기 편한 구조라 가져왔습니다. 각 레이어를 기준으로 이야기해 보겠습니다.

 

Presentation Layer

 먼저 UI를 구성하는 Presentation Layer입니다. 여기는 UI를 구성하고 있기 때문에 프레임워크(안드로이드) 의존적입니다. 당연하게도 Android의 UI는 import할 때 android 혹은 androidX로 시작하기 때문에 안드로이드 프레임워크 없이는 이 레이어를 구성할 수 없습니다. 멀티플랫폼으로 활용 가능하다는 Kotlin의 Compose도 안드로이드 프레임워크 내에서는 Androidx.compose를 사용하고 있습니다.

 저는 여기를 주로 UiState, ViewModel, View의 조합을 사용합니다. UiState의 경우, 주로 하나의 data class로 한 화면을 묶어두나 리스트 형태가 아닌 경우에는 대부분이 그렇듯 각 필드들을 분리시켜 놓는 경우가 많습니다(하나의 수정에도 모든 UI가 업데이트되는 경우를 방지하기 위함입니다).

 그리고 위 표에서는 Domain Layer와 Data Layer 모두를 참조하고 있지만, 저는 개인적으로 Domain Layer만 참조하여 사용합니다. 아무대로 Presentation Layer에서 직접적으로 Data Layer를 참조할 일을 없게 개발을 진행하고 있기 때문에, 아예 Data Layer와 Presentation Layer의 의존성을 분리시켜 Domain을 통해 작업하도록 코드를 진행합니다.

View

Activity 내부에 Fragment와 ViewHolder, CustomComponent(혹은 ComposeUI) 등을 조합해 구성합니다. 기본적으로 SAA를 적용하기 때문에 Activity는 한 프로젝트 당 많아도 3개 이하로 작성합니다(전부 Compose로 구성되는 경우 아예 1 Activity로의 작성도 어렵지 않은 것 같더군요).

ViewModel

AAC ViewModel을 사용하지만, MVVM 구조를 따르기 위해 View에서는 비즈니스 로직 및 데이터 관리를 전혀 하지 않도록 코드를 작성합니다. View에서는 ViewModel에게 Event를 전달하고, ViewModel은 Action을 전달하는 방식으로 작업할 수 있습니다. 여기서 Event는 ViewModel의 메소드로, action은 SharedFlow(+ Coroutine)를 이용해 collect해 reactive한 코드를 작성할 수 있도록 작업합니다. 관리를 쉽게 할 수 있도록 Event와 Action은 아래와 같이 sealed class로 작업했습니다.

sealed class ImageDetailAction {
    object MoveToBack : ImageDetailAction()
}

sealed class ImageDetailEvent {
    object BackBtnClicked: ImageDetailEvent()
    object LeftMoveBtnClicked: ImageDetailEvent()
    object RightMoveBtnClicked: ImageDetailEvent()
}

interface ImageDetailViewModel: ViewModel {
    val actions: SharedFlow<ImageDetailAction>
    
    fun handleEvent(receiveEvent: ImageDetailEvent)
}

 실제 ViewModel에는 저 데이터만 들어가는 것은 아니지만, Action & Event를 저런식으로 표현한다고 말하기 위해 작성했습니다. interface를 통해 구현부와 View에서 사용하는 부분을 분리하고 있습니다. 이는 테스트 목적도 있지만, 초기 API 연결 전 UI를 확인하기 위해 dummy ViewModel을 만들때 사용하기 위함도 있습니다. 이러한 ViewModel안에는 UiState와 DataBinding이 목적인 여러 변수들(주로 StateFlow로 작성되는)이 들어가서 ViewModel 내부에서 필드 값들을 변경해주는 것으로 자연스럽게 UI가 변경될 수 있도록 작업합니다.

 

Domain Layer

 Domain Layer의 경우 다른 레이어와 참조가 없기 때문에 다른 계층의 변경에 독립적입니다. 만약 다른 계층에서 변경점이 필요하다면 고정된 Domain Layer의 파라미터 및 리턴 값을 가지고 구조 변경을 진행해야 한다는 의미입니다. 이는 역으로 Domain Layer에서 무언가를 바꾸기 위해서는 그 것을 사용하는 모든 다른 계층을 변경해야 할 필요가 있다는 것을 의미합니다.

 프로젝트 설계를 진행할 때 다른 Layer들 보다 이쪽부분부터 자연스럽게 진행하게 됩니다. 아무래도 API DataSource 및 Repository에서 전달 받은 데이터가 어떠한 값이든 상관 없이 안드로이드에서 사용하려는 Entity를 지정하는 부분이기 때문에 독립적으로 설계할 수 있고(물론 그 전에 서버에서 줄 수 있는 데이터와 없는 데이터에 대한 커뮤니케이션은 필요하겠지만) 이를 이용해 Presentation Layer를 작업할 수 있게 됩니다(물론 대부분 Presentation Layer를 작업하다보면 부족한 데이터나 불필요해지는 데이터가 등장하기도 하는데, 이는 아직 제 실력이나 경험의 이슈가...). 한 번 잘 설계해두면 크게 고칠 부분이 없는 단계이기도 합니다.

Repository

 먼저 Domain Layer에 있는 Repository는 interface입니다. 이는 Domain Layer가 Data Layer를 참조할 수 없기 때문에, 미리 interface로 Repository를 지정해두는 방식입니다. 이렇게 Repository Interface가 Domain Layer에 존재하고, 이를 Data Layer의 RepositoryImpl이 상속받아 사용한다면 Domain Layer의 UseCase는 DI를 통해 Repository를 주입받게 되고 이로 인해 내부 구현과 독립적으로 구성할 수 있게 됩니다. 사용법은 아래 UseCase 설명 부분의 코드에서 확인할 수 있습니다.

UseCase

 UseCase는 하나의 비즈니스 로직을 감싸둔 클래스입니다. 구현 방식은 아래의 코드처럼 작업을 했습니다. 먼저 위에서 설명했듯, RunningTalkRepository는 interface이며 이를 구현한 RunningTalkRepositoryImpl은 Data Layer에 존재합니다. 이를 Di를 통해 받아서 invoke 내부에서 가공처리를 거칩니다. 하지만 아래 예제 코드에서는 그냥 메소드를 던지기만 하는데, 이는 이미 repository의 return value가 UseCase가 반환할 값에 맞춰져 있기 때문입니다. 만약, repo.getRunningTalks()에서 특정 데이터만 분리해 다른 UseCase(예를 들어 GetRunningTalkDateUseCase)를 만든다면, 거기에서 필요한 데이터만 분리해서 다시 전달하는 코드를 작성할 수도 있을 것입니다.

class GetRunningTalkUseCase @Inject constructor(private val repo : RunningTalkRepository) {
    operator fun invoke() : Flow<CommonResponse> = flow {
        runCatching {
            emit(CommonResponse.Loading)
            repo.getRunningTalks()
        }.onSuccess {
            emit(it)
        }.onFailure {
            it.printStackTrace()
            emit(CommonResponse.Failed(999, it.message?:"error"))
        }
    }
}

 

Entity

 Domain Layer에서 사용할 데이터입니다. Presentation Layer에서 실제로 사용되거나 Data Layer에 전달하기 위한 필수적인 데이터를 가지고 있습니다. 이를 Presentation Layer가 직접 사용할 수도 있고, UiState와 같은 또 다른 구조로 변환해서 사용할 수 있습니다. 이 Entity를 통해 각 Layer가 소통을 합니다(Model로 표현될 수 있습니다). 다른 프레임워크의 클린 아키텍처에서는 Entity라는 이름이 Data Layer에 존재하는 경우가 있더라구요. 하지만 안드로이드에서 Entity라는 이름을 쓰는 모델은 실제로 Clean Architecture의 Entity의 개념이라기 보다는 Domain Layer에서 사용하는 DataModel이라고 생각해주시면 되겠습니다.

 

Data Layer

 Data Layer는 외부 및 내부와의 통신을 담당하는 계층입니다. 그래서 주로 Retrofit이나 SharedPreferences와 같은 클래스들을 이용해 데이터를 가져오거나 저장하는 역할을 합니다. 그래서 이 부분은 혼자 모든 것들을 작업하는 경우가 아니라 팀원들과 같이 커뮤니케이션을 통해 작업을 진행하는 경우, 안드로이드 개발자의 입김을 통해 생각대로 구성하는 것이 굉장히 어렵지 않나 생각합니다.

 코드는 주로 Repository에서 DataSource를 사용하며, DataSource의 DTO등을 DataModel로서 이용합니다. 하지만 규모가 작은 프로젝트의 경우 DataSource가 생략되고, 그 역할을 Repository가 대체하는 경우가 많습니다.

DataSource

 외부와의 통신은 주로 Retrofit, Http3 등의 네트워크 통신 라이브러리를 통해 작업하고(네트워크 통신 전용 라이브러리는 회사에서 공개하지 않은 라이브러리까지 포함하면 굉장히 많습니다.), 로컬은 주로 Room, SQLite, MediaStore, SharedPreferences등 휴대폰 단말 내부에 저장을 지원하는 여러가지 툴을 이용합니다. 이들을 가져오는 호출부를 DataSource로 이용할 수 있습니다.

Respository

 여기서의 Repository는 Domain Layer의 interface를 implementation한 Repository입니다. Domain Layer가 다른 계층을 참조하지 않기 위해 만들어 둔 interface를 통해 작성합니다. 여기서 주로 Di를 통해 DataSource나 Retrofit의 API Interface를 주입받아 각 메소드 별로 설정해서 사용하곤 합니다. 또한 메모리 캐싱을 위한 데이터 저장을 같이 관리하기도 합니다. 이 부분은 DataSource가 있는 경우 주로 DataSource에서 작업됩니다.

Data Model

 대부분 외부 및 내부에서 미리 정의된 커뮤니케이션 로직에 따라 구성됩니다. 외부 서버에서 전달받는 경우 서버에 미리 정의된 데이터의 형식에 맞춰 제작되고, 로컬에 저장되는 경우에도 마찬가지로 로컬에 정의된 룰에 맞춰 작업을 진행합니다. 이는 주로 안드로이드 개발자 혼자 설정하기보다는 프로젝트 팀의 상황과 역할에 맞게 여러 구성원들이 합의해서 처리하는 경우가 대부분입니다.

Mapper

 Mapper는 Data Model을 Domain Layer에서 사용할 Entity로 전환하거나 그 반대 역할을 하는 부분입니다. 주로 서버에서 전달받은 데이터 중 필요 없는 데이터를 제외하고 Dmoain Layer에서 사용할 수 있도록 데이터를 가공해서 다시 전달하는 역할을 합니다. 이름 그대로 사용한다고 볼 수 있습니다. 

 


 이번 게시글은 클린 아키텍처에 대해서 짧게 정리해봤습니다. 상세한 부분들은 다른 게시글에서도 잘 표현되어 있어 최대한 짧고 간결하게 표현하려고 노력했습니다. 거기다 다음에는 안드로이드 앱 아키텍처 가이드를 다루고, 그 두 개를 비교하는 부분까지 작성해보고 싶어서 계속 정리할 것 같네요.

 클린 아키텍처를 사용하면서 매 번 느끼는 건데 미니미한 프로젝트에서 사용하기에는 확실히 보일러 코드가 많이 생겨 불편하다는 것을 많이 느낍니다. 굳이 싶은 부분들도 많은데, 너무 악착같이 아키텍처를 적용하려는 부분이 좋은 지 모르겠습니다. 모든 프로젝트는 유지보수 하다보면 규모가 커지기는 하니 처음부터 잘 잡는게 중요한가 싶기도 하지만, 필요 없는 코드를 아키텍처를 위해 꼭 작성해야 하는지는 매 번 이야기할 주제가 되는 것 같습니다. 

 틀린 부분에 대한 지적은 언제든 환영입니다.

 

참고

https://en.wikipedia.org/wiki/Separation_of_concerns

 

Separation of concerns - Wikipedia

From Wikipedia, the free encyclopedia Design principle for computer programming Diagram illustrating the principle of separation of concerns, which says that an action entity can only contain a single type of tasks In computer science, separation of concer

en.wikipedia.org

 

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

 

Clean Coder Blog

The Clean Architecture 13 August 2012 Over the last several years we’ve seen a whole range of ideas regarding the architecture of systems. These include: Though these architectures all vary somewhat in their details, they are very similar. They all have

blog.cleancoder.com

https://meetup.nhncloud.com/posts/345

 

[Android] 요즘 핫한 Clean Architecture 왜 쓰는 거야? : NHN Cloud Meetup

[Android] 요즘 핫한 Clean Architecture 왜 쓰는 거야?

meetup.nhncloud.com

https://medium.com/cj-onstyle/android-%EB%B2%84%ED%8B%B0%EC%BB%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%9D%98-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EB%8F%84%EC%9E%85-a26d833e103c

 

왜 Android 신규 프로젝트는 클린 아키텍처를 도입하였는가

소프트웨어 개발은 복잡한 문제를 해결하기 위해 코드를 작성하는 것 이상을 필요로 합니다. 단순히 결과론으로 화면을 보여주는 것만 생각하고 개발한다면, 코드는 점차 커지며 나중에는 손볼

medium.com

https://charlezz.com/?p=45391

 

안드로이드에 클린 아키텍처를 도입한다면 어떻게 될까? | 찰스의 안드로이드

안드로이드와 클린 아키텍처 안드로이드 앱 아키텍처에 관심을 갖게 되면 필히 접하는 키워드, 클린 아키텍처에 대해서 알아보려고 한다. 클린 아키텍처 "If you want to go fast, if you want to get done qui

charlezz.com

 

댓글