본문 바로가기

아키텍처

클린 아키텍처 in 안드로이드

 

이전에 클린 아키텍처 개념에 대해 포스팅을 작성했었는데요

그렇다면 클린 아키텍처가 안드로이드에서는 어떻게 사용되는지 살펴보겠습니다

 

 

 

👩🏻‍💻 안드로이드에 적용한 클린 아키텍처

 

 

클린 아키텍처에서 엔터프라이즈 업무 규칙, 애플리케이션 업무 규칙, 인터페이스 어댑터, 프레임워크와 드라이버로 나눠져 있던 청사진입니다

안드로이드에서는 이를 프레젠테이션 계층(Presentation Layer), 도메인 계층(Domain Layer), 데이터 계층(Data Layer) 3개의 계층으로 나누게 됩니다

 

클린 아키텍처 구조에서는 의존성이 무조건 바깥에서 안쪽으로 향하지만 안드로이드에서는 계층을 나누기 때문에 프레젠테이션 계층과 데이터 계층이 도메인 계층을 바라보며 의존성을 가지는 형태로 구성됩니다

 

그리고 각 계층에 해당하는 것들을 살펴보면

  • 프레젠테이션 계층 (Presentation Layer)
    • 뷰 (View) (= UI) : 직접적으로 플랫폼 의존적인 구현, UI 화면 표시와 사용자 입력을 담당, 단순하게 프레젠터가 명령하는 일만 수행
    • 프레젠터 (Presenter) : MVVM의 ViewModel과 같이 사용자 입력이 왔을 때 어떤 반응을 해야 하는지에 대한 판단을 하는 영역, 무엇을 그려야 할지 알고 있는 영역

 

  • 도메인 계층 (Domain Layer)
    • 유즈 케이스 (UseCase) : 비즈니스 로직이 들어있는 영역
    • 모델 (Entity) : 앱의 실질적인 데이터

 

  • 데이터 계층 (Data Layer)
    • 리포지터리 (Repository) : 유즈케이스가 필요로 하는 데이터의 저장 및 수정 등의 기능을 제공하는 영역으로, 데이터 소스를 인터페이스로 참조하여, 로컬 DB와 네트워크 통신을 자유롭게 함
    • 데이터 소스 (Data Source) : 실제 데이터의 입출력이 실행되는 곳

 

 

 

🔗 데이터의 흐름과 의존성

청사진을 보면 데이터가 위에서 아래로, 혹은 아래서 위로 흐르는 것을 볼 수 있습니다

이를 더 자세히 나타낸 이미지로 보면 아래와 같습니다

 

 

 

사용자의 버튼을 클릭하면 UI -> 프레젠터 -> 유즈케이스 -> 엔티티 -> 리포지터리 -> 데이터소스 의 순서로 이동하게 됩니다

 

이때, 위 이미지에서 몇 가지 의문이 드는 부분이 있었는데요

  • 데이터 계층에 있던 Repository는 왜 도메인 계층과 데이터 계층에 걸쳐있지?
  • 도메인 계층이 데이터 계층을 알고 있어야 데이터를 보낼 수 있는 거 아닌가?

 

이는 도메인 계층에서 UseCase로 데이터를 요청할 때, Repository를 참조하게 되는데 이렇게 되면 상위 계층인 도메인 계층이 데이터 계층에 의존성을 가지게 되므로 클린 아키텍처를 위배하게 됩니다

 

이를 해결하기 위해서는 상위 모듈에서 인터페이스를 생성하고 의존 관계를 역전시켜 모듈을 분리해주면 됩니다

이게 바로 "의존성 역전" 이라고 합니다

 

 

"의존성 역전" 이란?
객체 지향 프로그래밍에서 의존성 역전 원칙은 소프트웨어 모듈들을 분리하는 특정 형식을 지칭합니다. 이 원칙을 따르면 상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존 관계를 역전시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다

 

 

 

 

다시 그림을 보고 설명하자면 도메인 계층에서 인터페이스를 만들어두고, 하위 계층인 데이터 계층에서 이를 구현합니다

그리고 유즈케이스는 도메인 계층에 있는 인터페이스를 참조하면 유즈케이스를 사용할 때 데이터 레이어에 의존할 필요가 없고, 데이터 레이어에서 도메인 레이어를 바라보게 되어 의존성 역전이 일어나게 됩니다

 

이렇게 되면 프레젠테이션 레이어와 데이터 레이어가 모두 도메인 레이어를 바라보는 형태가 완성되게 됩니다

 

 

 

💻 실제 코드를 통해 살펴보자

 

클린 아키텍처를 도입했던 다온길 프로젝트에서 코드를 가져왔습니다

관광지 데이터를 화면에 띄우는 경우를 살펴보겠습니다

 

View

 

private fun getAroundPlaceInfo(
	binding: FragmentHomeMainBinding,
	areaCode: String,
	sigunguCode: String
) {
	viewModel.getPlaceMain(areaCode, sigunguCode)

	viewModel.aroundPlaceInfo.observe(requireActivity()) { aroundPlaceInfo ->
		if (aroundPlaceInfo.isNotEmpty()) {
			val aroundPlaceList = aroundPlaceInfo.map {
				AroundPlace(it.address, it.disability, it.image, it.name, it.placeId)
			}
			settingLocationRVAdapter(binding, aroundPlaceList)
		}
	}
}

 

 

홈 화면에 관광지 데이터를 띄워주기 위해 ViewModel을 호출합니다

 

 

 

 

Presenter (ViewModel)

 

@HiltViewModel
class HomeViewModel(
    private val getAreaCodeByNameUseCase: GetAreaCodeByNameUseCase,
    private val getPlaceMainInfoUseCase: GetPlaceMainInfoUseCase,
    private val getSigunguCodeByNameUseCase: GetSigunguCodeByNameUseCase
) : ViewModel() {

    @Inject
    private val _aroundPlaceInfo = MutableLiveData<List<AroundPlace>>()
    val aroundPlaceInfo : LiveData<List<AroundPlace>> = _aroundPlaceInfo

    private val _recommendPlaceInfo = MutableLiveData<List<RecommendPlace>>()
    val recommendPlaceInfo : LiveData<List<RecommendPlace>> = _recommendPlaceInfo

    fun getPlaceMain(area: String, sigungu: String) = viewModelScope.launch {
        val areaCode = getAreaCodeByNameUseCase(area)
        val sigunguCode = getSigunguCodeByNameUseCase(sigungu)
        
        if (areaCode != null && sigunguCode != null) {
            getPlaceMainInfoUseCase(areaCode, sigunguCode).onSuccess {
                _aroundPlaceInfo.value = it.aroundPlaceList
                _recommendPlaceInfo.value = it.recommendPlaceList
            }.onError {
                Log.d("getPlaceMain", it.toString())
            }
        }
    }
}

 

어떤 반응을 해야 하는지 판단하고, 무엇을 그려야 하는지 알고 있는 영역이기 때문에

장소 호출 비즈니스 로직을 담고 있는 getPlaceMainInfoUseCase를 호출했습니다

 

 

 

UseCase

 

class GetPlaceMainInfoUseCase @Inject constructor(
    private val placeMainInfoRepository: PlaceRepository
): BaseUseCase() {

    suspend operator fun invoke(areaCode: String, sigunguCode: String): Result<PlaceMainInfo> = execute {
        placeMainInfoRepository.getPlaceMainInfo(areaCode, sigunguCode)
    }
}

 

비즈니스 로직을 담고 있는 영역인 UseCase 입니다

UseCase는 도메인 계층의 인터페이스로 이루어진 Repository를 참조하고 있습니다

(구현체는 데이터 계층에 속해있습니다)

 

 

 

Repository

 

interface PlaceRepository {
    suspend fun getPlaceMainInfo(areaCode: String, sigunguCode: String): Result<PlaceMainInfo>
}

 

인터페이스로 구현된 Repository로 도메인 계층에 구현되어 있습니다

 

 

 

RepositoryImpl (Repository 구현체)

internal class PlaceRepositoryImpl @Inject constructor(
    private val placeDataSource: PlaceDataSource
) : PlaceRepository {

    override suspend fun getPlaceMainInfo(
        areaCode: String,
        sigunguCode: String
    ): Result<PlaceMainInfo> {
        return placeDataSource.getPlaceMainInfo(areaCode, sigunguCode)
    }
}

 

Repository 구현체로 데이터 계층에 속해 Repository를 참조하고 있으므로 의존성을 역전시켜줍니다

이때 데이터를 어디에서 가져올지 선택합니다 (네트워크 통신, 로컬 DB 등등..)

 

 

 

DataSource

 

internal class PlaceDataSource @Inject constructor(
    private val placeService: PlaceService
){

    suspend fun getPlaceMainInfo(areaCode: String, sigunguCode: String): Result<PlaceMainInfo> = execute {
        placeService.getPlaceMainInfo(areaCode, sigunguCode).toDomainModel()
    }
}

 

데이터 계층의 엔티티를 도메인 계층의 모델로 바꿔주는 역할을 합니다

 

 

 

🍀 마무리

이렇게 직접 코드를 통해 클린 아키텍처와 의존성이 역전되는 과정까지 살펴보았습니다

클린 아키텍처 개념만 보았을 땐 막막했는데 안드로이드에서 적용 되는 모습을 살펴보고,

직접 코드 구현에 적용해보니 클린 아키텍처 개념이 훨씬 와닿았던 것 같습니다

 

역시 추상적인 개념은 직접 코드로 작성해봐야 개념이 와닿는 것 같네요

클린 아키텍처를 구현하면서 했던 새로운 고민들도 다른 포스팅으로 작성해보겠습니다 :)

 

 

 

 

 

 

'아키텍처' 카테고리의 다른 글

클린 아키텍처(Clean Architecture)란?  (0) 2024.09.27