안드로이드 Clean Architecture (2)

ansehun·2023년 3월 12일
0

Clean Architecture

앞선 글에서는 Clean Architecture에 대한 이론적인 부분에 대해 설명했다. 이번 글에서는 실제로 구현한 내용에 대해서 작성해보려고 한다!

위의 그림은 Clean Architecture에 대한 구조를 다시 작성해봤다. 이벤트의 진행 방향과 의존성 방향에 대해 헷갈려 이해하는데 오래 걸렸다. 위 그림은 우아콘을 보고 그려본 구조이다. 가장 기본적인 구조이며 실제로 구현할 때는 Mapper와 같은 요소들이 추가되기도 한다.

Presentation, Domain, Data Layer 모듈화

우선 각각의 Layer에 대해 모듈화를 진행했다. 각 Layer에 해당하는 Package를 만들었다(Presentation Layer는 App Package임) 이후에는 의존성을 제공해야 한다. Presentation(App) -> Domain, Data -> Domain 방향으로 의존성을 가진다.

	//data package gradle dependencies
    
    implementation project(":domain") 
	//app package gradle dependencies
    
    implementation project(":domain")
    implementation project(":data") 

Presentation Layer

View

View는 UI를 그리는 역할을 한다. UI에 보여지는 데이터들은 Presenter에서 결정되며 이 데이터를 바탕으로 UI에 그리는 역할을 한다. 프로젝트에서는 Databinding(Viewbinding)을 사용하여 구현했다.

	// 함수명에 대해서는 아직 깔끔하지 않다(수정 예정)
    // SavedMonth(ArrayList)라는 데이터를 UI에 바인딩하기 위한 코드
    
    viewModel.getSecondSaveMonthInfo()
    viewModel.secondSaveMonthDTOs.observe(viewLifecycleOwner) { state ->
    	when(state) {
        	is UiState.Success -> {
              	...
            }
            is UiState.Failure -> {
            	...
            }
            is UiState.Loading -> {
               ...
            }  
    }

Presenter

Presenter는 UI에 바인딩할 데이터를 결정한다. 우선 Use Case를 의존성 주입하고 이를 통해 SavedMonth(ArrayList) 데이터를 가져와 LiveData에 할당한다.

아래 코드는 domain package에서 Use Case를 의존성 주입한 코드이다. 필자가 Use Case에 대한 코드를 작성했기 때문에 @Inject constructor() 을 통해 의존성 주입을 구현했다.

    @HiltViewModel
    class SecondViewModel @Inject constructor (
    	private val getSavedTimeMonthUseCase: GetSavedTimeMonthUseCase,
        .
        .
        .
    ) {
     	.
        .
        .
    }

아래 코드는 원하는 데이터를 가져와 LiveData에 최신 데이터를 할당하는 코드이다. getSecondSaveMonthInfo()라는 Method를 호출하면 데이터를 받아와 LiveData에 할당할 수 있다.

    // sMs = savedMonthDTOs (줄넘김으로 인해 요약해서 작성)
	private val _sMs =MutableLiveData<UiState<ArrayList<SavedTimeMonthModel>>>()
    val sMs : LiveData<UiState<ArrayList<SavedTimeMonthModel>>>
        get() = _sMs

    fun getSecondSaveMonthInfo() {
        getSavedTimeMonthUseCase(viewModelScope) {
            _secondSaveMonthDTOs.value = it
        }
    }

Domain Layer

Use Case

Use Case는 Application별 비즈니스 규칙에 대해 캡슐화하고 있다. 예를 들어 SavedMonth(ArrayList) 데이터를 가져오고 싶다면 이 기능을 제공할 수 있는 동작을 정의해야 한다. Use Case는 하나의 동작에 대해서만 코드를 작성하고 파일명을 읽었을 때 무슨 동작을 하는지 확실하게 알아야 한다.

또한, UiState로 Use Case에서 정의된 동작을 실행했을 때 성공, 실패, 로딩 여부를 State 패턴으로 바꿔 결과를 받아올 수 있다.

	operator fun invoke (
        scope: CoroutineScope,
        onResult: (UiState<ArrayList<SavedTimeMonthModel>>) -> Unit
    ) {
        scope.launch(Dispatchers.Main) {
            try {
                onResult(UiState.Loading)

                // withContext로 대체 가능하긴 함
                val savedTime = async (Dispatchers.IO) {
                    savedTimeRepository.getSavedTimeMonthModel()
                }.await()

                onResult(UiState.Success(savedTime))
            } catch (e : Exception) {
                onResult(UiState.Failure("Failure"))
            }
        }
    }

Use Case를 구현할 때 알아야할 사항이 하나 더 있다. 바로 Repository에 대해 의존 역전의 법칙이 일어난다!

  class GetSavedTimeMonthUseCase @Inject constructor(
      private val savedTimeRepository: SavedTimeRepository (Interface임)
  )

의존 역전의 법칙을 사용하는 이유?
의존 역전의 법칙이란 객체지향 프로그래밍의 5번째 원칙으로써 모듈을 분리할 수 있는 형식을 지칭한다. 저수준 모듈(구체화)에 의존하는 것이 아니라 고수준 모듈(추상화)에 의존하여 고수준 모듈이 저수준 모듈의 구현으로부터 독립될 수 있다.

Repository(Interface)

Domain Layer에는 Repository의 Interface가 존재한다. 이것이 Domain Layer에 존재하는 이유에 대해 의문이 있었지만 조금만 생각해보면 이유를 쉽게 찾을 수 있다.

  • Domain Layer는 의존 관계가 없고 독립적이기 때문에 추상화된 요소들을 담고 있어야한다.
  • 안드로이드에서 이벤트 진행 방향은 Use Case -> Repository이다. 하지만, Domain Layer 독립적이기 때문에 Data Layer에 대해 알 수 없 수 없다. 즉 Data Layer의 Repository에 대한 정보를 알 수 없다는 것이다. 이를 해결해주기 위해서 Repository의 Interface를 Domain Layer에 둬서 해결할 수 있다.

Entity

실질적으로 사용되는 데이터이다. 필자는 ~~Model로 data class를 만들었다(Entity로 하는게 맞는 것 같다...) Use Case에서는 이 Entity를 통해 Repository에 구체적인 데이터(DTO)를 요구할 수 있다.

    data class SavedTimeMonthModel (
        var month :Int = 0,
        var year : Int = 0,
        var count : Int = 0,
    ) : Parcelable

Data Layer

Repository

Use Case에서 필요로 하는 데이터들을 저장, 받아오는 기능이 집합된 영역이다. 의존성 주입을 통해 Data Source의 Interface를 의존성 주입하여 Database로 부터 실제 데이터를 받아올 수 있다.

    override suspend fun getSavedTimeDayModel(): ArrayList<SavedTimeDayModel> {
        return savedTimeDataSource.getSavedTimeDayEntity().map {
            SavedTimeMapper.mapperToSavedTimeDayModel(it)
        }.toCollection(ArrayList())
    }

    override suspend fun getSavedTimeMonthModel(): ArrayList<SavedTimeMonthModel> {
        return savedTimeDataSource.getSavedTimeMonthEntity().map {
            SavedTimeMapper.mapperToSavedTimeMonthModel(it)
        }.toCollection(ArrayList())
    }

Mapper

DTO와 Entity 사이의 데이터를 매핑하는 역할을 한다. 데이터를 가져올 때는 DTO를 Entity로 바꿔 Use Case에서 사용하고 데이터를 저장할 때는 Entity를 DTO로 바꿔 Data Source에서 DTO를 저장할 수 있게 한다.

    fun mapperToSavedTimeMonthModel(savedTimeMonth: SavedTimeMonthEntity) : SavedTimeMonthModel {
        return SavedTimeMonthModel(
            count = savedTimeMonth.count,
            month = savedTimeMonth.month,
            year = savedTimeMonth.year,
        )
    }

    fun mapperToSavedTimeDayEntity(savedTimeMonth: SavedTimeMonthModel) : SavedTimeMonthEntity {
        return SavedTimeMonthEntity(
            count = savedTimeMonth.count,
            month = savedTimeMonth.month,
            year = savedTimeMonth.year,
        )
    }

Data Source

실제로 데이터의 입출력이 일어나는 곳이다. 필자는 Firestore를 사용했기에 Firestore로부터 데이터를 저장, 가져오는 코드를 작성하여 CRUD가 일어나도록 했다.

 override suspend fun getSavedTimeDayEntity(): ArrayList<SavedTimeDayEntity> {
        return firebaseFirestore.collection(...).document(...)
            .collection(...).document(...)
            ...await()
            .toObjects<SavedTimeDayEntity>().toCollection(ArrayList())
    }

마무리

Clean Architecture에 대해 정리하는 시간을 가졌다. Clean Architecture의 개념에 대해서는 잘 이해했다고 생각하지만, 실제 코드로 작성할 때 부족한 부분이 조금 있는 것 같다.

  • interface의 필요성을 완벽히 느끼지 못했다. -> 이 부분은 코딩을 하다보면 나아질 것 같다.
  • 의존 역전 법칙에 대한 필요성을 완벽히 느끼지 못했다. -> 이 부분은 예제 코드를 찾아보면서 다시 이해를 해봐야 할 것 같다.
  • 변수명, 함수명에 대한 철학이 아직은 부족하다(대상에 대한 단수, 복수에 대한 정보를 파일명, 함수명 등에 녹이지 못하는 것 같다) -> 이름을 잘 짓지 못해 다시 리펙토링하면서 시간이 많이 소비된다 ㅠㅠ
  • 신입 개발자로써 부족함을 정말 많이 느끼고 있다. 하지만, 부족함을 장점으로 바꾸고자 하는 마음가짐으로 계속해서 노력하면 실력을 향상시킬 수 있지 않을까...! 앞으로 계속해서 노력해봐야겠다 ㅎㅎ

(해당 블로그 글에 대해 아직 부족한 부분이 있을 수 있습니다, 피드백은 언제나 환영입니다!)

0개의 댓글