일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 클린아키텍처
- MVVM
- Compose
- ListAdapter
- 컴포즈
- 회원가입
- coroutine
- 뷰
- NavHost
- DiffUtil
- 코틀린
- 알고리즘
- 플레이스토어
- 코딩테스트
- Jetpack
- 리사이클러뷰
- cleanarchitecture
- 로그인
- Authentication
- Android
- sharedFlow
- 파이어베이스
- 커스텀뷰
- XML
- 안드로이드
- Kotlin
- UiState
- Build variants
- NavController
- Flow
- Today
- Total
Grusie 안드로이드 개발 기술 블로그
[Android] 클린아키텍처(CleanArchitecture) 개념 본문
클린 아키텍처의 개념
클린 아키텍처는 계층을 나누어 관심사를 분리하는 것에 초점을 두고 만들어진 아키텍처로서,각 분리된 계층마다 한 가지 역할만 하도록 구현하는 방식이다.
계층 구조에서 외부에서 내부로 의존성을 가지고 있기 때문에, 내부로 갈 수록 의존성이 낮아지게 된다.
- 동작을 수행할 때, 자기보다 내부에 있는 계층에서 변화가 발생하면 동작을 행하는 계층에도 영향이 있을 수 있지만, 외부에 있는 계층이 변화하는 것 때문에 동작을 행하는 계층에 영향이 있으면 안 된다.
공부하며 찾아본 모든 블로그에 있던 이미지
- 클린 아키텍처에서 의존성은 단방향으로만 이루어져 있다(외부 -> 내부).
이건 클린 아키텍처의 기본 구조를 나타내는 것이고, 안드로이드 기준으로는 다른 이미지를 보면 조금 더 이해가 수월할 것이다.
안드로이드 관점에서의 클린아키텍처
안드로이드를 기준으로 하였을 때, 이렇게 계층을 나눌 수 있다.
:Presentation, :Domain, :Data
각 계층별 설명
- 프레젠테이션(Presentation) 계층
- 뷰(View) : 직접적으로 사용자에게 보여질 부분을 담당한다. 프레젠터에 의해 화면을 그린다.
- 프레젠터(Presenter) : MVVM의 ViewModel과 같이, 사용자에게 입력이 왔을 때, 어떻게 반응을 해야 하는지 판단을 하고, 서버 통신이 수행된 후의 결과를 받았을 때 뷰에게 UI를 나타내도록 명령을 하는 역할도 한다.
- 도메인(Domain) 계층
- 유즈 케이스(Use Case) : 비즈니스 로직이 들어 있는 영역이다. 동작을 수행할 기능 별로 유즈 케이스를 작성하며, 보통 레퍼지토리 패턴과 함께 쓰여, 레퍼지토리를 호출하는 형태로 구현한다.
- 모델(Entity) : 앱의 실질적인 데이터라고 볼 수 있다. 외부의 변경으로부터 보호가 되도록 구현하며, 데이터 계층에서 받은 데이터를 사용가능한 데이터로 가공하여 가지고 있는 부분이다.
- 데이터(Data) 계층
- 레퍼지토리(Repository) : 유즈 케이스가 필요로 하는 데이터의 저장 및 수정 등의 기능을 제공하는 영역으로, 데이터 소스를 인터페이스로 참조하여, 로컬 DB와 네트워크 통신을 자유롭게 할 수 있다.
- 데이터 소스(Data Source) : 실제 데이터 입출력이 실행된다.
이제 실제 안드로이드 개발에 실제로 사용될 계층들을 보면서 이해해보자
안드로이드에서의 각 모듈별 설명
1. 프레젠테이션(Presentation) 모듈
- presentation 계층은 UI와 관련된 코드들을 캡슐화한다. 모든 UI와 관련된 컴포넌트 혹은 안드로이드 프레임워크와 관련된 코드들을 이 계층에서 다룬다.
- presentation 모듈은 domain 모듈에 의존한다. 의존하는 모듈에 대해서는 블로그마다 다른데, 어떤 예제들은 domain과 data 모듈 둘 다 의존하도록 되어있으나, 필자는 domain 모듈에만 의존하도록 하였다. 이유는 서버통신을 진행할 때, presentation모듈에서, domain계층의 useCase를 호출하고, useCase에서 data모듈에 있는 repository를 호출하기 때문에 presentaion 계층에서 data계층을 의존할 필요가 없다고 생각해서이다.
2. 도메인(domain) 모듈
- 비즈니스 로직을 격리하기 위해서 domain 모듈을 구현하는 것이 좋다. 사실 domain 모듈이 필수가 아니라 옵션이라고는 하지만, 필수라고 생각 할 정도로 중요하다고 본다.
- domain 계층은 비즈니스 로직을 담고 있기에, 망쳐지지 않도록 어떠한 계층에도 의존하지 않는다.
- domain 모듈은 다음과 같은 코드들을 포함한다.
- Entity : 특정 영역을 표현하는 객체 ex) DTO, VO 등
- UseCase : Entity와 함께 비즈니스 로직을 수행한다. (프레젠테이션 영역에서 주입 받아 호출된다.)
- Repository Interface : Data계층의 Repository와 연결될 인터페이스이다. (UseCase에서 받은 이벤트를 토대로 Repository에 서버통신을 요청할 때 사용한다.)
3. 데이터계층(Data) 모듈
- Data 모듈은 데이터 소스(로컬 DB, 서버 등)와 상호작용을 담당하는 코드가 포함되는 곳이다. data모듈은 domain모듈에 의존한다.
- data 모듈은 다음과 같은 두 가지 책임을 갖는다.
- 데이터 입출력 코드를 하나의 계층에서 관리한다.
- 데이터 소스들과 데이터를 소비하는 다른 계층과의 경계를 둔다.
- data모듈에서는 domain 모듈에서 정의한 Repository 인터페이스를 구현한다.
코드 예제
예제로 쓰인 모든 코드들은 필자가 클린 아키텍처를 공부하며 개발중인 청년정책 앱에서 쓰인 코드들이다.
1. 프레젠테이션 계층
@HiltViewModel
class PolicyListViewModel @Inject constructor(private val policyUseCases: PolicyUseCases) :
ViewModel() {
...
private fun getPolicyList() {
viewModelScope.launch {
setPolicyUiState(PolicyListUiState.Loading)
try {
policyUseCases.getPolicyListUseCase().cachedIn(viewModelScope)
.collect { pagingData ->
_policyList.emit(pagingData)
setPolicyUiState(PolicyListUiState.Success)
}
} catch (e: Exception) {
setPolicyUiState(PolicyListUiState.Error(e))
}
}
}
...
}
- 뷰모델에서 도메인 모듈의 UseCase를 주입 받아, 사용자의 입력을 받거나, 필요해 의해 UseCase의 함수를 호출한다.
2. 도메인 계층
class GetPolicyListUseCase(private val repository: PolicyRepository) {
suspend operator fun invoke() = repository.getPolicyList()
}
- 도메인 계층에서의 UseCase에선, Repository 인터페이스를 파라미터로 받아, 프레젠테이션 계층에서 넘어온 동작에 의해 Repository 인터페이스의 함수를 호출한다.
interface PolicyRepository {
suspend fun getPolicyList() : Flow<PagingData<PolicySimple>>
suspend fun getPolicyInfo(policyId:String) : Flow<PolicyDetail>
}
- Repository 인터페이스에선 동작할 함수들을 정의해 둔 후, data 계층에서 실제로 구현을 한다.
3. 데이터 계층
class PolicyRepositoryImpl @Inject constructor(
private val policyRemoteSource: PolicyRemoteSource,
private val policyLocalDataSource: PolicyLocalDataSource
) :
PolicyRepository {
override suspend fun getPolicyList(): Flow<PagingData<PolicySimple>> =
policyRemoteSource.getPolicyList().map { pagingData ->
pagingData.map {
it.toPolicySimple()
}
}
override suspend fun getPolicyInfo(
policyId: String
): Flow<PolicyDetail> {
return policyLocalDataSource.getPoliciesFromDB(
policyId = policyId,
).map { it.toPolicyDetail() }
}
}
- 도메인 계층에서 선언한 Repository의 구현코드로서, 데이터 소스를 주입 받아, 서버통신 후 결과를 필요한 데이터로 변환 후 반환한다.
interface PolicyLocalDataSource {
fun getPoliciesFromDB(policyId: String): Flow<PolicyItem>
}
class PolicyLocalDataSourceImpl(private val policyInfoDao: PolicyInfoDao) : PolicyLocalDataSource {
override fun getPoliciesFromDB(policyId: String): Flow<PolicyItem> = policyInfoDao.getPolicy(policyId)
}
- 로컬 DB를 사용하는 DataSource로서, 실질적인 통신을 담당한다. 위에 말했던, Repository에서 주입받아 사용된다.
- 로컬 DB를 사용하는 코드이기에, Dao를 직접 호출하였고, 만약 서버통신이 필요하다면, Service를 따로 구현하여 호출해야 한다.
참고
https://www.charlezz.com/?p=45391
후기
이렇게 클린아키텍처의 개념과 각 계층의 역할, 안드로이드에서의 사용법 등을 예제와 함께 알아보았다.
공부하며 익힌 것을 토대로 작성하였고, 아직까지 많이 부족하기에 더더욱 공부해나갈 예정이다.
'안드로이드 개발 > 클린아키텍처' 카테고리의 다른 글
[Android] 클린아키텍처에서 Room DB 사용해, 임시저장 기능 구현하기 (1) | 2024.05.20 |
---|---|
[Android] 클린 아키텍처 회사 프로젝트에 적용하기 (0) | 2024.03.26 |